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 ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const clipboard = useClipboard();
const i18n = useI18n(); const i18n = useI18n();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const clipboard = useClipboard();
const { activeNode } = ndvStore; const { activeNode } = ndvStore;
const pinnedData = usePinnedData(activeNode); const pinnedData = usePinnedData(activeNode);
const { showToast } = useToast(); 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 { useClipboard as useClipboardCore, useThrottleFn } from '@vueuse/core';
import { IsInPiPWindowSymbol } from '@/constants'; import { PiPWindowSymbol } from '@/constants';
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void; type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
export function useClipboard( export function useClipboard({
options: { onPaste: onPasteFn = () => {},
onPaste: ClipboardEventFn; }: {
} = { onPaste?: ClipboardEventFn;
onPaste() {}, } = {}) {
}, const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
) { const { copy, copied, isSupported, text } = useClipboardCore({
const isInPiPWindow = inject(IsInPiPWindowSymbol, false); navigator: pipWindow?.value?.navigator ?? window.navigator,
const { copy, copied, isSupported, text } = useClipboardCore({ legacy: true }); legacy: true,
});
const ignoreClasses = ['el-messsage-box', 'ignore-key-press-canvas']; const ignoreClasses = ['el-messsage-box', 'ignore-key-press-canvas'];
const initialized = ref(false); const initialized = ref(false);
const onPasteCallback = ref<ClipboardEventFn | null>(options.onPaste || null); const onPasteCallback = ref<ClipboardEventFn | null>(onPasteFn || null);
/** /**
* Handles copy/paste events * Handles copy/paste events
@@ -74,9 +75,7 @@ export function useClipboard(
return { return {
copy, copy,
copied, 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". isSupported,
// Therefore, we disable copying features in the PiP window for now.
isSupported: computed(() => isSupported && !unref(isInPiPWindow)),
text, text,
onPaste: onPasteCallback, onPaste: onPasteCallback,
}; };

View File

@@ -10,7 +10,7 @@ import type {
CanvasNodeHandleInjectionData, CanvasNodeHandleInjectionData,
CanvasNodeInjectionData, CanvasNodeInjectionData,
} from '@/types'; } 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_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 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 CanvasNodeKey = 'canvasNode' as unknown as InjectionKey<CanvasNodeInjectionData>;
export const CanvasNodeHandleKey = export const CanvasNodeHandleKey =
'canvasNodeHandle' as unknown as InjectionKey<CanvasNodeHandleInjectionData>; 'canvasNodeHandle' as unknown as InjectionKey<CanvasNodeHandleInjectionData>;
export const IsInPiPWindowSymbol = 'IsInPipWindow' as unknown as InjectionKey< export const PiPWindowSymbol = 'PiPWindow' as unknown as InjectionKey<Ref<Window | undefined>>;
MaybeRefOrGetter<boolean>
>;
/** Auth */ /** Auth */
export const APP_MODALS_ELEMENT_ID = 'app-modals'; export const APP_MODALS_ELEMENT_ID = 'app-modals';

View File

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

View File

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

View File

@@ -6,8 +6,9 @@ import { type IRunDataDisplayMode, type NodePanelType } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { waitingNodeTooltip } from '@/utils/executionUtils'; import { waitingNodeTooltip } from '@/utils/executionUtils';
import { N8nLink, N8nText } from '@n8n/design-system'; import { N8nLink, N8nText } from '@n8n/design-system';
import { computed, ref } from 'vue'; import { computed, inject, ref } from 'vue';
import { I18nT } from 'vue-i18n'; import { I18nT } from 'vue-i18n';
import { PiPWindowSymbol } from '@/constants';
const { title, logEntry, paneType } = defineProps<{ const { title, logEntry, paneType } = defineProps<{
title: string; title: string;
@@ -18,6 +19,8 @@ const { title, logEntry, paneType } = defineProps<{
const locale = useI18n(); const locale = useI18n();
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const displayMode = ref<IRunDataDisplayMode>(paneType === 'input' ? 'schema' : 'table'); const displayMode = ref<IRunDataDisplayMode>(paneType === 'input' ? 'schema' : 'table');
const isMultipleInput = computed( const isMultipleInput = computed(
() => paneType === 'input' && (logEntry.runData?.source.length ?? 0) > 1, () => paneType === 'input' && (logEntry.runData?.source.length ?? 0) > 1,
@@ -65,6 +68,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
<RunData <RunData
v-if="runDataProps" v-if="runDataProps"
v-bind="runDataProps" v-bind="runDataProps"
:key="`run-data${pipWindow ? '-pip' : ''}`"
:workflow="logEntry.workflow" :workflow="logEntry.workflow"
:workflow-execution="logEntry.execution" :workflow-execution="logEntry.execution"
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')" :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 { useUIStore } from '@/stores/ui.store';
import { applyThemeToBody } from '@/stores/ui.utils'; import { applyThemeToBody } from '@/stores/ui.utils';
import { useProvideTooltipAppendTo } from '@n8n/design-system/composables/useTooltipAppendTo'; 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 // Copy over dynamic styles to PiP window to support lazily imported modules
observer.observe(document.head, { childList: true, subtree: true }); observer.observe(document.head, { childList: true, subtree: true });
provide(IsInPiPWindowSymbol, isPoppedOut); provide(PiPWindowSymbol, pipWindow);
useProvideTooltipAppendTo(tooltipContainer); useProvideTooltipAppendTo(tooltipContainer);
async function showPip() { async function showPip() {