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:
Alex Grozav
2024-01-04 11:22:56 +02:00
committed by GitHub
parent f4092a9e49
commit b50d8058cf
21 changed files with 678 additions and 92 deletions

View File

@@ -286,7 +286,7 @@ export const jsonFieldCompletions = defineComponent({
nodeName = quotedNodeName.replace(/^"/, '').replace(/"$/, '');
}
const pinData: IPinData | undefined = this.workflowsStore.getPinData;
const pinData: IPinData | undefined = this.workflowsStore.pinnedWorkflowData;
const nodePinData = pinData?.[nodeName];

View File

@@ -1,6 +1,6 @@
<template>
<RunData
:node-ui="currentNode"
:node="currentNode"
:run-index="runIndex"
:linked-runs="linkedRuns"
:can-link-runs="!mappedNode && canLinkRuns"

View File

@@ -159,7 +159,6 @@ import {
} from '@/constants';
import { nodeBase } from '@/mixins/nodeBase';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { pinData } from '@/mixins/pinData';
import type {
ConnectionTypes,
IExecutionsSummary,
@@ -187,6 +186,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { type ContextMenuTarget, useContextMenu } from '@/composables/useContextMenu';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({
name: 'Node',
@@ -195,7 +195,7 @@ export default defineComponent({
FontAwesomeIcon,
NodeIcon,
},
mixins: [nodeBase, workflowHelpers, pinData, debounceHelper],
mixins: [nodeBase, workflowHelpers, debounceHelper],
props: {
isProductionExecutionPreview: {
type: Boolean,
@@ -210,17 +210,20 @@ export default defineComponent({
default: false,
},
},
setup() {
setup(props) {
const workflowsStore = useWorkflowsStore();
const contextMenu = useContextMenu();
const externalHooks = useExternalHooks();
const nodeHelpers = useNodeHelpers();
const node = workflowsStore.getNodeByName(props.name);
const pinnedData = usePinnedData(node);
return { contextMenu, externalHooks, nodeHelpers };
return { contextMenu, externalHooks, nodeHelpers, pinnedData };
},
computed: {
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
showPinnedDataInfo(): boolean {
return this.hasPinData && !this.isProductionExecutionPreview;
return this.pinnedData.hasData.value && !this.isProductionExecutionPreview;
},
isDuplicatable(): boolean {
if (!this.nodeType) return true;
@@ -247,7 +250,7 @@ export default defineComponent({
['crashed', 'error', 'failed'].includes(this.nodeExecutionStatus)
)
return true;
if (this.hasPinData) return false;
if (this.pinnedData.hasData.value) return false;
if (this.data?.issues !== undefined && Object.keys(this.data.issues).length) {
return true;
}
@@ -531,7 +534,7 @@ export default defineComponent({
!!this.node &&
this.isTriggerNode &&
!this.isPollingTypeNode &&
!this.hasPinData &&
!this.pinnedData.hasData.value &&
!this.isNodeDisabled &&
this.workflowRunning &&
this.workflowDataItems === 0 &&

View File

@@ -12,7 +12,7 @@
@dragend="onDragEnd"
>
<template #icon>
<div v-if="isSubNode" :class="$style.subNodeBackground"></div>
<div v-if="isSubNodeType" :class="$style.subNodeBackground"></div>
<NodeIcon :class="$style.nodeIcon" :node-type="nodeType" />
</template>
@@ -57,7 +57,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import { useActions } from '../composables/useActions';
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import { useNodeType } from '@/composables/useNodeType';
export interface Props {
nodeType: SimplifiedNodeType;
@@ -74,6 +74,9 @@ const telemetry = useTelemetry();
const { actions } = useNodeCreatorStore();
const { getAddedNodesAndConnections } = useActions();
const { isSubNodeType } = useNodeType({
nodeType: props.nodeType,
});
const dragging = ref(false);
const draggablePosition = ref({ x: -100, y: -100 });
@@ -124,16 +127,6 @@ const displayName = computed<string>(() => {
});
});
const isSubNode = computed<boolean>(() => {
if (!props.nodeType.outputs || typeof props.nodeType.outputs === 'string') {
return false;
}
const outputTypes = NodeHelpers.getConnectionTypes(props.nodeType.outputs);
return outputTypes
? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0
: false;
});
const isTrigger = computed<boolean>(() => {
return props.nodeType.group.includes('trigger') && !hasActions.value;
});

View File

@@ -135,7 +135,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { mapStores, storeToRefs } from 'pinia';
import { createEventBus } from 'n8n-design-system/utils';
import type {
INodeConnections,
@@ -163,7 +163,6 @@ import {
STICKY_NODE_TYPE,
} from '@/constants';
import { workflowActivate } from '@/mixins/workflowActivate';
import { pinData } from '@/mixins/pinData';
import { dataPinningEventBus } from '@/event-bus';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
@@ -174,6 +173,7 @@ import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useMessage } from '@/composables/useMessage';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({
name: 'NodeDetailsView',
@@ -184,7 +184,7 @@ export default defineComponent({
NDVDraggablePanels,
TriggerPanel,
},
mixins: [workflowHelpers, workflowActivate, pinData],
mixins: [workflowHelpers, workflowActivate],
props: {
readOnly: {
type: Boolean,
@@ -198,12 +198,16 @@ export default defineComponent({
},
},
setup(props, ctx) {
const ndvStore = useNDVStore();
const externalHooks = useExternalHooks();
const nodeHelpers = useNodeHelpers();
const { activeNode } = storeToRefs(ndvStore);
const pinnedData = usePinnedData(activeNode);
return {
externalHooks,
nodeHelpers,
pinnedData,
...useDeviceSupport(),
...useMessage(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -301,12 +305,10 @@ export default defineComponent({
return [];
},
parentNode(): string | undefined {
const pinData = this.workflowsStore.getPinData;
// Return the first parent node that contains data
for (const parentNodeName of this.parentNodes) {
// Check first for pinned data
if (pinData[parentNodeName]) {
if (this.workflowsStore.pinnedWorkflowData[parentNodeName]) {
return parentNodeName;
}
@@ -689,7 +691,7 @@ export default defineComponent({
if (shouldPinDataBeforeClosing === MODAL_CONFIRM) {
const { value } = this.outputPanelEditMode;
try {
this.setPinData(this.activeNode, jsonParse(value), 'on-ndv-close-modal');
this.pinnedData.setData(jsonParse(value), 'on-ndv-close-modal');
} catch (error) {
console.error(error);
}

View File

@@ -33,20 +33,21 @@ import {
import type { INodeUi } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow';
import { workflowRun } from '@/mixins/workflowRun';
import { pinData } from '@/mixins/pinData';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({
mixins: [workflowRun, pinData],
mixins: [workflowRun],
inheritAttrs: false,
props: {
nodeName: {
type: String,
required: true,
},
disabled: {
type: Boolean,
@@ -70,10 +71,14 @@ export default defineComponent({
},
},
setup(props, ctx) {
const workflowsStore = useWorkflowsStore();
const node = workflowsStore.getNodeByName(props.nodeName);
const pinnedData = usePinnedData(node);
const externalHooks = useExternalHooks();
return {
externalHooks,
pinnedData,
...useToast(),
...useMessage(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -221,7 +226,7 @@ export default defineComponent({
this.$emit('stopExecution');
} else {
let shouldUnpinAndExecute = false;
if (this.hasPinData) {
if (this.pinnedData.hasData.value) {
const confirmResult = await this.confirm(
this.$locale.baseText('ndv.pinData.unpinAndExecute.description'),
this.$locale.baseText('ndv.pinData.unpinAndExecute.title'),
@@ -233,11 +238,11 @@ export default defineComponent({
shouldUnpinAndExecute = confirmResult === MODAL_CONFIRM;
if (shouldUnpinAndExecute && this.node) {
this.unsetPinData(this.node, 'unpin-and-execute-modal');
this.pinnedData.unsetData('unpin-and-execute-modal');
}
}
if (!this.hasPinData || shouldUnpinAndExecute) {
if (!this.pinnedData.hasData.value || shouldUnpinAndExecute) {
const telemetryPayload = {
node_type: this.nodeType ? this.nodeType.name : null,
workflow_id: this.workflowsStore.workflowId,

View File

@@ -1,6 +1,6 @@
<template>
<RunData
:node-ui="node"
:node="node"
:run-index="runIndex"
:linked-runs="linkedRuns"
:can-link-runs="canLinkRuns"
@@ -36,11 +36,11 @@
{{ $locale.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }}
</span>
<RunInfo
v-if="hasNodeRun && !hasPinData && runsCount === 1"
v-if="hasNodeRun && !pinnedData.hasData.value && runsCount === 1"
v-show="!outputPanelEditMode.enabled"
:task-data="runTaskData"
:has-stale-data="staleData"
:has-pin-data="hasPinData"
:has-pin-data="pinnedData.hasData.value"
/>
</div>
</template>
@@ -50,7 +50,7 @@
$locale.baseText('ndv.output.waitingToRun')
}}</n8n-text>
<n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
<template v-if="isSubNode">
<template v-if="isSubNodeType.value">
{{ $locale.baseText('ndv.output.runNodeHintSubNode') }}
</template>
<template v-else>
@@ -93,7 +93,7 @@
</div>
</template>
<template v-if="!hasPinData && runsCount > 1" #run-info>
<template v-if="!pinnedData.hasData.value && runsCount > 1" #run-info>
<RunInfo :task-data="runTaskData" />
</template>
</RunData>
@@ -105,14 +105,15 @@ import type { IExecutionResponse, INodeUi } from '@/Interface';
import type { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow';
import RunData from './RunData.vue';
import RunInfo from './RunInfo.vue';
import { pinData } from '@/mixins/pinData';
import { mapStores } from 'pinia';
import { mapStores, storeToRefs } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import RunDataAi from './RunDataAi/RunDataAi.vue';
import { ndvEventBus } from '@/event-bus';
import { useNodeType } from '@/composables/useNodeType';
import { usePinnedData } from '@/composables/usePinnedData';
type RunDataRef = InstanceType<typeof RunData>;
@@ -124,7 +125,6 @@ const OUTPUT_TYPE = {
export default defineComponent({
name: 'OutputPanel',
components: { RunData, RunInfo, RunDataAi },
mixins: [pinData],
props: {
runIndex: {
type: Number,
@@ -155,6 +155,22 @@ export default defineComponent({
default: false,
},
},
setup(props) {
const ndvStore = useNDVStore();
const { activeNode } = storeToRefs(ndvStore);
const { isSubNodeType } = useNodeType({
node: activeNode,
});
const pinnedData = usePinnedData(activeNode, {
runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode('output'),
});
return {
pinnedData,
isSubNodeType,
};
},
data() {
return {
outputMode: 'regular',
@@ -271,7 +287,7 @@ export default defineComponent({
return this.ndvStore.outputPanelEditMode;
},
canPinData(): boolean {
return this.isPinDataNodeType && !this.isReadOnly;
return this.pinnedData.isValidNodeType.value && !this.isReadOnly;
},
},
methods: {

View File

@@ -1,7 +1,9 @@
<template>
<div :class="['run-data', $style.container]" @mouseover="activatePane">
<n8n-callout
v-if="canPinData && hasPinData && !editMode.enabled && !isProductionExecutionPreview"
v-if="
canPinData && pinnedData.hasData.value && !editMode.enabled && !isProductionExecutionPreview
"
theme="secondary"
icon="thumbtack"
:class="$style.pinnedDataCallout"
@@ -98,11 +100,11 @@
<n8n-icon-button
:class="['ml-2xs', $style.pinDataButton]"
type="tertiary"
:active="hasPinData"
:active="pinnedData.hasData.value"
icon="thumbtack"
:disabled="
editMode.enabled ||
(rawInputData.length === 0 && !hasPinData) ||
(rawInputData.length === 0 && !pinnedData.hasData.value) ||
isReadOnlyRoute ||
readOnlyEnv
"
@@ -562,7 +564,7 @@
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { defineAsyncComponent, defineComponent, toRef } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import { useStorage } from '@/composables/useStorage';
@@ -606,8 +608,8 @@ import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import { pinData } from '@/mixins/pinData';
import type { PinDataSource } from '@/mixins/pinData';
import type { PinDataSource } from '@/composables/usePinnedData';
import { usePinnedData } from '@/composables/usePinnedData';
import { dataPinningEventBus } from '@/event-bus';
import { clearJsonKey, isEmpty } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
@@ -642,10 +644,11 @@ export default defineComponent({
RunDataHtml,
RunDataSearch,
},
mixins: [genericHelpers, pinData],
mixins: [genericHelpers],
props: {
nodeUi: {
node: {
type: Object as PropType<INodeUi>,
default: null,
},
runIndex: {
type: Number,
@@ -674,6 +677,7 @@ export default defineComponent({
},
paneType: {
type: String as PropType<NodePanelType>,
required: true,
},
overrideOutputs: {
type: Array as PropType<number[]>,
@@ -697,14 +701,21 @@ export default defineComponent({
default: false,
},
},
setup() {
setup(props) {
const ndvStore = useNDVStore();
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks();
const node = toRef(props, 'node');
const pinnedData = usePinnedData(node, {
runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode(props.paneType),
});
return {
...useToast(),
externalHooks,
nodeHelpers,
pinnedData,
};
},
data() {
@@ -761,9 +772,6 @@ export default defineComponent({
displayMode(): IRunDataDisplayMode {
return this.ndvStore.getPanelDisplayMode(this.paneType);
},
node(): INodeUi | null {
return (this.nodeUi as INodeUi | null) || null;
},
nodeType(): INodeTypeDescription | null {
if (this.node) {
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
@@ -796,7 +804,7 @@ export default defineComponent({
return (
!nonMainInputs &&
!this.isPaneTypeInput &&
this.isPinDataNodeType &&
this.pinnedData.isValidNodeType.value &&
!(this.binaryData && this.binaryData.length > 0)
);
},
@@ -832,7 +840,7 @@ export default defineComponent({
!this.isExecuting &&
this.node &&
((this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name)) ||
this.hasPinData),
this.pinnedData.hasData.value),
);
},
isArtificialRecoveredEventItem(): boolean {
@@ -864,7 +872,9 @@ export default defineComponent({
return this.getDataCount(this.runIndex, this.currentOutputIndex);
},
unfilteredDataCount(): number {
return this.pinData ? this.pinData.length : this.rawInputData.length;
return this.pinnedData.data.value
? this.pinnedData.data.value.length
: this.rawInputData.length;
},
dataSizeInMB(): string {
return (this.dataSize / 1024 / 1000).toLocaleString();
@@ -1072,8 +1082,8 @@ export default defineComponent({
useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG).value = 'true';
},
enterEditMode({ origin }: EnterEditModeArgs) {
const inputData = this.pinData
? clearJsonKey(this.pinData)
const inputData = this.pinnedData.data.value
? clearJsonKey(this.pinnedData.data.value)
: executionDataToJson(this.rawInputData);
const data = inputData.length > 0 ? inputData : TEST_PIN_DATA;
@@ -1086,9 +1096,9 @@ export default defineComponent({
click_type: origin === 'editIconButton' ? 'button' : 'link',
session_id: this.sessionId,
run_index: this.runIndex,
is_output_present: this.hasNodeRun || this.hasPinData,
view: !this.hasNodeRun && !this.hasPinData ? 'undefined' : this.displayMode,
is_data_pinned: this.hasPinData,
is_output_present: this.hasNodeRun || this.pinnedData.hasData.value,
view: !this.hasNodeRun && !this.pinnedData.hasData.value ? 'undefined' : this.displayMode,
is_data_pinned: this.pinnedData.hasData.value,
});
},
onClickCancelEdit() {
@@ -1106,7 +1116,7 @@ export default defineComponent({
this.clearAllStickyNotifications();
try {
this.setPinData(this.node, clearJsonKey(value) as INodeExecutionData[], 'save-edit');
this.pinnedData.setData(clearJsonKey(value) as INodeExecutionData[], 'save-edit');
} catch (error) {
console.error(error);
return;
@@ -1135,7 +1145,7 @@ export default defineComponent({
node_type: this.activeNode.type,
session_id: this.sessionId,
run_index: this.runIndex,
view: !this.hasNodeRun && !this.hasPinData ? 'none' : this.displayMode,
view: !this.hasNodeRun && !this.pinnedData.hasData.value ? 'none' : this.displayMode,
};
void this.externalHooks.run('runData.onTogglePinData', telemetryPayload);
@@ -1144,13 +1154,13 @@ export default defineComponent({
this.nodeHelpers.updateNodeParameterIssues(this.node);
if (this.hasPinData) {
this.unsetPinData(this.node, source);
if (this.pinnedData.hasData.value) {
this.pinnedData.unsetData(source);
return;
}
try {
this.setPinData(this.node, this.rawInputData, 'pin-icon-click');
this.pinnedData.setData(this.rawInputData, 'pin-icon-click');
} catch (error) {
console.error(error);
return;
@@ -1299,14 +1309,14 @@ export default defineComponent({
return inputData;
},
getPinDataOrLiveData(inputData: INodeExecutionData[]): INodeExecutionData[] {
if (this.pinData && !this.isProductionExecutionPreview) {
return Array.isArray(this.pinData)
? this.pinData.map((value) => ({
if (this.pinnedData.data.value && !this.isProductionExecutionPreview) {
return Array.isArray(this.pinnedData.data.value)
? this.pinnedData.data.value.map((value) => ({
json: value,
}))
: [
{
json: this.pinData,
json: this.pinnedData.data.value,
},
];
}

View File

@@ -37,11 +37,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import { mapStores, storeToRefs } from 'pinia';
import jp from 'jsonpath';
import type { INodeUi } from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { pinData } from '@/mixins/pinData';
import { genericHelpers } from '@/mixins/genericHelpers';
import { clearJsonKey, convertPath } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
@@ -52,6 +51,7 @@ import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import { nonExistingJsonPath } from '@/constants';
import { useClipboard } from '@/composables/useClipboard';
import { usePinnedData } from '@/composables/usePinnedData';
type JsonPathData = {
path: string;
@@ -60,7 +60,7 @@ type JsonPathData = {
export default defineComponent({
name: 'RunDataJsonActions',
mixins: [genericHelpers, pinData],
mixins: [genericHelpers],
props: {
node: {
type: Object as PropType<INodeUi>,
@@ -93,14 +93,18 @@ export default defineComponent({
},
},
setup() {
const ndvStore = useNDVStore();
const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
const clipboard = useClipboard();
const { activeNode } = storeToRefs(ndvStore);
const pinnedData = usePinnedData(activeNode);
return {
i18n,
nodeHelpers,
clipboard,
pinnedData,
...useToast(),
};
},
@@ -123,8 +127,8 @@ export default defineComponent({
const inExecutionsFrame =
window !== window.parent && window.parent.location.pathname.includes('/executions');
if (this.hasPinData && !inExecutionsFrame) {
selectedValue = clearJsonKey(this.pinData as object);
if (this.pinnedData.hasData.value && !inExecutionsFrame) {
selectedValue = clearJsonKey(this.pinnedData.data.value as object);
} else {
selectedValue = executionDataToJson(
this.nodeHelpers.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex),

View File

@@ -84,7 +84,7 @@ describe('RunData', () => {
const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) =>
createComponentRenderer(RunData, {
props: {
nodeUi: {
node: {
name: 'Test Node',
},
},
@@ -103,7 +103,7 @@ describe('RunData', () => {
},
})({
props: {
nodeUi: {
node: {
id: '1',
name: 'Test Node',
position: [0, 0],