Merge branch 'master' of github.com:n8n-io/n8n into ado-2808-1

This commit is contained in:
Mutasem Aldmour 2024-11-11 09:24:03 +01:00
commit cc97c6c026
No known key found for this signature in database
GPG key ID: 3DFA8122BB7FD6B8
27 changed files with 1119 additions and 759 deletions

View file

@ -11,6 +11,8 @@ Photos and videos are recommended.
Include links to **Linear ticket** or Github issue or Community forum post. Include links to **Linear ticket** or Github issue or Community forum post.
Important in order to close *automatically* and provide context to reviewers. Important in order to close *automatically* and provide context to reviewers.
--> -->
<!-- Use "closes #<issue-number>", "fixes #<issue-number>", or "resolves #<issue-number>" to automatically close issues when the PR is merged. -->
## Review / Merge checklist ## Review / Merge checklist

View file

@ -14,7 +14,7 @@ import {
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
import { getTracingConfig } from '../../../../../utils/tracing'; import { getTracingConfig } from '../../../../../utils/tracing';
import { extractParsedOutput } from '../utils'; import { checkForStructuredTools, extractParsedOutput } from '../utils';
export async function conversationalAgentExecute( export async function conversationalAgentExecute(
this: IExecuteFunctions, this: IExecuteFunctions,
@ -34,6 +34,8 @@ export async function conversationalAgentExecute(
const tools = await getConnectedTools(this, nodeVersion >= 1.5); const tools = await getConnectedTools(this, nodeVersion >= 1.5);
const outputParsers = await getOptionalOutputParsers(this); const outputParsers = await getOptionalOutputParsers(this);
await checkForStructuredTools(tools, this.getNode(), 'Conversational Agent');
// TODO: Make it possible in the future to use values for other items than just 0 // TODO: Make it possible in the future to use values for other items than just 0
const options = this.getNodeParameter('options', 0, {}) as { const options = this.getNodeParameter('options', 0, {}) as {
systemMessage?: string; systemMessage?: string;

View file

@ -14,7 +14,7 @@ import { getConnectedTools, getPromptInputByType } from '../../../../../utils/he
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
import { getTracingConfig } from '../../../../../utils/tracing'; import { getTracingConfig } from '../../../../../utils/tracing';
import { extractParsedOutput } from '../utils'; import { checkForStructuredTools, extractParsedOutput } from '../utils';
export async function planAndExecuteAgentExecute( export async function planAndExecuteAgentExecute(
this: IExecuteFunctions, this: IExecuteFunctions,
@ -28,6 +28,7 @@ export async function planAndExecuteAgentExecute(
const tools = await getConnectedTools(this, nodeVersion >= 1.5); const tools = await getConnectedTools(this, nodeVersion >= 1.5);
await checkForStructuredTools(tools, this.getNode(), 'Plan & Execute Agent');
const outputParsers = await getOptionalOutputParsers(this); const outputParsers = await getOptionalOutputParsers(this);
const options = this.getNodeParameter('options', 0, {}) as { const options = this.getNodeParameter('options', 0, {}) as {

View file

@ -19,7 +19,7 @@ import {
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
import { getTracingConfig } from '../../../../../utils/tracing'; import { getTracingConfig } from '../../../../../utils/tracing';
import { extractParsedOutput } from '../utils'; import { checkForStructuredTools, extractParsedOutput } from '../utils';
export async function reActAgentAgentExecute( export async function reActAgentAgentExecute(
this: IExecuteFunctions, this: IExecuteFunctions,
@ -33,6 +33,8 @@ export async function reActAgentAgentExecute(
const tools = await getConnectedTools(this, nodeVersion >= 1.5); const tools = await getConnectedTools(this, nodeVersion >= 1.5);
await checkForStructuredTools(tools, this.getNode(), 'ReAct Agent');
const outputParsers = await getOptionalOutputParsers(this); const outputParsers = await getOptionalOutputParsers(this);
const options = this.getNodeParameter('options', 0, {}) as { const options = this.getNodeParameter('options', 0, {}) as {

View file

@ -1,5 +1,7 @@
import type { ZodObjectAny } from '@langchain/core/dist/types/zod';
import type { BaseOutputParser } from '@langchain/core/output_parsers'; import type { BaseOutputParser } from '@langchain/core/output_parsers';
import type { IExecuteFunctions } from 'n8n-workflow'; import type { DynamicStructuredTool, Tool } from 'langchain/tools';
import { NodeOperationError, type IExecuteFunctions, type INode } from 'n8n-workflow';
export async function extractParsedOutput( export async function extractParsedOutput(
ctx: IExecuteFunctions, ctx: IExecuteFunctions,
@ -17,3 +19,24 @@ export async function extractParsedOutput(
// with fallback to the original output if it's not present // with fallback to the original output if it's not present
return parsedOutput?.output ?? parsedOutput; return parsedOutput?.output ?? parsedOutput;
} }
export async function checkForStructuredTools(
tools: Array<Tool | DynamicStructuredTool<ZodObjectAny>>,
node: INode,
currentAgentType: string,
) {
const dynamicStructuredTools = tools.filter(
(tool) => tool.constructor.name === 'DynamicStructuredTool',
);
if (dynamicStructuredTools.length > 0) {
const getToolName = (tool: Tool | DynamicStructuredTool) => `"${tool.name}"`;
throw new NodeOperationError(
node,
`The selected tools are not supported by "${currentAgentType}", please use "Tools Agent" instead`,
{
itemIndex: 0,
description: `Incompatible connected tools: ${dynamicStructuredTools.map(getToolName).join(', ')}`,
},
);
}
}

View file

@ -0,0 +1,106 @@
import type { Tool } from 'langchain/tools';
import { DynamicStructuredTool } from 'langchain/tools';
import { NodeOperationError } from 'n8n-workflow';
import type { INode } from 'n8n-workflow';
import { z } from 'zod';
import { checkForStructuredTools } from '../agents/utils';
describe('checkForStructuredTools', () => {
let mockNode: INode;
beforeEach(() => {
mockNode = {
id: 'test-node',
name: 'Test Node',
type: 'test',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
});
it('should not throw error when no DynamicStructuredTools are present', async () => {
const tools = [
{
name: 'regular-tool',
constructor: { name: 'Tool' },
} as Tool,
];
await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).resolves.not.toThrow();
});
it('should throw NodeOperationError when DynamicStructuredTools are present', async () => {
const dynamicTool = new DynamicStructuredTool({
name: 'dynamic-tool',
description: 'test tool',
schema: z.object({}),
func: async () => 'result',
});
const tools: Array<Tool | DynamicStructuredTool> = [dynamicTool];
await expect(checkForStructuredTools(tools, mockNode, 'Conversation Agent')).rejects.toThrow(
NodeOperationError,
);
await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).rejects.toMatchObject({
message:
'The selected tools are not supported by "Conversation Agent", please use "Tools Agent" instead',
description: 'Incompatible connected tools: "dynamic-tool"',
});
});
it('should list multiple dynamic tools in error message', async () => {
const dynamicTool1 = new DynamicStructuredTool({
name: 'dynamic-tool-1',
description: 'test tool 1',
schema: z.object({}),
func: async () => 'result',
});
const dynamicTool2 = new DynamicStructuredTool({
name: 'dynamic-tool-2',
description: 'test tool 2',
schema: z.object({}),
func: async () => 'result',
});
const tools = [dynamicTool1, dynamicTool2];
await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).rejects.toMatchObject({
description: 'Incompatible connected tools: "dynamic-tool-1", "dynamic-tool-2"',
});
});
it('should throw error with mixed tool types and list only dynamic tools in error message', async () => {
const regularTool = {
name: 'regular-tool',
constructor: { name: 'Tool' },
} as Tool;
const dynamicTool = new DynamicStructuredTool({
name: 'dynamic-tool',
description: 'test tool',
schema: z.object({}),
func: async () => 'result',
});
const tools = [regularTool, dynamicTool];
await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).rejects.toMatchObject({
message:
'The selected tools are not supported by "Conversation Agent", please use "Tools Agent" instead',
description: 'Incompatible connected tools: "dynamic-tool"',
});
});
});

View file

@ -390,7 +390,15 @@ export class LoadNodesAndCredentials {
const toWatch = loader.isLazyLoaded const toWatch = loader.isLazyLoaded
? ['**/nodes.json', '**/credentials.json'] ? ['**/nodes.json', '**/credentials.json']
: ['**/*.js', '**/*.json']; : ['**/*.js', '**/*.json'];
watch(toWatch, { cwd: realModulePath }).on('change', reloader); const files = await glob(toWatch, {
cwd: realModulePath,
ignore: ['node_modules/**'],
});
const watcher = watch(files, {
cwd: realModulePath,
ignoreInitial: true,
});
watcher.on('add', reloader).on('change', reloader).on('unlink', reloader);
}); });
} }
} }

View file

@ -5,18 +5,22 @@ import type { TaskRunner } from '../task-broker.service';
export class TaskRunnerOomError extends ApplicationError { export class TaskRunnerOomError extends ApplicationError {
public description: string; public description: string;
constructor(runnerId: TaskRunner['id'], isCloudDeployment: boolean) { constructor(
super(`Task runner (${runnerId}) ran out of memory.`, { level: 'error' }); public readonly runnerId: TaskRunner['id'],
isCloudDeployment: boolean,
) {
super('Node ran out of memory.', { level: 'error' });
const fixSuggestions = { const fixSuggestions = {
reduceItems: 'Reduce the number of items processed at a time by batching the input.', reduceItems:
'Reduce the number of items processed at a time, by batching them using a loop node',
increaseMemory: increaseMemory:
"Increase the memory available to the task runner with 'N8N_RUNNERS_MAX_OLD_SPACE_SIZE' environment variable.", "Increase the memory available to the task runner with 'N8N_RUNNERS_MAX_OLD_SPACE_SIZE' environment variable",
upgradePlan: 'Upgrade your cloud plan to increase the available memory.', upgradePlan: 'Upgrade your cloud plan to increase the available memory',
}; };
const subtitle = const subtitle =
'The runner executing the code ran out of memory. This usually happens when there are too many items to process. You can try the following:'; 'This usually happens when there are too many items to process. You can try the following:';
const suggestions = isCloudDeployment const suggestions = isCloudDeployment
? [fixSuggestions.reduceItems, fixSuggestions.upgradePlan] ? [fixSuggestions.reduceItems, fixSuggestions.upgradePlan]
: [fixSuggestions.reduceItems, fixSuggestions.increaseMemory]; : [fixSuggestions.reduceItems, fixSuggestions.increaseMemory];

View file

@ -635,7 +635,6 @@ export type WorkflowCallerPolicyDefaultOption = 'any' | 'none' | 'workflowsFromA
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
errorWorkflow?: string; errorWorkflow?: string;
saveManualExecutions?: boolean;
timezone?: string; timezone?: string;
executionTimeout?: number; executionTimeout?: number;
maxExecutionTimeout?: number; maxExecutionTimeout?: number;
@ -1594,3 +1593,44 @@ export type ApiKey = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
export type InputPanel = {
displayMode: IRunDataDisplayMode;
nodeName?: string;
run?: number;
branch?: number;
data: {
isEmpty: boolean;
};
};
export type OutputPanel = {
branch?: number;
displayMode: IRunDataDisplayMode;
data: {
isEmpty: boolean;
};
editMode: {
enabled: boolean;
value: string;
};
};
export type Draggable = {
isDragging: boolean;
type: string;
data: string;
dimensions: DOMRect | null;
activeTarget: { id: string; stickyPosition: null | XYPosition } | null;
};
export type MainPanelType = 'regular' | 'dragless' | 'inputless' | 'unknown' | 'wide';
export type MainPanelDimensions = Record<
MainPanelType,
{
relativeLeft: number;
relativeRight: number;
relativeWidth: number;
}
>;

View file

@ -63,8 +63,8 @@ export const defaultSettings: FrontendSettings = {
}, },
publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } }, publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } },
pushBackend: 'websocket', pushBackend: 'websocket',
saveDataErrorExecution: 'DEFAULT', saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'DEFAULT', saveDataSuccessExecution: 'all',
saveManualExecutions: false, saveManualExecutions: false,
saveExecutionProgress: false, saveExecutionProgress: false,
sso: { sso: {

View file

@ -26,7 +26,8 @@ describe('InlineExpressionTip.vue', () => {
beforeEach(() => { beforeEach(() => {
mockNdvState = { mockNdvState = {
hasInputData: true, hasInputData: true,
isNDVDataEmpty: vi.fn(() => true), isInputPanelEmpty: true,
isOutputPanelEmpty: true,
setHighlightDraggables: vi.fn(), setHighlightDraggables: vi.fn(),
}; };
}); });
@ -42,7 +43,8 @@ describe('InlineExpressionTip.vue', () => {
test('should show the drag-n-drop tip', async () => { test('should show the drag-n-drop tip', async () => {
mockNdvState = { mockNdvState = {
hasInputData: true, hasInputData: true,
isNDVDataEmpty: vi.fn(() => false), isInputPanelEmpty: false,
isOutputPanelEmpty: false,
focusedMappableInput: 'Some Input', focusedMappableInput: 'Some Input',
setHighlightDraggables: vi.fn(), setHighlightDraggables: vi.fn(),
}; };
@ -62,7 +64,8 @@ describe('InlineExpressionTip.vue', () => {
mockNdvState = { mockNdvState = {
hasInputData: false, hasInputData: false,
isInputParentOfActiveNode: true, isInputParentOfActiveNode: true,
isNDVDataEmpty: vi.fn(() => false), isInputPanelEmpty: false,
isOutputPanelEmpty: false,
focusedMappableInput: 'Some Input', focusedMappableInput: 'Some Input',
setHighlightDraggables: vi.fn(), setHighlightDraggables: vi.fn(),
}; };
@ -77,7 +80,8 @@ describe('InlineExpressionTip.vue', () => {
test('should show the correct tip for objects', async () => { test('should show the correct tip for objects', async () => {
mockNdvState = { mockNdvState = {
hasInputData: true, hasInputData: true,
isNDVDataEmpty: vi.fn(() => false), isInputPanelEmpty: false,
isOutputPanelEmpty: false,
focusedMappableInput: 'Some Input', focusedMappableInput: 'Some Input',
setHighlightDraggables: vi.fn(), setHighlightDraggables: vi.fn(),
}; };
@ -106,7 +110,8 @@ describe('InlineExpressionTip.vue', () => {
test('should show the correct tip for primitives', async () => { test('should show the correct tip for primitives', async () => {
mockNdvState = { mockNdvState = {
hasInputData: true, hasInputData: true,
isNDVDataEmpty: vi.fn(() => false), isInputPanelEmpty: false,
isOutputPanelEmpty: false,
focusedMappableInput: 'Some Input', focusedMappableInput: 'Some Input',
setHighlightDraggables: vi.fn(), setHighlightDraggables: vi.fn(),
}; };

View file

@ -30,7 +30,7 @@ const canAddDotToExpression = ref(false);
const resolvedExpressionHasFields = ref(false); const resolvedExpressionHasFields = ref(false);
const canDragToFocusedInput = computed( const canDragToFocusedInput = computed(
() => !ndvStore.isNDVDataEmpty('input') && ndvStore.focusedMappableInput, () => !ndvStore.isInputPanelEmpty && ndvStore.focusedMappableInput,
); );
const emptyExpression = computed(() => props.unresolvedExpression.trim().length === 0); const emptyExpression = computed(() => props.unresolvedExpression.trim().length === 0);

View file

@ -9,7 +9,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue'; import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import type { XYPosition } from '@/Interface'; import type { MainPanelType, XYPosition } from '@/Interface';
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'; import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
@ -20,7 +20,7 @@ const PANEL_WIDTH = 350;
const PANEL_WIDTH_LARGE = 420; const PANEL_WIDTH_LARGE = 420;
const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH; const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
const initialMainPanelWidth: { [key: string]: number } = { const initialMainPanelWidth: Record<MainPanelType, number> = {
regular: MAIN_NODE_PANEL_WIDTH, regular: MAIN_NODE_PANEL_WIDTH,
dragless: MAIN_NODE_PANEL_WIDTH, dragless: MAIN_NODE_PANEL_WIDTH,
unknown: MAIN_NODE_PANEL_WIDTH, unknown: MAIN_NODE_PANEL_WIDTH,
@ -106,22 +106,16 @@ watch(containerWidth, (width) => {
setPositions(mainPanelDimensions.value.relativeLeft); setPositions(mainPanelDimensions.value.relativeLeft);
}); });
const currentNodePaneType = computed((): string => { const currentNodePaneType = computed((): MainPanelType => {
if (!hasInputSlot.value) return 'inputless'; if (!hasInputSlot.value) return 'inputless';
if (!props.isDraggable) return 'dragless'; if (!props.isDraggable) return 'dragless';
if (props.nodeType === null) return 'unknown'; if (props.nodeType === null) return 'unknown';
return props.nodeType.parameterPane ?? 'regular'; return props.nodeType.parameterPane ?? 'regular';
}); });
const mainPanelDimensions = computed( const mainPanelDimensions = computed(() => {
(): { return ndvStore.mainPanelDimensions[currentNodePaneType.value];
relativeWidth: number; });
relativeLeft: number;
relativeRight: number;
} => {
return ndvStore.getMainPanelDimensions(currentNodePaneType.value);
},
);
const calculatedPositions = computed( const calculatedPositions = computed(
(): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } => { (): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } => {

View file

@ -141,6 +141,10 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS); const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS);
const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS); const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS);
const websiteCategoryURL = templatesStore.websiteTemplateRepositoryParameters;
websiteCategoryURL.append('utm_user_role', 'AdvancedAI');
return { return {
value: AI_NODE_CREATOR_VIEW, value: AI_NODE_CREATOR_VIEW,
title: i18n.baseText('nodeCreator.aiPanel.aiNodes'), title: i18n.baseText('nodeCreator.aiPanel.aiNodes'),
@ -154,7 +158,7 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
icon: 'box-open', icon: 'box-open',
description: i18n.baseText('nodeCreator.aiPanel.linkItem.description'), description: i18n.baseText('nodeCreator.aiPanel.linkItem.description'),
name: 'ai_templates_root', name: 'ai_templates_root',
url: templatesStore.getWebsiteCategoryURL(undefined, 'AdvancedAI'), url: websiteCategoryURL.toString(),
tag: { tag: {
type: 'info', type: 'info',
text: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerTag'), text: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerTag'),

View file

@ -316,7 +316,7 @@ async function onClick() {
codeGenerationInProgress.value = false; codeGenerationInProgress.value = false;
} }
if (isChatNode.value || (isChatChild.value && ndvStore.isNDVDataEmpty('input'))) { if (isChatNode.value || (isChatChild.value && ndvStore.isInputPanelEmpty)) {
ndvStore.setActiveNodeName(null); ndvStore.setActiveNodeName(null);
nodeViewEventBus.emit('openChat'); nodeViewEventBus.emit('openChat');
} else if (isListeningForEvents.value) { } else if (isListeningForEvents.value) {

View file

@ -78,7 +78,7 @@ const { isSubNodeType } = useNodeType({
}); });
const pinnedData = usePinnedData(activeNode, { const pinnedData = usePinnedData(activeNode, {
runIndex: props.runIndex, runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode('output'), displayMode: ndvStore.outputPanelDisplayMode,
}); });
// Data // Data

View file

@ -54,7 +54,8 @@ describe('ParameterInput.vue', () => {
type: 'test', type: 'test',
typeVersion: 1, typeVersion: 1,
}, },
isNDVDataEmpty: vi.fn(() => false), isInputPanelEmpty: false,
isOutputPanelEmpty: false,
}; };
mockNodeTypesState = { mockNodeTypesState = {
allNodeTypes: [], allNodeTypes: [],

View file

@ -523,7 +523,7 @@ const isHtmlNode = computed(() => !!node.value && node.value.type === HTML_NODE_
const isInputTypeString = computed(() => props.parameter.type === 'string'); const isInputTypeString = computed(() => props.parameter.type === 'string');
const isInputTypeNumber = computed(() => props.parameter.type === 'number'); const isInputTypeNumber = computed(() => props.parameter.type === 'number');
const isInputDataEmpty = computed(() => ndvStore.isNDVDataEmpty('input')); const isInputDataEmpty = computed(() => ndvStore.isInputPanelEmpty);
const isDropDisabled = computed( const isDropDisabled = computed(
() => () =>
props.parameter.noDataExpression || props.parameter.noDataExpression ||

View file

@ -187,12 +187,17 @@ const node = toRef(props, 'node');
const pinnedData = usePinnedData(node, { const pinnedData = usePinnedData(node, {
runIndex: props.runIndex, runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode(props.paneType), displayMode:
props.paneType === 'input' ? ndvStore.inputPanelDisplayMode : ndvStore.outputPanelDisplayMode,
}); });
const { isSubNodeType } = useNodeType({ const { isSubNodeType } = useNodeType({
node, node,
}); });
const displayMode = computed(() =>
props.paneType === 'input' ? ndvStore.inputPanelDisplayMode : ndvStore.outputPanelDisplayMode,
);
const isReadOnlyRoute = computed(() => route.meta.readOnlyCanvas === true); const isReadOnlyRoute = computed(() => route.meta.readOnlyCanvas === true);
const isWaitNodeWaiting = computed( const isWaitNodeWaiting = computed(
() => () =>
@ -202,7 +207,6 @@ const isWaitNodeWaiting = computed(
); );
const { activeNode } = storeToRefs(ndvStore); const { activeNode } = storeToRefs(ndvStore);
const displayMode = computed(() => ndvStore.getPanelDisplayMode(props.paneType));
const nodeType = computed(() => { const nodeType = computed(() => {
if (!node.value) return null; if (!node.value) return null;

View file

@ -145,4 +145,50 @@ describe('WorkflowSettingsVue', () => {
expect(getByTestId('workflow-caller-policy-workflow-ids')).toHaveValue(cleanedUpWorkflowList); expect(getByTestId('workflow-caller-policy-workflow-ids')).toHaveValue(cleanedUpWorkflowList);
}); });
test.each([
['workflow-settings-save-failed-executions', 'Default - Save', () => {}],
[
'workflow-settings-save-failed-executions',
'Default - Do not save',
() => {
settingsStore.saveDataErrorExecution = 'none';
},
],
['workflow-settings-save-success-executions', 'Default - Save', () => {}],
[
'workflow-settings-save-success-executions',
'Default - Do not save',
() => {
settingsStore.saveDataSuccessExecution = 'none';
},
],
[
'workflow-settings-save-manual-executions',
'Default - Save',
() => {
settingsStore.saveManualExecutions = true;
},
],
['workflow-settings-save-manual-executions', 'Default - Do not save', () => {}],
[
'workflow-settings-save-execution-progress',
'Default - Save',
() => {
settingsStore.saveDataProgressExecution = true;
},
],
['workflow-settings-save-execution-progress', 'Default - Do not save', () => {}],
])(
'should show %s dropdown correct default value as %s',
async (testId, optionText, storeSetter) => {
storeSetter();
const { getByTestId } = createComponent({ pinia });
await nextTick();
const dropdownItems = await getDropdownItems(getByTestId(testId));
expect(dropdownItems[0]).toHaveTextContent(optionText);
},
);
}); });

View file

@ -70,14 +70,14 @@ const helpTexts = computed(() => ({
workflowCallerPolicy: i18n.baseText('workflowSettings.helpTexts.workflowCallerPolicy'), workflowCallerPolicy: i18n.baseText('workflowSettings.helpTexts.workflowCallerPolicy'),
workflowCallerIds: i18n.baseText('workflowSettings.helpTexts.workflowCallerIds'), workflowCallerIds: i18n.baseText('workflowSettings.helpTexts.workflowCallerIds'),
})); }));
const defaultValues = computed(() => ({ const defaultValues = ref({
timezone: 'America/New_York', timezone: 'America/New_York',
saveDataErrorExecution: 'all', saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all', saveDataSuccessExecution: 'all',
saveExecutionProgress: false, saveExecutionProgress: false,
saveManualExecutions: false, saveManualExecutions: false,
workflowCallerPolicy: 'workflowsFromSameOwner', workflowCallerPolicy: 'workflowsFromSameOwner',
})); });
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly); const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
const workflowName = computed(() => workflowsStore.workflowName); const workflowName = computed(() => workflowsStore.workflowName);
const workflowId = computed(() => workflowsStore.workflowId); const workflowId = computed(() => workflowsStore.workflowId);
@ -145,8 +145,7 @@ const loadWorkflowCallerPolicyOptions = async () => {
}; };
const loadSaveDataErrorExecutionOptions = async () => { const loadSaveDataErrorExecutionOptions = async () => {
saveDataErrorExecutionOptions.value.length = 0; saveDataErrorExecutionOptions.value = [
saveDataErrorExecutionOptions.value.push.apply(saveDataErrorExecutionOptions.value, [
{ {
key: 'DEFAULT', key: 'DEFAULT',
value: i18n.baseText('workflowSettings.saveDataErrorExecutionOptions.defaultSave', { value: i18n.baseText('workflowSettings.saveDataErrorExecutionOptions.defaultSave', {
@ -166,12 +165,11 @@ const loadSaveDataErrorExecutionOptions = async () => {
key: 'none', key: 'none',
value: i18n.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotSave'), value: i18n.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotSave'),
}, },
]); ];
}; };
const loadSaveDataSuccessExecutionOptions = async () => { const loadSaveDataSuccessExecutionOptions = async () => {
saveDataSuccessExecutionOptions.value.length = 0; saveDataSuccessExecutionOptions.value = [
saveDataSuccessExecutionOptions.value.push.apply(saveDataSuccessExecutionOptions.value, [
{ {
key: 'DEFAULT', key: 'DEFAULT',
value: i18n.baseText('workflowSettings.saveDataSuccessExecutionOptions.defaultSave', { value: i18n.baseText('workflowSettings.saveDataSuccessExecutionOptions.defaultSave', {
@ -191,12 +189,11 @@ const loadSaveDataSuccessExecutionOptions = async () => {
key: 'none', key: 'none',
value: i18n.baseText('workflowSettings.saveDataSuccessExecutionOptions.doNotSave'), value: i18n.baseText('workflowSettings.saveDataSuccessExecutionOptions.doNotSave'),
}, },
]); ];
}; };
const loadSaveExecutionProgressOptions = async () => { const loadSaveExecutionProgressOptions = async () => {
saveExecutionProgressOptions.value.length = 0; saveExecutionProgressOptions.value = [
saveExecutionProgressOptions.value.push.apply(saveExecutionProgressOptions.value, [
{ {
key: 'DEFAULT', key: 'DEFAULT',
value: i18n.baseText('workflowSettings.saveExecutionProgressOptions.defaultSave', { value: i18n.baseText('workflowSettings.saveExecutionProgressOptions.defaultSave', {
@ -215,29 +212,30 @@ const loadSaveExecutionProgressOptions = async () => {
key: false, key: false,
value: i18n.baseText('workflowSettings.saveExecutionProgressOptions.doNotSave'), value: i18n.baseText('workflowSettings.saveExecutionProgressOptions.doNotSave'),
}, },
]); ];
}; };
const loadSaveManualOptions = async () => { const loadSaveManualOptions = async () => {
saveManualOptions.value.length = 0; saveManualOptions.value = [
saveManualOptions.value.push({ {
key: 'DEFAULT', key: 'DEFAULT',
value: i18n.baseText('workflowSettings.saveManualOptions.defaultSave', { value: i18n.baseText('workflowSettings.saveManualOptions.defaultSave', {
interpolate: { interpolate: {
defaultValue: defaultValues.value.saveManualExecutions defaultValue: defaultValues.value.saveManualExecutions
? i18n.baseText('workflowSettings.saveManualOptions.save') ? i18n.baseText('workflowSettings.saveManualOptions.save')
: i18n.baseText('workflowSettings.saveManualOptions.doNotSave'), : i18n.baseText('workflowSettings.saveManualOptions.doNotSave'),
}, },
}), }),
}); },
saveManualOptions.value.push({ {
key: true, key: true,
value: i18n.baseText('workflowSettings.saveManualOptions.save'), value: i18n.baseText('workflowSettings.saveManualOptions.save'),
}); },
saveManualOptions.value.push({ {
key: false, key: false,
value: i18n.baseText('workflowSettings.saveManualOptions.doNotSave'), value: i18n.baseText('workflowSettings.saveManualOptions.doNotSave'),
}); },
];
}; };
const loadTimezones = async () => { const loadTimezones = async () => {
@ -400,6 +398,7 @@ onMounted(async () => {
defaultValues.value.saveDataErrorExecution = settingsStore.saveDataErrorExecution; defaultValues.value.saveDataErrorExecution = settingsStore.saveDataErrorExecution;
defaultValues.value.saveDataSuccessExecution = settingsStore.saveDataSuccessExecution; defaultValues.value.saveDataSuccessExecution = settingsStore.saveDataSuccessExecution;
defaultValues.value.saveManualExecutions = settingsStore.saveManualExecutions; defaultValues.value.saveManualExecutions = settingsStore.saveManualExecutions;
defaultValues.value.saveExecutionProgress = settingsStore.saveDataProgressExecution;
defaultValues.value.timezone = rootStore.timezone; defaultValues.value.timezone = rootStore.timezone;
defaultValues.value.workflowCallerPolicy = settingsStore.workflowCallerPolicyDefaultOption; defaultValues.value.workflowCallerPolicy = settingsStore.workflowCallerPolicyDefaultOption;
@ -423,7 +422,7 @@ onMounted(async () => {
); );
} }
const workflowSettingsData = deepCopy(workflowsStore.workflowSettings) as IWorkflowSettings; const workflowSettingsData = deepCopy(workflowsStore.workflowSettings);
if (workflowSettingsData.timezone === undefined) { if (workflowSettingsData.timezone === undefined) {
workflowSettingsData.timezone = 'DEFAULT'; workflowSettingsData.timezone = 'DEFAULT';
@ -438,7 +437,7 @@ onMounted(async () => {
workflowSettingsData.saveExecutionProgress = 'DEFAULT'; workflowSettingsData.saveExecutionProgress = 'DEFAULT';
} }
if (workflowSettingsData.saveManualExecutions === undefined) { if (workflowSettingsData.saveManualExecutions === undefined) {
workflowSettingsData.saveManualExecutions = defaultValues.value.saveManualExecutions; workflowSettingsData.saveManualExecutions = 'DEFAULT';
} }
if (workflowSettingsData.callerPolicy === undefined) { if (workflowSettingsData.callerPolicy === undefined) {
workflowSettingsData.callerPolicy = defaultValues.value workflowSettingsData.callerPolicy = defaultValues.value

View file

@ -1,10 +1,13 @@
import type { import type {
INodeUi, Draggable,
InputPanel,
IRunDataDisplayMode, IRunDataDisplayMode,
MainPanelDimensions,
MainPanelType,
NDVState, NDVState,
NodePanelType, NodePanelType,
OutputPanel,
TargetItem, TargetItem,
XYPosition,
} from '@/Interface'; } from '@/Interface';
import { useStorage } from '@/composables/useStorage'; import { useStorage } from '@/composables/useStorage';
import { import {
@ -13,316 +16,411 @@ import {
LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED, LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED,
STORES, STORES,
} from '@/constants'; } from '@/constants';
import type { INodeExecutionData, INodeIssues } from 'n8n-workflow'; import type { INodeIssues } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { useWorkflowsStore } from './workflows.store'; import { useWorkflowsStore } from './workflows.store';
import { computed, ref } from 'vue';
export const useNDVStore = defineStore(STORES.NDV, { const DEFAULT_MAIN_PANEL_DIMENSIONS = {
state: (): NDVState => ({ relativeLeft: 1,
activeNodeName: null, relativeRight: 1,
mainPanelDimensions: {}, relativeWidth: 1,
pushRef: '', };
input: {
displayMode: 'schema', export const useNDVStore = defineStore(STORES.NDV, () => {
nodeName: undefined, const localStorageMappingIsOnboarded = useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED);
run: undefined, const localStorageTableHoverIsOnboarded = useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED);
branch: undefined, const localStorageAutoCompleteIsOnboarded = useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED);
data: {
isEmpty: true, const activeNodeName = ref<string | null>(null);
}, const mainPanelDimensions = ref<MainPanelDimensions>({
unknown: { ...DEFAULT_MAIN_PANEL_DIMENSIONS },
regular: { ...DEFAULT_MAIN_PANEL_DIMENSIONS },
dragless: { ...DEFAULT_MAIN_PANEL_DIMENSIONS },
inputless: { ...DEFAULT_MAIN_PANEL_DIMENSIONS },
wide: { ...DEFAULT_MAIN_PANEL_DIMENSIONS },
});
const pushRef = ref('');
const input = ref<InputPanel>({
displayMode: 'schema',
nodeName: undefined,
run: undefined,
branch: undefined,
data: {
isEmpty: true,
}, },
output: { });
displayMode: 'table', const output = ref<OutputPanel>({
branch: undefined, displayMode: 'table',
data: { branch: undefined,
isEmpty: true, data: {
}, isEmpty: true,
editMode: {
enabled: false,
value: '',
},
}, },
focusedMappableInput: '', editMode: {
focusedInputPath: '', enabled: false,
mappingTelemetry: {}, value: '',
hoveringItem: null, },
expressionOutputItemIndex: 0, });
draggable: { const focusedMappableInput = ref('');
const focusedInputPath = ref('');
const mappingTelemetry = ref<Record<string, string | number | boolean>>({});
const hoveringItem = ref<null | TargetItem>(null);
const expressionOutputItemIndex = ref(0);
const draggable = ref<Draggable>({
isDragging: false,
type: '',
data: '',
dimensions: null,
activeTarget: null,
});
const isMappingOnboarded = ref(localStorageMappingIsOnboarded.value === 'true');
const isTableHoverOnboarded = ref(localStorageTableHoverIsOnboarded.value === 'true');
const isAutocompleteOnboarded = ref(localStorageAutoCompleteIsOnboarded.value === 'true');
const highlightDraggables = ref(false);
const workflowsStore = useWorkflowsStore();
const activeNode = computed(() => {
return workflowsStore.getNodeByName(activeNodeName.value || '');
});
const ndvInputData = computed(() => {
const executionData = workflowsStore.getWorkflowExecution;
const inputNodeName: string | undefined = input.value.nodeName;
const inputRunIndex: number = input.value.run ?? 0;
const inputBranchIndex: number = input.value.branch ?? 0;
if (
!executionData ||
!inputNodeName ||
inputRunIndex === undefined ||
inputBranchIndex === undefined
) {
return [];
}
return (
executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[
inputBranchIndex
] ?? []
);
});
const ndvInputNodeName = computed(() => {
return input.value.nodeName;
});
const ndvInputDataWithPinnedData = computed(() => {
const data = ndvInputData.value;
return ndvInputNodeName.value
? (workflowsStore.pinDataByNodeName(ndvInputNodeName.value) ?? data)
: data;
});
const hasInputData = computed(() => {
return ndvInputDataWithPinnedData.value.length > 0;
});
const inputPanelDisplayMode = computed(() => input.value.displayMode);
const outputPanelDisplayMode = computed(() => output.value.displayMode);
const isDraggableDragging = computed(() => draggable.value.isDragging);
const draggableType = computed(() => draggable.value.type);
const draggableData = computed(() => draggable.value.data);
const canDraggableDrop = computed(() => draggable.value.activeTarget !== null);
const outputPanelEditMode = computed(() => output.value.editMode);
const draggableStickyPos = computed(() => draggable.value.activeTarget?.stickyPosition ?? null);
const ndvNodeInputNumber = computed(() => {
const returnData: { [nodeName: string]: number[] } = {};
const workflow = workflowsStore.getCurrentWorkflow();
const activeNodeConections = (
workflow.connectionsByDestinationNode[activeNode.value?.name || ''] ?? {}
).main;
if (!activeNodeConections || activeNodeConections.length < 2) return returnData;
for (const [index, connection] of activeNodeConections.entries()) {
for (const node of connection) {
if (!returnData[node.node]) {
returnData[node.node] = [];
}
returnData[node.node].push(index + 1);
}
}
return returnData;
});
const ndvInputRunIndex = computed(() => input.value.run);
const ndvInputBranchIndex = computed(() => input.value.branch);
const isInputPanelEmpty = computed(() => input.value.data.isEmpty);
const isOutputPanelEmpty = computed(() => output.value.data.isEmpty);
const isInputParentOfActiveNode = computed(() => {
const inputNodeName = ndvInputNodeName.value;
if (!activeNode.value || !inputNodeName) {
return false;
}
const workflow = workflowsStore.getCurrentWorkflow();
const parentNodes = workflow.getParentNodes(activeNode.value.name, NodeConnectionType.Main, 1);
return parentNodes.includes(inputNodeName);
});
const getHoveringItem = computed(() => {
if (isInputParentOfActiveNode.value) {
return hoveringItem.value;
}
return null;
});
const expressionTargetItem = computed(() => {
if (getHoveringItem.value) {
return getHoveringItem.value;
}
if (expressionOutputItemIndex.value && ndvInputNodeName.value) {
return {
nodeName: ndvInputNodeName.value,
runIndex: ndvInputRunIndex.value ?? 0,
outputIndex: ndvInputBranchIndex.value ?? 0,
itemIndex: expressionOutputItemIndex.value,
};
}
return null;
});
const isNDVOpen = computed(() => activeNodeName.value !== null);
const setActiveNodeName = (nodeName: string | null): void => {
activeNodeName.value = nodeName;
};
const setInputNodeName = (nodeName: string | undefined): void => {
input.value.nodeName = nodeName;
};
const setInputRunIndex = (run?: number): void => {
input.value.run = run;
};
const setMainPanelDimensions = (params: {
panelType: MainPanelType;
dimensions: { relativeLeft?: number; relativeRight?: number; relativeWidth?: number };
}): void => {
mainPanelDimensions.value[params.panelType] = {
...mainPanelDimensions.value[params.panelType],
...params.dimensions,
};
};
const setNDVPushRef = (): void => {
pushRef.value = `ndv-${uuid()}`;
};
const resetNDVPushRef = (): void => {
pushRef.value = '';
};
const setPanelDisplayMode = (params: {
pane: NodePanelType;
mode: IRunDataDisplayMode;
}): void => {
if (params.pane === 'input') {
input.value.displayMode = params.mode;
} else {
output.value.displayMode = params.mode;
}
};
const setOutputPanelEditModeEnabled = (isEnabled: boolean): void => {
output.value.editMode.enabled = isEnabled;
};
const setOutputPanelEditModeValue = (payload: string): void => {
output.value.editMode.value = payload;
};
const setMappableNDVInputFocus = (paramName: string): void => {
focusedMappableInput.value = paramName;
};
const draggableStartDragging = ({
type,
data,
dimensions,
}: { type: string; data: string; dimensions: DOMRect | null }): void => {
draggable.value = {
isDragging: true,
type,
data,
dimensions,
activeTarget: null,
};
};
const draggableStopDragging = (): void => {
draggable.value = {
isDragging: false, isDragging: false,
type: '', type: '',
data: '', data: '',
dimensions: null, dimensions: null,
activeTarget: null, activeTarget: null,
}, };
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true', };
isTableHoverOnboarded: useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED).value === 'true',
isAutocompleteOnboarded: useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value === 'true',
highlightDraggables: false,
}),
getters: {
activeNode(): INodeUi | null {
const workflowsStore = useWorkflowsStore();
return workflowsStore.getNodeByName(this.activeNodeName || '');
},
ndvInputData(): INodeExecutionData[] {
const workflowsStore = useWorkflowsStore();
const executionData = workflowsStore.getWorkflowExecution;
const inputNodeName: string | undefined = this.input.nodeName;
const inputRunIndex: number = this.input.run ?? 0;
const inputBranchIndex: number = this.input.branch ?? 0;
if ( const setDraggableTarget = (target: NDVState['draggable']['activeTarget']): void => {
!executionData || draggable.value.activeTarget = target;
!inputNodeName || };
inputRunIndex === undefined ||
inputBranchIndex === undefined
) {
return [];
}
return ( const setMappingTelemetry = (telemetry: { [key: string]: string | number | boolean }): void => {
executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[ mappingTelemetry.value = { ...mappingTelemetry.value, ...telemetry };
inputBranchIndex };
] ?? []
);
},
ndvInputDataWithPinnedData(): INodeExecutionData[] {
const data = this.ndvInputData;
return this.ndvInputNodeName
? (useWorkflowsStore().pinDataByNodeName(this.ndvInputNodeName) ?? data)
: data;
},
hasInputData(): boolean {
return this.ndvInputDataWithPinnedData.length > 0;
},
getPanelDisplayMode() {
return (panel: NodePanelType) => this[panel].displayMode;
},
inputPanelDisplayMode(): IRunDataDisplayMode {
return this.input.displayMode;
},
outputPanelDisplayMode(): IRunDataDisplayMode {
return this.output.displayMode;
},
isDraggableDragging(): boolean {
return this.draggable.isDragging;
},
draggableType(): string {
return this.draggable.type;
},
draggableData(): string {
return this.draggable.data;
},
canDraggableDrop(): boolean {
return this.draggable.activeTarget !== null;
},
outputPanelEditMode(): NDVState['output']['editMode'] {
return this.output.editMode;
},
getMainPanelDimensions() {
return (panelType: string) => {
const defaults = { relativeRight: 1, relativeLeft: 1, relativeWidth: 1 };
return { ...defaults, ...this.mainPanelDimensions[panelType] };
};
},
draggableStickyPos(): XYPosition | null {
return this.draggable.activeTarget?.stickyPosition ?? null;
},
ndvInputNodeName(): string | undefined {
return this.input.nodeName;
},
ndvInputRunIndex(): number | undefined {
return this.input.run;
},
ndvInputBranchIndex(): number | undefined {
return this.input.branch;
},
isNDVDataEmpty() {
return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty;
},
isInputParentOfActiveNode(): boolean {
const inputNodeName = this.ndvInputNodeName;
if (!this.activeNode || !inputNodeName) {
return false;
}
const workflow = useWorkflowsStore().getCurrentWorkflow();
const parentNodes = workflow.getParentNodes(this.activeNode.name, NodeConnectionType.Main, 1);
return parentNodes.includes(inputNodeName);
},
getHoveringItem(): TargetItem | null {
if (this.isInputParentOfActiveNode) {
return this.hoveringItem;
}
return null; const resetMappingTelemetry = (): void => {
}, mappingTelemetry.value = {};
expressionTargetItem(): TargetItem | null { };
if (this.getHoveringItem) {
return this.getHoveringItem;
}
if (this.expressionOutputItemIndex && this.ndvInputNodeName) { const setHoveringItem = (item: TargetItem | null): void => {
return { if (item) setTableHoverOnboarded();
nodeName: this.ndvInputNodeName, hoveringItem.value = item;
runIndex: this.ndvInputRunIndex ?? 0, };
outputIndex: this.ndvInputBranchIndex ?? 0,
itemIndex: this.expressionOutputItemIndex,
};
}
return null; const setNDVBranchIndex = (e: { pane: NodePanelType; branchIndex: number }): void => {
}, if (e.pane === 'input') {
isNDVOpen(): boolean { input.value.branch = e.branchIndex;
return this.activeNodeName !== null; } else {
}, output.value.branch = e.branchIndex;
ndvNodeInputNumber() { }
const returnData: { [nodeName: string]: number[] } = {}; };
const workflow = useWorkflowsStore().getCurrentWorkflow();
const activeNodeConections = (
workflow.connectionsByDestinationNode[this.activeNode?.name || ''] ?? {}
).main;
if (!activeNodeConections || activeNodeConections.length < 2) return returnData; const setNDVPanelDataIsEmpty = (params: {
panel: NodePanelType;
isEmpty: boolean;
}): void => {
if (params.panel === 'input') {
input.value.data.isEmpty = params.isEmpty;
} else {
output.value.data.isEmpty = params.isEmpty;
}
};
for (const [index, connection] of activeNodeConections.entries()) { const setMappingOnboarded = () => {
for (const node of connection) { isMappingOnboarded.value = true;
if (!returnData[node.node]) { localStorageMappingIsOnboarded.value = 'true';
returnData[node.node] = []; };
}
returnData[node.node].push(index + 1);
}
}
return returnData; const setTableHoverOnboarded = () => {
}, isTableHoverOnboarded.value = true;
}, localStorageTableHoverIsOnboarded.value = 'true';
actions: { };
setActiveNodeName(nodeName: string | null): void {
this.activeNodeName = nodeName; const setAutocompleteOnboarded = () => {
}, isAutocompleteOnboarded.value = true;
setInputNodeName(nodeName: string | undefined): void { localStorageAutoCompleteIsOnboarded.value = 'true';
this.input = { };
...this.input,
nodeName, const setHighlightDraggables = (highlight: boolean) => {
}; highlightDraggables.value = highlight;
}, };
setInputRunIndex(run?: number): void {
this.input = { const updateNodeParameterIssues = (issues: INodeIssues): void => {
...this.input, const activeNode = workflowsStore.getNodeByName(activeNodeName.value || '');
run,
}; if (activeNode) {
}, const nodeIndex = workflowsStore.workflow.nodes.findIndex((node) => {
setMainPanelDimensions(params: { return node.name === activeNode.name;
panelType: string; });
dimensions: { relativeLeft?: number; relativeRight?: number; relativeWidth?: number };
}): void { workflowsStore.updateNodeAtIndex(nodeIndex, {
this.mainPanelDimensions = { issues: {
...this.mainPanelDimensions, ...activeNode.issues,
[params.panelType]: { ...issues,
...this.mainPanelDimensions[params.panelType],
...params.dimensions,
}, },
}; });
}, }
setNDVPushRef(): void { };
this.pushRef = `ndv-${uuid()}`;
},
resetNDVPushRef(): void {
this.pushRef = '';
},
setPanelDisplayMode(params: { pane: NodePanelType; mode: IRunDataDisplayMode }): void {
this[params.pane].displayMode = params.mode;
},
setOutputPanelEditModeEnabled(isEnabled: boolean): void {
this.output.editMode.enabled = isEnabled;
},
setOutputPanelEditModeValue(payload: string): void {
this.output.editMode.value = payload;
},
setMappableNDVInputFocus(paramName: string): void {
this.focusedMappableInput = paramName;
},
draggableStartDragging({
type,
data,
dimensions,
}: {
type: string;
data: string;
dimensions: DOMRect | null;
}): void {
this.draggable = {
isDragging: true,
type,
data,
dimensions,
activeTarget: null,
};
},
draggableStopDragging(): void {
this.draggable = {
isDragging: false,
type: '',
data: '',
dimensions: null,
activeTarget: null,
};
},
setDraggableTarget(target: NDVState['draggable']['activeTarget']): void {
this.draggable.activeTarget = target;
},
setMappingTelemetry(telemetry: { [key: string]: string | number | boolean }): void {
this.mappingTelemetry = { ...this.mappingTelemetry, ...telemetry };
},
resetMappingTelemetry(): void {
this.mappingTelemetry = {};
},
setHoveringItem(item: null | NDVState['hoveringItem']): void {
if (item) this.setTableHoverOnboarded();
this.hoveringItem = item;
},
setNDVBranchIndex(e: { pane: 'input' | 'output'; branchIndex: number }): void {
this[e.pane].branch = e.branchIndex;
},
setNDVPanelDataIsEmpty(payload: { panel: 'input' | 'output'; isEmpty: boolean }): void {
this[payload.panel].data.isEmpty = payload.isEmpty;
},
setMappingOnboarded() {
this.isMappingOnboarded = true;
useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value = 'true';
},
setTableHoverOnboarded() {
this.isTableHoverOnboarded = true;
useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED).value = 'true';
},
setAutocompleteOnboarded() {
this.isAutocompleteOnboarded = true;
useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value = 'true';
},
setHighlightDraggables(highlight: boolean) {
this.highlightDraggables = highlight;
},
updateNodeParameterIssues(issues: INodeIssues): void {
const workflowsStore = useWorkflowsStore();
const activeNode = workflowsStore.getNodeByName(this.activeNodeName || '');
if (activeNode) { const setFocusedInputPath = (path: string) => {
const nodeIndex = workflowsStore.workflow.nodes.findIndex((node) => { focusedInputPath.value = path;
return node.name === activeNode.name; };
});
workflowsStore.updateNodeAtIndex(nodeIndex, { return {
issues: { activeNode,
...activeNode.issues, ndvInputData,
...issues, ndvInputNodeName,
}, ndvInputDataWithPinnedData,
}); hasInputData,
} inputPanelDisplayMode,
}, outputPanelDisplayMode,
setFocusedInputPath(path: string) { isDraggableDragging,
this.focusedInputPath = path; draggableType,
}, draggableData,
}, canDraggableDrop,
outputPanelEditMode,
draggableStickyPos,
ndvNodeInputNumber,
ndvInputRunIndex,
ndvInputBranchIndex,
isInputParentOfActiveNode,
getHoveringItem,
expressionTargetItem,
isNDVOpen,
isInputPanelEmpty,
isOutputPanelEmpty,
focusedMappableInput,
isMappingOnboarded,
pushRef,
activeNodeName,
focusedInputPath,
input,
output,
hoveringItem,
highlightDraggables,
mappingTelemetry,
draggable,
isAutocompleteOnboarded,
expressionOutputItemIndex,
isTableHoverOnboarded,
mainPanelDimensions,
setActiveNodeName,
setInputNodeName,
setInputRunIndex,
setMainPanelDimensions,
setNDVPushRef,
resetNDVPushRef,
setPanelDisplayMode,
setOutputPanelEditModeEnabled,
setOutputPanelEditModeValue,
setMappableNDVInputFocus,
draggableStartDragging,
draggableStopDragging,
setDraggableTarget,
setMappingTelemetry,
resetMappingTelemetry,
setHoveringItem,
setNDVBranchIndex,
setNDVPanelDataIsEmpty,
setMappingOnboarded,
setTableHoverOnboarded,
setAutocompleteOnboarded,
setHighlightDraggables,
updateNodeParameterIssues,
setFocusedInputPath,
};
}); });

View file

@ -6,24 +6,17 @@ import type {
ITemplatesCollection, ITemplatesCollection,
ITemplatesCollectionFull, ITemplatesCollectionFull,
ITemplatesQuery, ITemplatesQuery,
ITemplateState,
ITemplatesWorkflow, ITemplatesWorkflow,
ITemplatesWorkflowFull, ITemplatesWorkflowFull,
IWorkflowTemplate, IWorkflowTemplate,
} from '@/Interface'; } from '@/Interface';
import { useSettingsStore } from './settings.store'; import { useSettingsStore } from './settings.store';
import { import * as templatesApi from '@/api/templates';
getCategories,
getCollectionById,
getCollections,
getTemplateById,
getWorkflows,
getWorkflowTemplate,
} from '@/api/templates';
import { getFixedNodesList } from '@/utils/nodeViewUtils'; import { getFixedNodesList } from '@/utils/nodeViewUtils';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useUsersStore } from './users.store'; import { useUsersStore } from './users.store';
import { useWorkflowsStore } from './workflows.store'; import { useWorkflowsStore } from './workflows.store';
import { computed, ref } from 'vue';
const TEMPLATES_PAGE_SIZE = 20; const TEMPLATES_PAGE_SIZE = 20;
@ -33,398 +26,424 @@ function getSearchKey(query: ITemplatesQuery): string {
export type TemplatesStore = ReturnType<typeof useTemplatesStore>; export type TemplatesStore = ReturnType<typeof useTemplatesStore>;
export const useTemplatesStore = defineStore(STORES.TEMPLATES, { export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => {
state: (): ITemplateState => ({ const categories = ref<ITemplatesCategory[]>([]);
categories: [], const collections = ref<Record<string, ITemplatesCollection>>({});
collections: {}, const workflows = ref<Record<string, ITemplatesWorkflow | ITemplatesWorkflowFull>>({});
workflows: {}, const workflowSearches = ref<
collectionSearches: {}, Record<
workflowSearches: {}, string,
currentSessionId: '', {
previousSessionId: '', workflowIds: string[];
currentN8nPath: `${window.location.protocol}//${window.location.host}${window.BASE_PATH}`, totalWorkflows: number;
}), loadingMore?: boolean;
getters: { categories?: ITemplatesCategory[];
allCategories(): ITemplatesCategory[] {
return Object.values(this.categories).sort((a: ITemplatesCategory, b: ITemplatesCategory) =>
a.name > b.name ? 1 : -1,
);
},
getTemplateById() {
return (id: string): null | ITemplatesWorkflow => this.workflows[id];
},
getFullTemplateById() {
return (id: string): null | ITemplatesWorkflowFull => {
const template = this.workflows[id];
return template && 'full' in template && template.full ? template : null;
};
},
getCollectionById() {
return (id: string): null | ITemplatesCollection => this.collections[id];
},
getCategoryById() {
return (id: string): null | ITemplatesCategory => this.categories[id as unknown as number];
},
getSearchedCollections() {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = this.collectionSearches[searchKey];
if (!search) {
return null;
}
return search.collectionIds.map((collectionId: string) => this.collections[collectionId]);
};
},
getSearchedWorkflows() {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = this.workflowSearches[searchKey];
if (!search) {
return null;
}
return search.workflowIds.map((workflowId: string) => this.workflows[workflowId]);
};
},
getSearchedWorkflowsTotal() {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = this.workflowSearches[searchKey];
return search ? search.totalWorkflows : 0;
};
},
isSearchLoadingMore() {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = this.workflowSearches[searchKey];
return Boolean(search && search.loadingMore);
};
},
isSearchFinished() {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = this.workflowSearches[searchKey];
return Boolean(
search && !search.loadingMore && search.totalWorkflows === search.workflowIds.length,
);
};
},
hasCustomTemplatesHost(): boolean {
const settingsStore = useSettingsStore();
return settingsStore.templatesHost !== TEMPLATES_URLS.DEFAULT_API_HOST;
},
/**
* Constructs URLSearchParams object based on the default parameters for the template repository
* and provided additional parameters
*/
websiteTemplateRepositoryParameters(_roleOverride?: string) {
const rootStore = useRootStore();
const userStore = useUsersStore();
const workflowsStore = useWorkflowsStore();
const defaultParameters: Record<string, string> = {
...TEMPLATES_URLS.UTM_QUERY,
utm_instance: this.currentN8nPath,
utm_n8n_version: rootStore.versionCli,
utm_awc: String(workflowsStore.activeWorkflows.length),
};
const userRole: string | null | undefined =
userStore.currentUserCloudInfo?.role ??
(userStore.currentUser?.personalizationAnswers &&
'role' in userStore.currentUser.personalizationAnswers
? userStore.currentUser.personalizationAnswers.role
: undefined);
if (userRole) {
defaultParameters.utm_user_role = userRole;
} }
return (additionalParameters: Record<string, string> = {}) => { >
return new URLSearchParams({ >({});
...defaultParameters, const collectionSearches = ref<
...additionalParameters, Record<
}); string,
}; {
}, collectionIds: string[];
/**
* Construct the URL for the template repository on the website
* @returns {string}
*/
websiteTemplateRepositoryURL(): string {
return `${
TEMPLATES_URLS.BASE_WEBSITE_URL
}?${this.websiteTemplateRepositoryParameters().toString()}`;
},
/**
* Construct the URL for the template category page on the website for a given category id
*/
getWebsiteCategoryURL() {
return (id?: string, roleOverride?: string) => {
const payload: Record<string, string> = {};
if (id) {
payload.categories = id;
}
if (roleOverride) {
payload.utm_user_role = roleOverride;
}
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?${this.websiteTemplateRepositoryParameters(payload).toString()}`;
};
},
},
actions: {
addCategories(categories: ITemplatesCategory[]): void {
categories.forEach((category: ITemplatesCategory) => {
this.categories = {
...this.categories,
[category.id]: category,
};
});
},
addCollections(collections: Array<ITemplatesCollection | ITemplatesCollectionFull>): void {
collections.forEach((collection) => {
const workflows = (collection.workflows || []).map((workflow) => ({ id: workflow.id }));
const cachedCollection = this.collections[collection.id] || {};
this.collections = {
...this.collections,
[collection.id]: {
...cachedCollection,
...collection,
workflows,
},
};
});
},
addWorkflows(workflows: Array<ITemplatesWorkflow | ITemplatesWorkflowFull>): void {
workflows.forEach((workflow: ITemplatesWorkflow) => {
const cachedWorkflow = this.workflows[workflow.id] || {};
this.workflows = {
...this.workflows,
[workflow.id]: {
...cachedWorkflow,
...workflow,
},
};
});
},
addCollectionSearch(data: {
collections: ITemplatesCollection[];
query: ITemplatesQuery;
}): void {
const collectionIds = data.collections.map((collection) => String(collection.id));
const searchKey = getSearchKey(data.query);
this.collectionSearches = {
...this.collectionSearches,
[searchKey]: {
collectionIds,
},
};
},
addWorkflowsSearch(data: {
totalWorkflows: number;
workflows: ITemplatesWorkflow[];
query: ITemplatesQuery;
}): void {
const workflowIds = data.workflows.map((workflow) => workflow.id);
const searchKey = getSearchKey(data.query);
const cachedResults = this.workflowSearches[searchKey];
if (!cachedResults) {
this.workflowSearches = {
...this.workflowSearches,
[searchKey]: {
workflowIds: workflowIds as unknown as string[],
totalWorkflows: data.totalWorkflows,
categories: this.categories,
},
};
return;
} }
>
>({});
const currentSessionId = ref<string>('');
const previousSessionId = ref<string>('');
const currentN8nPath = ref<string>(
`${window.location.protocol}//${window.location.host}${window.BASE_PATH}`,
);
this.workflowSearches = { const settingsStore = useSettingsStore();
...this.workflowSearches, const rootStore = useRootStore();
[searchKey]: { const userStore = useUsersStore();
workflowIds: [...cachedResults.workflowIds, ...workflowIds] as string[], const workflowsStore = useWorkflowsStore();
totalWorkflows: data.totalWorkflows,
categories: this.categories, const allCategories = computed(() => {
}, return categories.value.sort((a: ITemplatesCategory, b: ITemplatesCategory) =>
}; a.name > b.name ? 1 : -1,
}, );
setWorkflowSearchLoading(query: ITemplatesQuery): void { });
const getTemplatesById = computed(() => {
return (id: string): null | ITemplatesWorkflow => workflows.value[id];
});
const getFullTemplateById = computed(() => {
return (id: string): null | ITemplatesWorkflowFull => {
const template = workflows.value[id];
return template && 'full' in template && template.full ? template : null;
};
});
const getCollectionById = computed(() => collections.value);
const getCategoryById = computed(() => {
return (id: string): null | ITemplatesCategory => categories.value[id as unknown as number];
});
const getSearchedCollections = computed(() => {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query); const searchKey = getSearchKey(query);
const cachedResults = this.workflowSearches[searchKey]; const search = collectionSearches.value[searchKey];
if (!cachedResults) { if (!search) {
return; return null;
} }
this.workflowSearches[searchKey] = { return search.collectionIds.map((collectionId: string) => collections.value[collectionId]);
...this.workflowSearches[searchKey], };
loadingMore: true, });
};
}, const getSearchedWorkflows = computed(() => {
setWorkflowSearchLoaded(query: ITemplatesQuery): void { return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query); const searchKey = getSearchKey(query);
const cachedResults = this.workflowSearches[searchKey]; const search = workflowSearches.value[searchKey];
if (!cachedResults) { if (!search) {
return; return null;
} }
this.workflowSearches[searchKey] = { return search.workflowIds.map((workflowId: string) => workflows.value[workflowId]);
...this.workflowSearches[searchKey], };
loadingMore: false, });
const getSearchedWorkflowsTotal = computed(() => {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = workflowSearches.value[searchKey];
return search ? search.totalWorkflows : 0;
};
});
const isSearchLoadingMore = computed(() => {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = workflowSearches.value[searchKey];
return Boolean(search && search.loadingMore);
};
});
const isSearchFinished = computed(() => {
return (query: ITemplatesQuery) => {
const searchKey = getSearchKey(query);
const search = workflowSearches.value[searchKey];
return Boolean(
search && !search.loadingMore && search.totalWorkflows === search.workflowIds.length,
);
};
});
const hasCustomTemplatesHost = computed(() => {
return settingsStore.templatesHost !== TEMPLATES_URLS.DEFAULT_API_HOST;
});
const websiteTemplateRepositoryParameters = computed(() => {
const defaultParameters: Record<string, string> = {
...TEMPLATES_URLS.UTM_QUERY,
utm_instance: currentN8nPath.value,
utm_n8n_version: rootStore.versionCli,
utm_awc: String(workflowsStore.activeWorkflows.length),
};
const userRole: string | null | undefined =
userStore.currentUserCloudInfo?.role ??
(userStore.currentUser?.personalizationAnswers &&
'role' in userStore.currentUser.personalizationAnswers
? userStore.currentUser.personalizationAnswers.role
: undefined);
if (userRole) {
defaultParameters.utm_user_role = userRole;
}
return new URLSearchParams({
...defaultParameters,
});
});
const websiteTemplateRepositoryURL = computed(
() =>
`${TEMPLATES_URLS.BASE_WEBSITE_URL}?${websiteTemplateRepositoryParameters.value.toString()}`,
);
const addCategories = (_categories: ITemplatesCategory[]): void => {
categories.value = _categories;
};
const addCollections = (
_collections: Array<ITemplatesCollection | ITemplatesCollectionFull>,
): void => {
_collections.forEach((collection) => {
const workflows = (collection.workflows || []).map((workflow) => ({ id: workflow.id }));
const cachedCollection = collections.value[collection.id] || {};
collections.value[collection.id] = {
...cachedCollection,
...collection,
workflows,
}; };
}, });
resetSessionId(): void { };
this.previousSessionId = this.currentSessionId;
this.currentSessionId = ''; const addWorkflows = (_workflows: Array<ITemplatesWorkflow | ITemplatesWorkflowFull>): void => {
}, _workflows.forEach((workflow) => {
setSessionId(): void { const cachedWorkflow = workflows.value[workflow.id] || {};
if (!this.currentSessionId) { workflows.value[workflow.id.toString()] = { ...cachedWorkflow, ...workflow };
this.currentSessionId = `templates-${Date.now()}`; });
} };
},
async fetchTemplateById(templateId: string): Promise<ITemplatesWorkflowFull> { const addCollectionsSearch = (data: {
const settingsStore = useSettingsStore(); _collections: ITemplatesCollection[];
const rootStore = useRootStore(); query: ITemplatesQuery;
const apiEndpoint: string = settingsStore.templatesHost; }) => {
const versionCli: string = rootStore.versionCli; const collectionIds = data._collections.map((collection) => String(collection.id));
const response = await getTemplateById(apiEndpoint, templateId, { const searchKey = getSearchKey(data.query);
'n8n-version': versionCli,
collectionSearches.value[searchKey] = {
collectionIds,
};
};
const addWorkflowsSearch = (data: {
totalWorkflows: number;
workflows: ITemplatesWorkflow[];
query: ITemplatesQuery;
}) => {
const workflowIds = data.workflows.map((workflow) => workflow.id);
const searchKey = getSearchKey(data.query);
const cachedResults = workflowSearches.value[searchKey];
if (!cachedResults) {
workflowSearches.value[searchKey] = {
workflowIds: workflowIds as unknown as string[],
totalWorkflows: data.totalWorkflows,
categories: categories.value,
};
return;
}
workflowSearches.value[searchKey] = {
workflowIds: [...cachedResults.workflowIds, ...workflowIds] as string[],
totalWorkflows: data.totalWorkflows,
categories: categories.value,
};
};
const setWorkflowSearchLoading = (query: ITemplatesQuery): void => {
const searchKey = getSearchKey(query);
const cachedResults = workflowSearches.value[searchKey];
if (!cachedResults) {
return;
}
workflowSearches.value[searchKey] = {
...workflowSearches.value[searchKey],
loadingMore: true,
};
};
const setWorkflowSearchLoaded = (query: ITemplatesQuery): void => {
const searchKey = getSearchKey(query);
const cachedResults = workflowSearches.value[searchKey];
if (!cachedResults) {
return;
}
workflowSearches.value[searchKey] = {
...workflowSearches.value[searchKey],
loadingMore: false,
};
};
const resetSessionId = (): void => {
previousSessionId.value = currentSessionId.value;
currentSessionId.value = '';
};
const setSessionId = (): void => {
if (!currentSessionId.value) {
currentSessionId.value = `templates-${Date.now()}`;
}
};
const fetchTemplateById = async (templateId: string): Promise<ITemplatesWorkflowFull> => {
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
const response = await templatesApi.getTemplateById(apiEndpoint, templateId, {
'n8n-version': versionCli,
});
const template: ITemplatesWorkflowFull = {
...response.workflow,
full: true,
};
addWorkflows([template]);
return template;
};
const fetchCollectionById = async (
collectionId: string,
): Promise<ITemplatesCollection | null> => {
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
const response = await templatesApi.getCollectionById(apiEndpoint, collectionId, {
'n8n-version': versionCli,
});
const collection: ITemplatesCollectionFull = {
...response.collection,
full: true,
};
addCollections([collection]);
addWorkflows(response.collection.workflows);
return getCollectionById.value[collectionId];
};
const getCategories = async (): Promise<ITemplatesCategory[]> => {
const cachedCategories = allCategories.value;
if (cachedCategories.length) {
return cachedCategories;
}
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
const response = await templatesApi.getCategories(apiEndpoint, {
'n8n-version': versionCli,
});
const categories = response.categories;
addCategories(categories);
return categories;
};
const getCollections = async (query: ITemplatesQuery): Promise<ITemplatesCollection[]> => {
const cachedResults = getSearchedCollections.value(query);
if (cachedResults) {
return cachedResults;
}
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
const response = await templatesApi.getCollections(apiEndpoint, query, {
'n8n-version': versionCli,
});
const collections = response.collections;
addCollections(collections);
addCollectionsSearch({ query, _collections: collections });
collections.forEach((collection) => addWorkflows(collection.workflows as ITemplatesWorkflow[]));
return collections;
};
const getWorkflows = async (query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> => {
const cachedResults = getSearchedWorkflows.value(query);
if (cachedResults) {
categories.value = workflowSearches.value[getSearchKey(query)].categories ?? [];
return cachedResults;
}
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
const payload = await templatesApi.getWorkflows(
apiEndpoint,
{ ...query, page: 1, limit: TEMPLATES_PAGE_SIZE },
{ 'n8n-version': versionCli },
);
addWorkflows(payload.workflows);
addWorkflowsSearch({ ...payload, query });
return getSearchedWorkflows.value(query) || [];
};
const getMoreWorkflows = async (query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> => {
if (isSearchLoadingMore.value(query) && !isSearchFinished.value(query)) {
return [];
}
const cachedResults = getSearchedWorkflows.value(query) || [];
const apiEndpoint: string = settingsStore.templatesHost;
setWorkflowSearchLoading(query);
try {
const payload = await templatesApi.getWorkflows(apiEndpoint, {
...query,
page: cachedResults.length / TEMPLATES_PAGE_SIZE + 1,
limit: TEMPLATES_PAGE_SIZE,
}); });
const template: ITemplatesWorkflowFull = { setWorkflowSearchLoaded(query);
...response.workflow, addWorkflows(payload.workflows);
full: true, addWorkflowsSearch({ ...payload, query });
};
this.addWorkflows([template]);
return template; return getSearchedWorkflows.value(query) || [];
}, } catch (e) {
async fetchCollectionById(collectionId: string): Promise<ITemplatesCollection | null> { setWorkflowSearchLoaded(query);
const settingsStore = useSettingsStore(); throw e;
const rootStore = useRootStore(); }
const apiEndpoint: string = settingsStore.templatesHost; };
const versionCli: string = rootStore.versionCli;
const response = await getCollectionById(apiEndpoint, collectionId, { const getWorkflowTemplate = async (templateId: string): Promise<IWorkflowTemplate> => {
'n8n-version': versionCli, const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
return await templatesApi.getWorkflowTemplate(apiEndpoint, templateId, {
'n8n-version': versionCli,
});
};
const getFixedWorkflowTemplate = async (
templateId: string,
): Promise<IWorkflowTemplate | undefined> => {
const template = await getWorkflowTemplate(templateId);
if (template?.workflow?.nodes) {
template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[];
template.workflow.nodes?.forEach((node) => {
if (node.credentials) {
delete node.credentials;
}
}); });
const collection: ITemplatesCollectionFull = { }
...response.collection,
full: true,
};
this.addCollections([collection]); return template;
this.addWorkflows(response.collection.workflows); };
return this.getCollectionById(collectionId);
},
async getCategories(): Promise<ITemplatesCategory[]> {
const cachedCategories = this.allCategories;
if (cachedCategories.length) {
return cachedCategories;
}
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli });
const categories = response.categories;
this.addCategories(categories); return {
return categories; categories,
}, collections,
async getCollections(query: ITemplatesQuery): Promise<ITemplatesCollection[]> { workflows,
const cachedResults = this.getSearchedCollections(query); workflowSearches,
if (cachedResults) { collectionSearches,
return cachedResults; currentSessionId,
} previousSessionId,
currentN8nPath,
const settingsStore = useSettingsStore(); allCategories,
const rootStore = useRootStore(); getTemplatesById,
const apiEndpoint: string = settingsStore.templatesHost; getFullTemplateById,
const versionCli: string = rootStore.versionCli; getCollectionById,
const response = await getCollections(apiEndpoint, query, { 'n8n-version': versionCli }); getCategoryById,
const collections = response.collections; getSearchedCollections,
getSearchedWorkflows,
this.addCollections(collections); getSearchedWorkflowsTotal,
this.addCollectionSearch({ query, collections }); isSearchLoadingMore,
collections.forEach((collection) => isSearchFinished,
this.addWorkflows(collection.workflows as ITemplatesWorkflowFull[]), hasCustomTemplatesHost,
); websiteTemplateRepositoryURL,
websiteTemplateRepositoryParameters,
return collections; addCategories,
}, addCollections,
async getWorkflows(query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> { addWorkflows,
const cachedResults = this.getSearchedWorkflows(query); addCollectionsSearch,
if (cachedResults) { addWorkflowsSearch,
this.categories = this.workflowSearches[getSearchKey(query)].categories ?? []; setWorkflowSearchLoading,
return cachedResults; setWorkflowSearchLoaded,
} resetSessionId,
setSessionId,
const settingsStore = useSettingsStore(); fetchTemplateById,
const rootStore = useRootStore(); fetchCollectionById,
const apiEndpoint: string = settingsStore.templatesHost; getCategories,
const versionCli: string = rootStore.versionCli; getCollections,
getWorkflows,
const payload = await getWorkflows( getMoreWorkflows,
apiEndpoint, getWorkflowTemplate,
{ ...query, page: 1, limit: TEMPLATES_PAGE_SIZE }, getFixedWorkflowTemplate,
{ 'n8n-version': versionCli }, };
);
this.addWorkflows(payload.workflows);
this.addWorkflowsSearch({ ...payload, query });
return this.getSearchedWorkflows(query) || [];
},
async getMoreWorkflows(query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> {
if (this.isSearchLoadingMore(query) && !this.isSearchFinished(query)) {
return [];
}
const cachedResults = this.getSearchedWorkflows(query) || [];
const settingsStore = useSettingsStore();
const apiEndpoint: string = settingsStore.templatesHost;
this.setWorkflowSearchLoading(query);
try {
const payload = await getWorkflows(apiEndpoint, {
...query,
page: cachedResults.length / TEMPLATES_PAGE_SIZE + 1,
limit: TEMPLATES_PAGE_SIZE,
});
this.setWorkflowSearchLoaded(query);
this.addWorkflows(payload.workflows);
this.addWorkflowsSearch({ ...payload, query });
return this.getSearchedWorkflows(query) || [];
} catch (e) {
this.setWorkflowSearchLoaded(query);
throw e;
}
},
async getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = rootStore.versionCli;
return await getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli });
},
async getFixedWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate | undefined> {
const template = await this.getWorkflowTemplate(templateId);
if (template?.workflow?.nodes) {
template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[];
template.workflow.nodes?.forEach((node) => {
if (node.credentials) {
delete node.credentials;
}
});
}
return template;
},
},
}); });

View file

@ -35,14 +35,14 @@ const collectionId = computed(() => {
return Array.isArray(id) ? id[0] : id; return Array.isArray(id) ? id[0] : id;
}); });
const collection = computed(() => templatesStore.getCollectionById(collectionId.value)); const collection = computed(() => templatesStore.getCollectionById[collectionId.value]);
const collectionWorkflows = computed(() => { const collectionWorkflows = computed(() => {
if (!collection.value || loading.value) { if (!collection.value || loading.value) {
return []; return [];
} }
return collection.value.workflows return collection.value.workflows
.map(({ id }) => templatesStore.getTemplateById(id.toString())) .map(({ id }) => templatesStore.getTemplatesById(id.toString()))
.filter((workflow): workflow is ITemplatesWorkflow => !!workflow); .filter((workflow): workflow is ITemplatesWorkflow => !!workflow);
}); });

View file

@ -124,14 +124,16 @@ export class Supabase implements INodeType {
const items = this.getInputData(); const items = this.getInputData();
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
const length = items.length; const length = items.length;
const qs: IDataObject = {}; let qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0); const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
if (resource === 'row') { if (resource === 'row') {
const tableId = this.getNodeParameter('tableId', 0) as string;
if (operation === 'create') { if (operation === 'create') {
const records: IDataObject[] = []; const records: IDataObject[] = [];
const tableId = this.getNodeParameter('tableId', 0) as string;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const record: IDataObject = {}; const record: IDataObject = {};
const dataToSend = this.getNodeParameter('dataToSend', 0) as const dataToSend = this.getNodeParameter('dataToSend', 0) as
@ -185,7 +187,6 @@ export class Supabase implements INodeType {
} }
if (operation === 'delete') { if (operation === 'delete') {
const tableId = this.getNodeParameter('tableId', 0) as string;
const filterType = this.getNodeParameter('filterType', 0) as string; const filterType = this.getNodeParameter('filterType', 0) as string;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
let endpoint = `/${tableId}`; let endpoint = `/${tableId}`;
@ -241,7 +242,6 @@ export class Supabase implements INodeType {
} }
if (operation === 'get') { if (operation === 'get') {
const tableId = this.getNodeParameter('tableId', 0) as string;
const endpoint = `/${tableId}`; const endpoint = `/${tableId}`;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
@ -281,11 +281,13 @@ export class Supabase implements INodeType {
} }
if (operation === 'getAll') { if (operation === 'getAll') {
const tableId = this.getNodeParameter('tableId', 0) as string;
const returnAll = this.getNodeParameter('returnAll', 0); const returnAll = this.getNodeParameter('returnAll', 0);
const filterType = this.getNodeParameter('filterType', 0) as string; const filterType = this.getNodeParameter('filterType', 0) as string;
let endpoint = `/${tableId}`; let endpoint = `/${tableId}`;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
qs = {}; // reset qs
if (filterType === 'manual') { if (filterType === 'manual') {
const matchType = this.getNodeParameter('matchType', 0) as string; const matchType = this.getNodeParameter('matchType', 0) as string;
const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[]; const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[];
@ -342,7 +344,6 @@ export class Supabase implements INodeType {
} }
if (operation === 'update') { if (operation === 'update') {
const tableId = this.getNodeParameter('tableId', 0) as string;
const filterType = this.getNodeParameter('filterType', 0) as string; const filterType = this.getNodeParameter('filterType', 0) as string;
let endpoint = `/${tableId}`; let endpoint = `/${tableId}`;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {

View file

@ -408,7 +408,8 @@ export function convertNodeToAiTool<
}; };
const noticeProp: INodeProperties = { const noticeProp: INodeProperties = {
displayName: 'Use the expression {{ $fromAI() }} for any data to be filled by the model', displayName:
"Use the expression {{ $fromAI('placeholder_name') }} for any data to be filled by the model",
name: 'notice', name: 'notice',
type: 'notice', type: 'notice',
default: '', default: '',

View file

@ -946,7 +946,7 @@ export class WorkflowDataProxy {
defaultValue?: unknown, defaultValue?: unknown,
) => { ) => {
if (!name || name === '') { if (!name || name === '') {
throw new ExpressionError('Please provide a key', { throw new ExpressionError("Add a key, e.g. $fromAI('placeholder_name')", {
runIndex: that.runIndex, runIndex: that.runIndex,
itemIndex: that.itemIndex, itemIndex: that.itemIndex,
}); });