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);
+            }
         }
     }
 

Reply via email to