This is an automated email from the ASF dual-hosted git repository.
mcgilman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new c3db6ca409 [NIFI-14327][NIFI-14249] - Copy/Paste handling when
clipboard is not available (#9701)
c3db6ca409 is described below
commit c3db6ca409b87cf3e178b22a9dd5b25e67ef3b43
Author: Rob Fellows <[email protected]>
AuthorDate: Thu Feb 13 15:00:02 2025 -0500
[NIFI-14327][NIFI-14249] - Copy/Paste handling when clipboard is not
available (#9701)
* [NIFI-14327] - Verify clipboard access before allowing copy
* [NIFI-14249] - Remove paste button from operation panel
* removed unused methods and actions per review feedback
* add check for clipboard access to the copy directive
* remove circular dependency
This closes #9701
---
.../flow-designer/service/canvas-utils.service.ts | 11 ++-
.../pages/flow-designer/state/flow/flow.actions.ts | 4 +-
.../pages/flow-designer/state/flow/flow.effects.ts | 80 ++++++----------------
.../flow-designer/ui/canvas/canvas.component.ts | 4 ++
.../operation-control.component.html | 8 ---
.../operation-control.component.ts | 8 ---
.../src/directives/copy/copy.directive.spec.ts | 13 +++-
.../shared/src/directives/copy/copy.directive.ts | 41 ++++++-----
8 files changed, 71 insertions(+), 98 deletions(-)
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index d0c16a765b..0995d2799e 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -956,7 +956,16 @@ export class CanvasUtils {
return { x, y };
}
+ public isClipboardAvailable(): boolean {
+ // system clipboard interaction requires the browser to be in a
secured context.
+ return window.isSecureContext && Object.hasOwn(window,
'ClipboardItem');
+ }
+
public isCopyable(selection: d3.Selection<any, any, any, any>): boolean {
+ if (!this.isClipboardAvailable()) {
+ return false;
+ }
+
// if nothing is selected return
if (selection.empty()) {
return false;
@@ -1001,7 +1010,7 @@ export class CanvasUtils {
}
public isPastable(): boolean {
- return this.canvasPermissions.canWrite;
+ return this.isClipboardAvailable() && this.canvasPermissions.canWrite;
}
/**
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 838cd64004..232f4ab422 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -112,7 +112,7 @@ import {
import { StatusHistoryRequest } from '../../../../state/status-history';
import { FetchComponentVersionsRequest, RegistryClientEntity } from
'../../../../state/shared';
import { ErrorContext } from '../../../../state/error';
-import { CopyRequest, CopyResponseContext, CopyResponseEntity } from
'../../../../state/copy';
+import { CopyResponseContext, CopyResponseEntity } from
'../../../../state/copy';
const CANVAS_PREFIX = '[Canvas]';
@@ -504,8 +504,6 @@ export const moveComponents = createAction(
props<{ request: MoveComponentsRequest }>()
);
-export const copy = createAction(`${CANVAS_PREFIX} Copy`, props<{ request:
CopyRequest }>());
-
export const copySuccess = createAction(`${CANVAS_PREFIX} Copy Success`,
props<{ response: CopyResponseContext }>());
export const paste = createAction(`${CANVAS_PREFIX} Paste`, props<{ request:
CopyResponseEntity }>());
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index fbced100d6..e42189da2f 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -20,6 +20,14 @@ import { FlowService } from '../../service/flow.service';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { concatLatestFrom } from '@ngrx/operators';
import * as FlowActions from './flow.actions';
+import {
+ disableComponent,
+ enableComponent,
+ setRegistryClients,
+ startComponent,
+ startPollingProcessorUntilStopped,
+ stopComponent
+} from './flow.actions';
import * as StatusHistoryActions from
'../../../../state/status-history/status-history.actions';
import * as ErrorActions from '../../../../state/error/error.actions';
import * as CopyActions from '../../../../state/copy/copy.actions';
@@ -82,9 +90,9 @@ import {
selectMaxZIndex,
selectOutputPort,
selectParentProcessGroupId,
+ selectPollingProcessor,
selectProcessGroup,
selectProcessor,
- selectPollingProcessor,
selectRefreshRpgDetails,
selectRemoteProcessGroup,
selectSaving,
@@ -119,7 +127,17 @@ import { OkDialog } from
'../../../../ui/common/ok-dialog/ok-dialog.component';
import { GroupComponents } from
'../../ui/canvas/items/process-group/group-components/group-components.component';
import { EditProcessGroup } from
'../../ui/canvas/items/process-group/edit-process-group/edit-process-group.component';
import { ControllerServiceService } from
'../../service/controller-service.service';
-import { YesNoDialog } from '@nifi/shared';
+import {
+ ComponentType,
+ isDefinedAndNotNull,
+ LARGE_DIALOG,
+ MEDIUM_DIALOG,
+ NiFiCommon,
+ SMALL_DIALOG,
+ Storage,
+ XL_DIALOG,
+ YesNoDialog
+} from '@nifi/shared';
import { PropertyTableHelperService } from
'../../../../service/property-table-helper.service';
import { ParameterHelperService } from
'../../service/parameter-helper.service';
import { RegistryService } from '../../service/registry.service';
@@ -151,32 +169,13 @@ import {
} from
'../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from
'../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
-import {
- ComponentType,
- isDefinedAndNotNull,
- LARGE_DIALOG,
- MEDIUM_DIALOG,
- SMALL_DIALOG,
- XL_DIALOG,
- NiFiCommon,
- Storage
-} from '@nifi/shared';
import { resetPollingFlowAnalysis } from
'../flow-analysis/flow-analysis.actions';
import { selectDocumentVisibilityState } from
'../../../../state/document-visibility/document-visibility.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DocumentVisibility } from '../../../../state/document-visibility';
import { ErrorContextKey } from '../../../../state/error';
-import {
- disableComponent,
- enableComponent,
- setRegistryClients,
- startComponent,
- startPollingProcessorUntilStopped,
- stopComponent
-} from './flow.actions';
import { CopyPasteService } from '../../service/copy-paste.service';
import { selectCopiedContent } from '../../../../state/copy/copy.selectors';
-import { CopyRequestContext, CopyResponseContext } from
'../../../../state/copy';
import * as ParameterActions from '../parameter/parameter.actions';
import { ParameterContextService } from
'../../../parameter-contexts/service/parameter-contexts.service';
@@ -2495,45 +2494,6 @@ export class FlowEffects {
)
);
- copy$ = createEffect(() =>
- this.actions$.pipe(
- ofType(FlowActions.copy),
- map((action) => action.request),
- concatLatestFrom(() =>
this.store.select(selectCurrentProcessGroupId)),
- switchMap(([request, processGroupId]) => {
- const copyRequest: CopyRequestContext = {
- ...request,
- processGroupId
- };
- return from(this.copyPasteService.copy(copyRequest)).pipe(
- switchMap((response) => {
- const copyBlob = new Blob([JSON.stringify(response,
null, 2)], { type: 'text/plain' });
- const clipboardItem: ClipboardItem = new
ClipboardItem({
- 'text/plain': copyBlob
- });
- return
from(navigator.clipboard.write([clipboardItem])).pipe(
- switchMap(() => {
- return of(
- FlowActions.copySuccess({
- response: {
- copyResponse: response,
- processGroupId,
- pasteCount: 0
- } as CopyResponseContext
- })
- );
- }),
- catchError(() => {
- return of(FlowActions.flowSnackbarError({
error: 'Copy failed' }));
- })
- );
- }),
- catchError((errorResponse: HttpErrorResponse) =>
of(this.snackBarOrFullScreenError(errorResponse)))
- );
- })
- )
- );
-
copySuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.copySuccess),
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
index 8107092714..0272a925e9 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
@@ -689,6 +689,10 @@ export class Canvas implements OnInit, OnDestroy {
@HostListener('window:keydown.control.c', ['$event'])
handleKeyDownCtrlC(event: KeyboardEvent) {
+ if (!this.canvasUtils.isClipboardAvailable()) {
+ return;
+ }
+
if (this.executeAction('copy', event)) {
event.preventDefault();
}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
index 3aa493f75f..bee1f504df 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
@@ -99,14 +99,6 @@
(click)="copy(selection)">
<i class="fa fa-copy"></i>
</button>
- <button
- mat-icon-button
- class="primary-icon-button mr-2"
- type="button"
- [disabled]="!canPaste()"
- (click)="paste()">
- <i class="fa fa-paste"></i>
- </button>
<button
mat-icon-button
class="primary-icon-button"
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
index 950002ebc8..d4a77c8d28 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
@@ -233,14 +233,6 @@ export class OperationControl {
this.canvasActionsService.getActionFunction('copy')(selection);
}
- canPaste(): boolean {
- return
this.canvasActionsService.getConditionFunction('paste')(d3.select(null));
- }
-
- paste(): void {
- return
this.canvasActionsService.getActionFunction('paste')(d3.select(null));
- }
-
canGroup(selection: d3.Selection<any, any, any, any>): boolean {
return
this.canvasActionsService.getConditionFunction('group')(selection);
}
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.spec.ts
index e299d85ef8..78311353a7 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.spec.ts
@@ -24,7 +24,7 @@ import { ComponentFixture, TestBed } from
'@angular/core/testing';
@Component({
standalone: true,
- template: `<div [copy]="copyText">test</div>`,
+ template: ` <div [copy]="copyText">test</div>`,
imports: [CopyDirective]
})
class TestComponent {
@@ -35,6 +35,7 @@ describe('CopyDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let directiveDebugEl: any;
+ let windowSpy;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -44,12 +45,22 @@ describe('CopyDirective', () => {
component = fixture.componentInstance;
fixture.detectChanges();
directiveDebugEl =
fixture.debugElement.query(By.directive(CopyDirective));
+ window.isSecureContext = true;
});
afterEach(() => {
jest.restoreAllMocks();
});
+ it('should not create a copy button on mouse enter if the clipboard is not
available', () => {
+ window.isSecureContext = false;
+
+ directiveDebugEl.triggerEventHandler('mouseenter', null);
+ fixture.detectChanges();
+ const copyButton =
directiveDebugEl.nativeElement.querySelector('.copy-button');
+ expect(copyButton).toBeNull();
+ });
+
it('should create a copy button on mouse enter', () => {
directiveDebugEl.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.ts
index 9f0d38cd99..b57b936646 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/directives/copy/copy.directive.ts
@@ -36,26 +36,33 @@ export class CopyDirective {
private zone: NgZone
) {}
+ isClipboardAvailable(): boolean {
+ // system clipboard interaction requires the browser to be in a
secured context.
+ return window.isSecureContext && Object.hasOwn(window,
'ClipboardItem');
+ }
+
@HostListener('mouseenter')
onMouseEnter() {
- this.copyButton = this.renderer.createElement('i');
- if (this.copyButton) {
- const cb: HTMLElement = this.copyButton;
- cb.classList.add('copy-button', 'fa', 'fa-copy', 'ml-2',
'primary-color');
+ if (this.isClipboardAvailable()) {
+ this.copyButton = this.renderer.createElement('i');
+ if (this.copyButton) {
+ const cb: HTMLElement = this.copyButton;
+ cb.classList.add('copy-button', 'fa', 'fa-copy', 'ml-2',
'primary-color');
- // run outside the angular zone to prevent unnecessary change
detection cycles
- this.subscription = this.zone.runOutsideAngular(() => {
- return fromEvent(cb, 'click')
- .pipe(
- switchMap(() =>
navigator.clipboard.writeText(this.copy)),
- take(1)
- )
- .subscribe(() => {
- cb.classList.remove('copy-button', 'fa-copy');
- cb.classList.add('copied', 'fa-check',
'success-color-default');
- });
- });
- this.renderer.appendChild(this.elementRef.nativeElement,
this.copyButton);
+ // run outside the angular zone to prevent unnecessary change
detection cycles
+ this.subscription = this.zone.runOutsideAngular(() => {
+ return fromEvent(cb, 'click')
+ .pipe(
+ switchMap(() =>
navigator.clipboard.writeText(this.copy)),
+ take(1)
+ )
+ .subscribe(() => {
+ cb.classList.remove('copy-button', 'fa-copy');
+ cb.classList.add('copied', 'fa-check',
'success-color-default');
+ });
+ });
+ this.renderer.appendChild(this.elementRef.nativeElement,
this.copyButton);
+ }
}
}