feat(editor): Migrate pinData mixin to usePinnedData composable (no-changelog) (#8207)
## Summary Required as part of NodeView refactoring: - Migrates `pinData` mixin to `usePinnedData` composable. - Adds `useActiveNode` and `useNodeType` composables ## Related tickets and issues https://linear.app/n8n/issue/N8N-6355/pindata ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useActiveNode } from '@/composables/useActiveNode';
|
||||
import { useNodeType } from '@/composables/useNodeType';
|
||||
import { createTestNode } from '@/__tests__/mocks';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import { computed } from 'vue';
|
||||
import { defaultMockNodeTypes } from '@/__tests__/defaults';
|
||||
|
||||
const node = computed(() => createTestNode({ name: 'Node', type: MANUAL_TRIGGER_NODE_TYPE }));
|
||||
const nodeType = computed(() => defaultMockNodeTypes[MANUAL_TRIGGER_NODE_TYPE]);
|
||||
|
||||
vi.mock('@/stores/ndv.store', () => ({
|
||||
useNDVStore: vi.fn(() => ({
|
||||
activeNode: node,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/composables/useNodeType', () => ({
|
||||
useNodeType: vi.fn(() => ({
|
||||
nodeType,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: vi.fn((store) => store),
|
||||
}));
|
||||
|
||||
describe('useActiveNode()', () => {
|
||||
it('should call useNodeType()', () => {
|
||||
useActiveNode();
|
||||
|
||||
expect(useNodeType).toHaveBeenCalledWith({
|
||||
node,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return activeNode and activeNodeType', () => {
|
||||
const { activeNode, activeNodeType } = useActiveNode();
|
||||
|
||||
expect(activeNode).toBe(node);
|
||||
expect(activeNodeType).toBe(nodeType);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useNodeType } from '@/composables/useNodeType';
|
||||
import type { INodeUi, SimplifiedNodeType } from '@/Interface'; // Adjust the path accordingly
|
||||
|
||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: vi.fn(() => ({
|
||||
getNodeType: vi.fn().mockImplementation((type, version) => ({ type, version })),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useNodeType()', () => {
|
||||
describe('nodeType', () => {
|
||||
it('returns correct nodeType from nodeType option', () => {
|
||||
const nodeTypeOption = { name: 'testNodeType' } as SimplifiedNodeType;
|
||||
const { nodeType } = useNodeType({ nodeType: nodeTypeOption });
|
||||
|
||||
expect(nodeType.value).toEqual(nodeTypeOption);
|
||||
});
|
||||
|
||||
it('returns correct nodeType from node option', () => {
|
||||
const nodeOption = { type: 'testType', typeVersion: 1 } as INodeUi;
|
||||
const { nodeType } = useNodeType({ node: nodeOption });
|
||||
|
||||
expect(nodeType.value).toEqual({ type: 'testType', version: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSubNodeType', () => {
|
||||
it('identifies sub node type correctly', () => {
|
||||
const nodeTypeOption = {
|
||||
name: 'testNodeType',
|
||||
outputs: ['Main', 'Other'],
|
||||
} as unknown as SimplifiedNodeType;
|
||||
const { isSubNodeType } = useNodeType({ nodeType: nodeTypeOption });
|
||||
|
||||
expect(isSubNodeType.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMultipleOutputsNodeType', () => {
|
||||
it('identifies multiple outputs node type correctly', () => {
|
||||
const nodeTypeOption = {
|
||||
name: 'testNodeType',
|
||||
outputs: ['Main', 'Other'],
|
||||
} as unknown as SimplifiedNodeType;
|
||||
const { isMultipleOutputsNodeType } = useNodeType({ nodeType: nodeTypeOption });
|
||||
|
||||
expect(isMultipleOutputsNodeType.value).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { usePinnedData } from '@/composables/usePinnedData';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { MAX_PINNED_DATA_SIZE } from '@/constants';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({ useToast: vi.fn(() => ({ showError: vi.fn() })) }));
|
||||
vi.mock('@/composables/useI18n', () => ({
|
||||
useI18n: vi.fn(() => ({ baseText: vi.fn((key) => key) })),
|
||||
}));
|
||||
vi.mock('@/composables/useExternalHooks', () => ({
|
||||
useExternalHooks: vi.fn(() => ({
|
||||
run: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('usePinnedData', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
describe('isValidJSON()', () => {
|
||||
it('should return true for valid JSON', () => {
|
||||
const { isValidJSON } = usePinnedData(ref(null));
|
||||
|
||||
expect(isValidJSON('{"key":"value"}')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid JSON', () => {
|
||||
const { isValidJSON } = usePinnedData(ref(null));
|
||||
const result = isValidJSON('invalid json');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidSize()', () => {
|
||||
it('should return true when data size is at upper limit', () => {
|
||||
const { isValidSize } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
|
||||
const largeData = new Array(MAX_PINNED_DATA_SIZE + 1).join('a');
|
||||
|
||||
expect(isValidSize(largeData)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when data size is too large', () => {
|
||||
const { isValidSize } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
|
||||
const largeData = new Array(MAX_PINNED_DATA_SIZE + 2).join('a');
|
||||
|
||||
expect(isValidSize(largeData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setData()', () => {
|
||||
it('should throw if data is not valid JSON', () => {
|
||||
const { setData } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
|
||||
|
||||
expect(() => setData('invalid json', 'pin-icon-click')).toThrow();
|
||||
});
|
||||
|
||||
it('should throw if data size is too large', () => {
|
||||
const { setData } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
|
||||
const largeData = new Array(MAX_PINNED_DATA_SIZE + 2).join('a');
|
||||
|
||||
expect(() => setData(largeData, 'pin-icon-click')).toThrow();
|
||||
});
|
||||
|
||||
it('should set data correctly for valid inputs', () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const node = ref({ name: 'testNode' } as INodeUi);
|
||||
const { setData } = usePinnedData(node);
|
||||
const testData = [{ json: { key: 'value' } }];
|
||||
|
||||
expect(() => setData(testData, 'pin-icon-click')).not.toThrow();
|
||||
expect(workflowsStore.workflow.pinData?.[node.value.name]).toEqual(testData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsetData()', () => {
|
||||
it('should unset data correctly', () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const node = ref({ name: 'testNode' } as INodeUi);
|
||||
const { setData, unsetData } = usePinnedData(node);
|
||||
const testData = [{ json: { key: 'value' } }];
|
||||
|
||||
setData(testData, 'pin-icon-click');
|
||||
unsetData('context-menu');
|
||||
|
||||
expect(workflowsStore.workflow.pinData?.[node.value.name]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSetDataSuccess()', () => {
|
||||
it('should trigger telemetry on successful data setting', async () => {
|
||||
const telemetry = useTelemetry();
|
||||
const spy = vi.spyOn(telemetry, 'track');
|
||||
const pinnedData = usePinnedData(ref({ name: 'testNode', type: 'someType' } as INodeUi), {
|
||||
displayMode: ref('json'),
|
||||
runIndex: ref(0),
|
||||
});
|
||||
|
||||
pinnedData.onSetDataSuccess({ source: 'pin-icon-click' });
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSetDataError()', () => {
|
||||
it('should trigger telemetry tracking on error in data setting', () => {
|
||||
const telemetry = useTelemetry();
|
||||
const spy = vi.spyOn(telemetry, 'track');
|
||||
const pinnedData = usePinnedData(ref({ name: 'testNode', type: 'someType' } as INodeUi), {
|
||||
displayMode: ref('json'),
|
||||
runIndex: ref(0),
|
||||
});
|
||||
|
||||
pinnedData.onSetDataError({ errorType: 'data-too-large', source: 'pin-icon-click' });
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onUnsetData()', () => {
|
||||
it('should trigger telemetry on successful data unsetting', async () => {
|
||||
const telemetry = useTelemetry();
|
||||
const spy = vi.spyOn(telemetry, 'track');
|
||||
const pinnedData = usePinnedData(ref({ name: 'testNode', type: 'someType' } as INodeUi), {
|
||||
displayMode: ref('json'),
|
||||
runIndex: ref(0),
|
||||
});
|
||||
|
||||
pinnedData.onUnsetData({ source: 'context-menu' });
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
17
packages/editor-ui/src/composables/useActiveNode.ts
Normal file
17
packages/editor-ui/src/composables/useActiveNode.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeType } from '@/composables/useNodeType';
|
||||
|
||||
export function useActiveNode() {
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const { activeNode } = storeToRefs(ndvStore);
|
||||
const { nodeType: activeNodeType } = useNodeType({
|
||||
node: activeNode,
|
||||
});
|
||||
|
||||
return {
|
||||
activeNode,
|
||||
activeNodeType,
|
||||
};
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export function useNodeHelpers() {
|
||||
workflow: Workflow,
|
||||
ignoreIssues?: string[],
|
||||
): INodeIssues | null {
|
||||
const pinDataNodeNames = Object.keys(workflowsStore.getPinData ?? {});
|
||||
const pinDataNodeNames = Object.keys(workflowsStore.pinnedWorkflowData ?? {});
|
||||
|
||||
let nodeIssues: INodeIssues | null = null;
|
||||
ignoreIssues = ignoreIssues ?? [];
|
||||
|
||||
46
packages/editor-ui/src/composables/useNodeType.ts
Normal file
46
packages/editor-ui/src/composables/useNodeType.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { MaybeRef } from 'vue';
|
||||
import { computed, unref } from 'vue';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import type { INodeUi, SimplifiedNodeType } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||
|
||||
export function useNodeType(
|
||||
options: {
|
||||
node?: MaybeRef<INodeUi | null>;
|
||||
nodeType?: MaybeRef<INodeTypeDescription | SimplifiedNodeType | null>;
|
||||
} = {},
|
||||
) {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const nodeType = computed(() => {
|
||||
if (options.nodeType) {
|
||||
return unref(options.nodeType);
|
||||
}
|
||||
|
||||
const activeNode = unref(options.node);
|
||||
if (activeNode) {
|
||||
return nodeTypesStore.getNodeType(activeNode.type, activeNode.typeVersion);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const isSubNodeType = computed(() => {
|
||||
if (!nodeType.value?.outputs || typeof nodeType.value?.outputs === 'string') {
|
||||
return false;
|
||||
}
|
||||
const outputTypes = NodeHelpers.getConnectionTypes(nodeType.value?.outputs);
|
||||
return outputTypes
|
||||
? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0
|
||||
: false;
|
||||
});
|
||||
|
||||
const isMultipleOutputsNodeType = computed(() => (nodeType.value?.outputs ?? []).length > 1);
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
isSubNodeType,
|
||||
isMultipleOutputsNodeType,
|
||||
};
|
||||
}
|
||||
253
packages/editor-ui/src/composables/usePinnedData.ts
Normal file
253
packages/editor-ui/src/composables/usePinnedData.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { INodeExecutionData, IPinData } from 'n8n-workflow';
|
||||
import { jsonParse, jsonStringify } from 'n8n-workflow';
|
||||
import {
|
||||
MAX_EXPECTED_REQUEST_SIZE,
|
||||
MAX_PINNED_DATA_SIZE,
|
||||
MAX_WORKFLOW_SIZE,
|
||||
PIN_DATA_NODE_TYPES_DENYLIST,
|
||||
} from '@/constants';
|
||||
import { stringSizeInBytes } from '@/utils/typesUtils';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { INodeUi, IRunDataDisplayMode } from '@/Interface';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import type { MaybeRef } from 'vue';
|
||||
import { computed, unref } from 'vue';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useNodeType } from '@/composables/useNodeType';
|
||||
|
||||
export type PinDataSource =
|
||||
| 'pin-icon-click'
|
||||
| 'save-edit'
|
||||
| 'on-ndv-close-modal'
|
||||
| 'duplicate-node'
|
||||
| 'add-nodes'
|
||||
| 'context-menu'
|
||||
| 'keyboard-shortcut';
|
||||
|
||||
export type UnpinDataSource = 'unpin-and-execute-modal' | 'context-menu' | 'keyboard-shortcut';
|
||||
|
||||
export function usePinnedData(
|
||||
node: MaybeRef<INodeUi | null>,
|
||||
options: {
|
||||
displayMode?: MaybeRef<IRunDataDisplayMode>;
|
||||
runIndex?: MaybeRef<number>;
|
||||
} = {},
|
||||
) {
|
||||
const rootStore = useRootStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const externalHooks = useExternalHooks();
|
||||
|
||||
const { sessionId } = storeToRefs(rootStore);
|
||||
const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({
|
||||
node,
|
||||
});
|
||||
|
||||
const data = computed<IPinData[string] | undefined>(() => {
|
||||
const targetNode = unref(node);
|
||||
return targetNode ? workflowsStore.pinDataByNodeName(targetNode.name) : undefined;
|
||||
});
|
||||
|
||||
const hasData = computed<boolean>(() => {
|
||||
const targetNode = unref(node);
|
||||
return !!targetNode && typeof data.value !== 'undefined';
|
||||
});
|
||||
|
||||
const isValidNodeType = computed(() => {
|
||||
const targetNode = unref(node);
|
||||
return (
|
||||
!!targetNode &&
|
||||
!isSubNodeType.value &&
|
||||
!isMultipleOutputsNodeType.value &&
|
||||
!PIN_DATA_NODE_TYPES_DENYLIST.includes(targetNode.type)
|
||||
);
|
||||
});
|
||||
|
||||
function isValidJSON(data: string): boolean {
|
||||
try {
|
||||
JSON.parse(data);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
const title = i18n.baseText('runData.editOutputInvalid');
|
||||
|
||||
const toRemove = new RegExp(/JSON\.parse:|of the JSON data/, 'g');
|
||||
const message = error.message.replace(toRemove, '').trim();
|
||||
const positionMatchRegEx = /at position (\d+)/;
|
||||
const positionMatch = error.message.match(positionMatchRegEx);
|
||||
|
||||
error.message = message.charAt(0).toUpperCase() + message.slice(1);
|
||||
error.message = error.message.replace(
|
||||
"Unexpected token ' in JSON",
|
||||
i18n.baseText('runData.editOutputInvalid.singleQuote'),
|
||||
);
|
||||
|
||||
if (positionMatch) {
|
||||
const position = parseInt(positionMatch[1], 10);
|
||||
const lineBreaksUpToPosition = (data.slice(0, position).match(/\n/g) || []).length;
|
||||
|
||||
error.message = error.message.replace(
|
||||
positionMatchRegEx,
|
||||
i18n.baseText('runData.editOutputInvalid.atPosition', {
|
||||
interpolate: {
|
||||
position: `${position}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
error.message = `${i18n.baseText('runData.editOutputInvalid.onLine', {
|
||||
interpolate: {
|
||||
line: `${lineBreaksUpToPosition + 1}`,
|
||||
},
|
||||
})} ${error.message}`;
|
||||
}
|
||||
|
||||
toast.showError(error, title);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidSize(data: string | object): boolean {
|
||||
const targetNode = unref(node);
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data === 'object') data = JSON.stringify(data);
|
||||
|
||||
const { pinData: currentPinData, ...workflow } = workflowsStore.getCurrentWorkflow();
|
||||
const workflowJson = jsonStringify(workflow, { replaceCircularRefs: true });
|
||||
|
||||
const newPinData = { ...currentPinData, [targetNode.name]: data };
|
||||
const newPinDataSize = workflowsStore.getPinDataSize(newPinData);
|
||||
|
||||
if (newPinDataSize > MAX_PINNED_DATA_SIZE) {
|
||||
toast.showError(
|
||||
new Error(i18n.baseText('ndv.pinData.error.tooLarge.description')),
|
||||
i18n.baseText('ndv.pinData.error.tooLarge.title'),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
stringSizeInBytes(workflowJson) + newPinDataSize >
|
||||
MAX_WORKFLOW_SIZE - MAX_EXPECTED_REQUEST_SIZE
|
||||
) {
|
||||
toast.showError(
|
||||
new Error(i18n.baseText('ndv.pinData.error.tooLargeWorkflow.description')),
|
||||
i18n.baseText('ndv.pinData.error.tooLargeWorkflow.title'),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function onSetDataSuccess({ source }: { source: PinDataSource }) {
|
||||
const targetNode = unref(node);
|
||||
const displayMode = unref(options.displayMode);
|
||||
const runIndex = unref(options.runIndex);
|
||||
const telemetryPayload = {
|
||||
pinning_source: source,
|
||||
node_type: targetNode?.type,
|
||||
session_id: sessionId.value,
|
||||
data_size: stringSizeInBytes(data.value),
|
||||
view: displayMode,
|
||||
run_index: runIndex,
|
||||
};
|
||||
|
||||
void externalHooks.run('runData.onDataPinningSuccess', telemetryPayload);
|
||||
telemetry.track('Ndv data pinning success', telemetryPayload);
|
||||
}
|
||||
|
||||
function onSetDataError({
|
||||
errorType,
|
||||
source,
|
||||
}: {
|
||||
errorType: 'data-too-large' | 'invalid-json';
|
||||
source: PinDataSource;
|
||||
}) {
|
||||
const targetNode = unref(node);
|
||||
const displayMode = unref(options.displayMode);
|
||||
const runIndex = unref(options.runIndex);
|
||||
|
||||
telemetry.track('Ndv data pinning failure', {
|
||||
pinning_source: source,
|
||||
node_type: targetNode?.type,
|
||||
session_id: sessionId.value,
|
||||
data_size: stringSizeInBytes(data.value),
|
||||
view: displayMode,
|
||||
run_index: runIndex,
|
||||
error_type: errorType,
|
||||
});
|
||||
}
|
||||
|
||||
function setData(data: string | INodeExecutionData[], source: PinDataSource) {
|
||||
const targetNode = unref(node);
|
||||
if (!targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
if (!isValidJSON(data)) {
|
||||
onSetDataError({ errorType: 'invalid-json', source });
|
||||
throw new Error('Invalid JSON');
|
||||
}
|
||||
|
||||
data = jsonParse(data);
|
||||
}
|
||||
|
||||
if (!isValidSize(data)) {
|
||||
onSetDataError({ errorType: 'data-too-large', source });
|
||||
throw new Error('Data too large');
|
||||
}
|
||||
|
||||
workflowsStore.pinData({ node: targetNode, data: data as INodeExecutionData[] });
|
||||
onSetDataSuccess({ source });
|
||||
}
|
||||
|
||||
function onUnsetData({ source }: { source: UnpinDataSource }) {
|
||||
const targetNode = unref(node);
|
||||
const runIndex = unref(options.runIndex);
|
||||
|
||||
telemetry.track('User unpinned ndv data', {
|
||||
node_type: targetNode?.type,
|
||||
session_id: sessionId.value,
|
||||
run_index: runIndex,
|
||||
source,
|
||||
data_size: stringSizeInBytes(data.value),
|
||||
});
|
||||
}
|
||||
|
||||
function unsetData(source: UnpinDataSource): void {
|
||||
const targetNode = unref(node);
|
||||
if (!targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUnsetData({ source });
|
||||
workflowsStore.unpinData({ node: targetNode });
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
hasData,
|
||||
isValidNodeType,
|
||||
setData,
|
||||
onSetDataSuccess,
|
||||
onSetDataError,
|
||||
unsetData,
|
||||
onUnsetData,
|
||||
isValidJSON,
|
||||
isValidSize,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user