fix(editor): Fix and enable copying to clipboard in PiP (#15632)

Co-authored-by: autologie <suguru@n8n.io>
This commit is contained in:
Alex Grozav
2025-06-17 10:24:48 +02:00
committed by GitHub
parent a953218b9d
commit f9f0fdf40d
7 changed files with 28 additions and 24 deletions

View File

@@ -39,9 +39,10 @@ const props = withDefaults(
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const clipboard = useClipboard();
const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
const clipboard = useClipboard();
const { activeNode } = ndvStore;
const pinnedData = usePinnedData(activeNode);
const { showToast } = useToast();

View File

@@ -1,23 +1,24 @@
import { computed, inject, onBeforeUnmount, onMounted, ref, unref } from 'vue';
import { inject, onBeforeUnmount, onMounted, ref } from 'vue';
import { useClipboard as useClipboardCore, useThrottleFn } from '@vueuse/core';
import { IsInPiPWindowSymbol } from '@/constants';
import { PiPWindowSymbol } from '@/constants';
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
export function useClipboard(
options: {
onPaste: ClipboardEventFn;
} = {
onPaste() {},
},
) {
const isInPiPWindow = inject(IsInPiPWindowSymbol, false);
const { copy, copied, isSupported, text } = useClipboardCore({ legacy: true });
export function useClipboard({
onPaste: onPasteFn = () => {},
}: {
onPaste?: ClipboardEventFn;
} = {}) {
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const { copy, copied, isSupported, text } = useClipboardCore({
navigator: pipWindow?.value?.navigator ?? window.navigator,
legacy: true,
});
const ignoreClasses = ['el-messsage-box', 'ignore-key-press-canvas'];
const initialized = ref(false);
const onPasteCallback = ref<ClipboardEventFn | null>(options.onPaste || null);
const onPasteCallback = ref<ClipboardEventFn | null>(onPasteFn || null);
/**
* Handles copy/paste events
@@ -74,9 +75,7 @@ export function useClipboard(
return {
copy,
copied,
// When the `copy()` method is invoked from inside of the document picture-in-picture (PiP) window, it throws the error "Document is not focused".
// Therefore, we disable copying features in the PiP window for now.
isSupported: computed(() => isSupported && !unref(isInPiPWindow)),
isSupported,
text,
onPaste: onPasteCallback,
};

View File

@@ -10,7 +10,7 @@ import type {
CanvasNodeHandleInjectionData,
CanvasNodeInjectionData,
} from '@/types';
import type { InjectionKey, MaybeRefOrGetter } from 'vue';
import type { InjectionKey, Ref } from 'vue';
export const MAX_WORKFLOW_SIZE = 1024 * 1024 * 16; // Workflow size limit in bytes
export const MAX_EXPECTED_REQUEST_SIZE = 2048; // Expected maximum workflow request metadata (i.e. headers) size in bytes
@@ -914,9 +914,7 @@ export const CanvasKey = 'canvas' as unknown as InjectionKey<CanvasInjectionData
export const CanvasNodeKey = 'canvasNode' as unknown as InjectionKey<CanvasNodeInjectionData>;
export const CanvasNodeHandleKey =
'canvasNodeHandle' as unknown as InjectionKey<CanvasNodeHandleInjectionData>;
export const IsInPiPWindowSymbol = 'IsInPipWindow' as unknown as InjectionKey<
MaybeRefOrGetter<boolean>
>;
export const PiPWindowSymbol = 'PiPWindow' as unknown as InjectionKey<Ref<Window | undefined>>;
/** Auth */
export const APP_MODALS_ELEMENT_ID = 'app-modals';

View File

@@ -36,6 +36,7 @@ const emit = defineEmits<{
}>();
const clipboard = useClipboard();
const locale = useI18n();
const toast = useToast();
@@ -149,7 +150,7 @@ async function copySessionId() {
@click="emit('clickHeader')"
>
<template #actions>
<N8nTooltip v-if="clipboard.isSupported.value && !isReadOnly">
<N8nTooltip v-if="clipboard.isSupported && !isReadOnly">
<template #content>
{{ sessionId }}
<br />

View File

@@ -136,6 +136,7 @@ async function handleOpenNdv(treeNode: LogEntry) {
@resizeend="onChatPanelResizeEnd"
>
<ChatMessagesPanel
:key="`canvas-chat-${currentSessionId}${isPoppedOut ? '-pip' : ''}`"
data-test-id="canvas-chat"
:is-open="isOpen"
:is-read-only="isReadOnly"

View File

@@ -6,8 +6,9 @@ import { type IRunDataDisplayMode, type NodePanelType } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { waitingNodeTooltip } from '@/utils/executionUtils';
import { N8nLink, N8nText } from '@n8n/design-system';
import { computed, ref } from 'vue';
import { computed, inject, ref } from 'vue';
import { I18nT } from 'vue-i18n';
import { PiPWindowSymbol } from '@/constants';
const { title, logEntry, paneType } = defineProps<{
title: string;
@@ -18,6 +19,8 @@ const { title, logEntry, paneType } = defineProps<{
const locale = useI18n();
const ndvStore = useNDVStore();
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const displayMode = ref<IRunDataDisplayMode>(paneType === 'input' ? 'schema' : 'table');
const isMultipleInput = computed(
() => paneType === 'input' && (logEntry.runData?.source.length ?? 0) > 1,
@@ -65,6 +68,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
<RunData
v-if="runDataProps"
v-bind="runDataProps"
:key="`run-data${pipWindow ? '-pip' : ''}`"
:workflow="logEntry.workflow"
:workflow-execution="logEntry.execution"
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')"

View File

@@ -1,4 +1,4 @@
import { IsInPiPWindowSymbol } from '@/constants';
import { PiPWindowSymbol } from '@/constants';
import { useUIStore } from '@/stores/ui.store';
import { applyThemeToBody } from '@/stores/ui.utils';
import { useProvideTooltipAppendTo } from '@n8n/design-system/composables/useTooltipAppendTo';
@@ -90,7 +90,7 @@ export function usePiPWindow({
// Copy over dynamic styles to PiP window to support lazily imported modules
observer.observe(document.head, { childList: true, subtree: true });
provide(IsInPiPWindowSymbol, isPoppedOut);
provide(PiPWindowSymbol, pipWindow);
useProvideTooltipAppendTo(tooltipContainer);
async function showPip() {