mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge remote-tracking branch 'origin/master' into ADO-2728/feature-change-auto-add-of-chattrigger
This commit is contained in:
commit
8e85ff55a0
|
@ -11,3 +11,7 @@
|
||||||
# refactor: Run lintfix (no-changelog) (#7537)
|
# refactor: Run lintfix (no-changelog) (#7537)
|
||||||
|
|
||||||
62c096710fab2f7e886518abdbded34b55e93f62
|
62c096710fab2f7e886518abdbded34b55e93f62
|
||||||
|
|
||||||
|
# refactor: Move test files alongside tested files (#11504)
|
||||||
|
|
||||||
|
7e58fc4fec468aca0b45d5bfe6150e1af632acbc
|
||||||
|
|
42
CHANGELOG.md
42
CHANGELOG.md
|
@ -1,3 +1,45 @@
|
||||||
|
# [1.66.0](https://github.com/n8n-io/n8n/compare/n8n@1.65.0...n8n@1.66.0) (2024-10-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Asana Node:** Fix issue with pagination ([#11415](https://github.com/n8n-io/n8n/issues/11415)) ([04c075a](https://github.com/n8n-io/n8n/commit/04c075a46bcc7b1964397f0244b0fde99476212d))
|
||||||
|
* **core:** Add 'user_id' to `license-community-plus-registered` telemetry event ([#11430](https://github.com/n8n-io/n8n/issues/11430)) ([7a8dafe](https://github.com/n8n-io/n8n/commit/7a8dafe9902fbc0d5001c50579c34959b95211ab))
|
||||||
|
* **core:** Add safeguard for command publishing ([#11337](https://github.com/n8n-io/n8n/issues/11337)) ([656439e](https://github.com/n8n-io/n8n/commit/656439e87138f9f96dea5a683cfdac3f661ffefb))
|
||||||
|
* **core:** Ensure `LoggerProxy` is not scoped ([#11379](https://github.com/n8n-io/n8n/issues/11379)) ([f4ea943](https://github.com/n8n-io/n8n/commit/f4ea943c9cb2321e41705de6c5c27535a0f5eae0))
|
||||||
|
* **core:** Ensure `remove-triggers-and-pollers` command is not debounced ([#11486](https://github.com/n8n-io/n8n/issues/11486)) ([529d4fc](https://github.com/n8n-io/n8n/commit/529d4fc3ef5206bd1b02d27634342cc50b45997e))
|
||||||
|
* **core:** Ensure job processor does not reprocess amended executions ([#11438](https://github.com/n8n-io/n8n/issues/11438)) ([c152a3a](https://github.com/n8n-io/n8n/commit/c152a3ac56f140a39eea4771a94f5a3082118df7))
|
||||||
|
* **core:** Fix Message Event Bus Metrics not counting up for labeled metrics ([#11396](https://github.com/n8n-io/n8n/issues/11396)) ([7fc3b25](https://github.com/n8n-io/n8n/commit/7fc3b25d21c6c4f1802f34b1ae065a65cac3001b))
|
||||||
|
* **core:** Fix resolving of $fromAI expression via `evaluateExpression` ([#11397](https://github.com/n8n-io/n8n/issues/11397)) ([2e64464](https://github.com/n8n-io/n8n/commit/2e6446454defbd3e5a47b66e6fd46d4f1b9fbd0f))
|
||||||
|
* **core:** Make execution and its data creation atomic ([#11392](https://github.com/n8n-io/n8n/issues/11392)) ([ed30d43](https://github.com/n8n-io/n8n/commit/ed30d43236bf3c6b657022636a02a41be01aa152))
|
||||||
|
* **core:** On unhandled rejections, extract the original exception correctly ([#11389](https://github.com/n8n-io/n8n/issues/11389)) ([8608bae](https://github.com/n8n-io/n8n/commit/8608baeb7ec302daddc8adca6e39778dcf7b2eda))
|
||||||
|
* **editor:** Fix TypeError: Cannot read properties of undefined (reading '0') ([#11399](https://github.com/n8n-io/n8n/issues/11399)) ([ae37c52](https://github.com/n8n-io/n8n/commit/ae37c520a91c75e353e818944b36a3619c0d8b4a))
|
||||||
|
* **editor:** Add Retry button for AI Assistant errors ([#11345](https://github.com/n8n-io/n8n/issues/11345)) ([7699240](https://github.com/n8n-io/n8n/commit/7699240073122cdef31cf109fd37fa66961f588a))
|
||||||
|
* **editor:** Change tooltip for workflow with execute workflow trigger ([#11374](https://github.com/n8n-io/n8n/issues/11374)) ([dcd6038](https://github.com/n8n-io/n8n/commit/dcd6038c3085135803cdaa546a239359a6d449eb))
|
||||||
|
* **editor:** Ensure toasts show above modal overlays ([#11410](https://github.com/n8n-io/n8n/issues/11410)) ([351134f](https://github.com/n8n-io/n8n/commit/351134f786af933f5f310bf8d9897269387635a0))
|
||||||
|
* **editor:** Fit view consistently after nodes are initialized on new canvas ([#11457](https://github.com/n8n-io/n8n/issues/11457)) ([497d637](https://github.com/n8n-io/n8n/commit/497d637fc5308b9c4a06bc764152fde1f1a9c130))
|
||||||
|
* **editor:** Fix adding connections when initializing workspace in templates view on new canvas ([#11451](https://github.com/n8n-io/n8n/issues/11451)) ([ea47b02](https://github.com/n8n-io/n8n/commit/ea47b025fb16c967d4fc73dcacc6e260d2aecd61))
|
||||||
|
* **editor:** Fix rendering of AI logs ([#11450](https://github.com/n8n-io/n8n/issues/11450)) ([73b0a80](https://github.com/n8n-io/n8n/commit/73b0a80ac92b4f4b5a300d0ec1c833b4395a222a))
|
||||||
|
* **editor:** Hide data mapping tooltip in credential edit modal ([#11356](https://github.com/n8n-io/n8n/issues/11356)) ([ff14dcb](https://github.com/n8n-io/n8n/commit/ff14dcb3a1ddaea4eca7c1ecd2e92c0abb0c413c))
|
||||||
|
* **editor:** Prevent running workflow that has issues if listening to webhook ([#11402](https://github.com/n8n-io/n8n/issues/11402)) ([8b0a48f](https://github.com/n8n-io/n8n/commit/8b0a48f53010378e497e4cc362fda75a958cf363))
|
||||||
|
* **editor:** Run external hooks after settings have been initialized ([#11423](https://github.com/n8n-io/n8n/issues/11423)) ([0ab24c8](https://github.com/n8n-io/n8n/commit/0ab24c814abd1787268750ba808993ab2735ac52))
|
||||||
|
* **editor:** Support middle click to scroll when using a mouse on new canvas ([#11384](https://github.com/n8n-io/n8n/issues/11384)) ([46f3b4a](https://github.com/n8n-io/n8n/commit/46f3b4a258f89f02e0d2bd1eef25a22e3a721167))
|
||||||
|
* **HTTP Request Tool Node:** Fix HTML response optimization issue ([#11439](https://github.com/n8n-io/n8n/issues/11439)) ([cf37e94](https://github.com/n8n-io/n8n/commit/cf37e94dd875e9f6ab1f189146fb34e7296af93c))
|
||||||
|
* **n8n Form Node:** Form Trigger does not wait in multi-form mode ([#11404](https://github.com/n8n-io/n8n/issues/11404)) ([151f4dd](https://github.com/n8n-io/n8n/commit/151f4dd7b8f800af424f8ae64cb8238975fb3cb8))
|
||||||
|
* Update required node js version in CONTRIBUTING.md ([#11437](https://github.com/n8n-io/n8n/issues/11437)) ([4f511aa](https://github.com/n8n-io/n8n/commit/4f511aab68651caa8fe47f70cd7cdb88bb06a3e2))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Anthropic Chat Model Node:** Add model claude-3-5-sonnet-20241022 ([#11465](https://github.com/n8n-io/n8n/issues/11465)) ([f6c8890](https://github.com/n8n-io/n8n/commit/f6c8890a8069de221b9b96e735418ecc9624cf7b))
|
||||||
|
* **core:** Handle nodes with multiple inputs and connections during partial executions ([#11376](https://github.com/n8n-io/n8n/issues/11376)) ([cb7c4d2](https://github.com/n8n-io/n8n/commit/cb7c4d29a6f042b590822e5b9c67fff0a8f0863d))
|
||||||
|
* **editor:** Add descriptive header to projects /workflow ([#11203](https://github.com/n8n-io/n8n/issues/11203)) ([5d19e8f](https://github.com/n8n-io/n8n/commit/5d19e8f2b45dc1abc5a8253f9e3a0fdacb1ebd91))
|
||||||
|
* **editor:** Improve placeholder for vector store tool ([#11483](https://github.com/n8n-io/n8n/issues/11483)) ([629e092](https://github.com/n8n-io/n8n/commit/629e09240785bc648ff6575f97910fbb4e77cdab))
|
||||||
|
* **editor:** Remove edge execution animation on new canvas ([#11446](https://github.com/n8n-io/n8n/issues/11446)) ([a701d87](https://github.com/n8n-io/n8n/commit/a701d87f5ba94ffc811e424b60e188b26ac6c1c5))
|
||||||
|
* **editor:** Update ownership pills ([#11155](https://github.com/n8n-io/n8n/issues/11155)) ([8147038](https://github.com/n8n-io/n8n/commit/8147038cf87dca657602e617e49698065bf1a63f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
|
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,14 @@ export function getOutputPanelDataContainer() {
|
||||||
return getOutputPanel().getByTestId('ndv-data-container');
|
return getOutputPanel().getByTestId('ndv-data-container');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOutputTableRows() {
|
||||||
|
return getOutputPanelDataContainer().find('table tr');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputTableRow(row: number) {
|
||||||
|
return getOutputTableRows().eq(row);
|
||||||
|
}
|
||||||
|
|
||||||
export function getOutputPanelTable() {
|
export function getOutputPanelTable() {
|
||||||
return getOutputPanelDataContainer().get('table');
|
return getOutputPanelDataContainer().get('table');
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,13 @@ export function getNodeCreatorPlusButton() {
|
||||||
return cy.getByTestId('node-creator-plus-button');
|
return cy.getByTestId('node-creator-plus-button');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCanvasNodes() {
|
||||||
|
return cy.ifCanvasVersion(
|
||||||
|
() => cy.getByTestId('canvas-node'),
|
||||||
|
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions
|
* Actions
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
|
||||||
routine: 'InitPostgres',
|
routine: 'InitPostgres',
|
||||||
} as unknown as Error,
|
} as unknown as Error,
|
||||||
} as ExecutionError,
|
} as ExecutionError,
|
||||||
|
metadata: {
|
||||||
|
subRun: [
|
||||||
|
{
|
||||||
|
node: 'Postgres Chat Memory',
|
||||||
|
runIndex: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||||
executionStatus: 'error',
|
executionStatus: 'error',
|
||||||
|
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
|
||||||
description: 'Internal error',
|
description: 'Internal error',
|
||||||
message: 'Internal error',
|
message: 'Internal error',
|
||||||
} as unknown as ExecutionError,
|
} as unknown as ExecutionError,
|
||||||
metadata: {
|
|
||||||
subRun: [
|
|
||||||
{
|
|
||||||
node: 'Postgres Chat Memory',
|
|
||||||
runIndex: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,6 +278,9 @@ describe('Langchain Integration', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||||
|
},
|
||||||
inputOverride: {
|
inputOverride: {
|
||||||
ai_languageModel: [
|
ai_languageModel: [
|
||||||
[
|
[
|
||||||
|
@ -316,9 +319,6 @@ describe('Langchain Integration', () => {
|
||||||
jsonData: {
|
jsonData: {
|
||||||
main: { output: 'Hi there! How can I assist you today?' },
|
main: { output: 'Hi there! How can I assist you today?' },
|
||||||
},
|
},
|
||||||
metadata: {
|
|
||||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
lastNodeExecuted: AGENT_NODE_NAME,
|
lastNodeExecuted: AGENT_NODE_NAME,
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
import workflow from '../fixtures/Manual_wait_set.json';
|
import { getOutputTableRow } from '../composables/ndv';
|
||||||
|
import { getCanvasNodes, openNode } from '../composables/workflow';
|
||||||
|
import SIMPLE_WORKFLOW from '../fixtures/Manual_wait_set.json';
|
||||||
|
import WORKFLOW_WITH_PINNED from '../fixtures/Webhook_set_pinned.json';
|
||||||
import { importWorkflow, visitDemoPage } from '../pages/demo';
|
import { importWorkflow, visitDemoPage } from '../pages/demo';
|
||||||
import { errorToast } from '../pages/notifications';
|
import { errorToast } from '../pages/notifications';
|
||||||
import { WorkflowPage } from '../pages/workflow';
|
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
|
||||||
|
|
||||||
describe('Demo', () => {
|
describe('Demo', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.overrideSettings({ previewMode: true });
|
cy.overrideSettings({ previewMode: true });
|
||||||
cy.signout();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can import template', () => {
|
it('can import template', () => {
|
||||||
visitDemoPage();
|
visitDemoPage();
|
||||||
errorToast().should('not.exist');
|
errorToast().should('not.exist');
|
||||||
importWorkflow(workflow);
|
importWorkflow(SIMPLE_WORKFLOW);
|
||||||
workflowPage.getters.canvasNodes().should('have.length', 3);
|
getCanvasNodes().should('have.length', 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can import workflow with pin data', () => {
|
||||||
|
visitDemoPage();
|
||||||
|
importWorkflow(WORKFLOW_WITH_PINNED);
|
||||||
|
getCanvasNodes().should('have.length', 2);
|
||||||
|
openNode('Webhook');
|
||||||
|
getOutputTableRow(0).should('include.text', 'headers');
|
||||||
|
getOutputTableRow(1).should('include.text', 'dragons');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can override theme to dark', () => {
|
it('can override theme to dark', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-monorepo",
|
"name": "n8n-monorepo",
|
||||||
"version": "1.65.0",
|
"version": "1.66.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.15",
|
"node": ">=20.15",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/api-types",
|
"name": "@n8n/api-types",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/config",
|
"name": "@n8n/config",
|
||||||
"version": "1.15.0",
|
"version": "1.16.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -63,7 +63,7 @@ export class ToolVectorStore implements INodeType {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'e.g. state_of_union_address',
|
placeholder: 'e.g. company_knowledge_base',
|
||||||
validateType: 'string-alphanumeric',
|
validateType: 'string-alphanumeric',
|
||||||
description: 'Name of the vector store',
|
description: 'Name of the vector store',
|
||||||
},
|
},
|
||||||
|
@ -72,7 +72,7 @@ export class ToolVectorStore implements INodeType {
|
||||||
name: 'description',
|
name: 'description',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'The most recent state of the Union address',
|
placeholder: 'Retrieves data about [insert information about your data here]...',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
rows: 3,
|
rows: 3,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/n8n-nodes-langchain",
|
"name": "@n8n/n8n-nodes-langchain",
|
||||||
"version": "1.65.0",
|
"version": "1.66.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/permissions",
|
"name": "@n8n/permissions",
|
||||||
"version": "0.15.0",
|
"version": "0.16.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/task-runner",
|
"name": "@n8n/task-runner",
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"start": "node dist/start.js",
|
"start": "node dist/start.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "1.65.0",
|
"version": "1.66.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -65,6 +65,42 @@ describe('Publisher', () => {
|
||||||
JSON.stringify({ ...msg, senderId: hostId, selfSend: false, debounce: true }),
|
JSON.stringify({ ...msg, senderId: hostId, selfSend: false, debounce: true }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not debounce `add-webhooks-triggers-and-pollers`', async () => {
|
||||||
|
const publisher = new Publisher(logger, redisClientService, instanceSettings);
|
||||||
|
const msg = mock<PubSub.Command>({ command: 'add-webhooks-triggers-and-pollers' });
|
||||||
|
|
||||||
|
await publisher.publishCommand(msg);
|
||||||
|
|
||||||
|
expect(client.publish).toHaveBeenCalledWith(
|
||||||
|
'n8n.commands',
|
||||||
|
JSON.stringify({
|
||||||
|
...msg,
|
||||||
|
_isMockObject: true,
|
||||||
|
senderId: hostId,
|
||||||
|
selfSend: true,
|
||||||
|
debounce: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not debounce `remove-triggers-and-pollers`', async () => {
|
||||||
|
const publisher = new Publisher(logger, redisClientService, instanceSettings);
|
||||||
|
const msg = mock<PubSub.Command>({ command: 'remove-triggers-and-pollers' });
|
||||||
|
|
||||||
|
await publisher.publishCommand(msg);
|
||||||
|
|
||||||
|
expect(client.publish).toHaveBeenCalledWith(
|
||||||
|
'n8n.commands',
|
||||||
|
JSON.stringify({
|
||||||
|
...msg,
|
||||||
|
_isMockObject: true,
|
||||||
|
senderId: hostId,
|
||||||
|
selfSend: true,
|
||||||
|
debounce: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('publishWorkerResponse', () => {
|
describe('publishWorkerResponse', () => {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { PubSub } from './pubsub/pubsub.types';
|
||||||
|
|
||||||
export const QUEUE_NAME = 'jobs';
|
export const QUEUE_NAME = 'jobs';
|
||||||
|
|
||||||
export const JOB_TYPE_NAME = 'job';
|
export const JOB_TYPE_NAME = 'job';
|
||||||
|
@ -11,7 +13,7 @@ export const WORKER_RESPONSE_PUBSUB_CHANNEL = 'n8n.worker-response';
|
||||||
/**
|
/**
|
||||||
* Commands that should be sent to the sender as well, e.g. during workflow activation and
|
* Commands that should be sent to the sender as well, e.g. during workflow activation and
|
||||||
* deactivation in multi-main setup. */
|
* deactivation in multi-main setup. */
|
||||||
export const SELF_SEND_COMMANDS = new Set([
|
export const SELF_SEND_COMMANDS = new Set<PubSub.Command['command']>([
|
||||||
'add-webhooks-triggers-and-pollers',
|
'add-webhooks-triggers-and-pollers',
|
||||||
'remove-triggers-and-pollers',
|
'remove-triggers-and-pollers',
|
||||||
]);
|
]);
|
||||||
|
@ -20,7 +22,8 @@ export const SELF_SEND_COMMANDS = new Set([
|
||||||
* Commands that should not be debounced when received, e.g. during webhook handling in
|
* Commands that should not be debounced when received, e.g. during webhook handling in
|
||||||
* multi-main setup.
|
* multi-main setup.
|
||||||
*/
|
*/
|
||||||
export const IMMEDIATE_COMMANDS = new Set([
|
export const IMMEDIATE_COMMANDS = new Set<PubSub.Command['command']>([
|
||||||
'add-webhooks-triggers-and-pollers',
|
'add-webhooks-triggers-and-pollers',
|
||||||
|
'remove-triggers-and-pollers',
|
||||||
'relay-execution-lifecycle-event',
|
'relay-execution-lifecycle-event',
|
||||||
]);
|
]);
|
||||||
|
|
43
packages/cli/src/services/__tests__/url.service.test.ts
Normal file
43
packages/cli/src/services/__tests__/url.service.test.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import type { GlobalConfig } from '@n8n/config';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
import { UrlService } from '../url.service';
|
||||||
|
|
||||||
|
describe('UrlService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.WEBHOOK_URL = undefined;
|
||||||
|
config.load(config.default);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInstanceBaseUrl', () => {
|
||||||
|
it('should set URL from N8N_EDITOR_BASE_URL', () => {
|
||||||
|
config.set('editorBaseUrl', 'https://example.com/');
|
||||||
|
process.env.WEBHOOK_URL = undefined;
|
||||||
|
const urlService = new UrlService(mock<GlobalConfig>());
|
||||||
|
expect(urlService.getInstanceBaseUrl()).toBe('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set URL from WEBHOOK_URL', () => {
|
||||||
|
config.set('editorBaseUrl', '');
|
||||||
|
process.env.WEBHOOK_URL = 'https://example.com/';
|
||||||
|
const urlService = new UrlService(mock<GlobalConfig>());
|
||||||
|
expect(urlService.getInstanceBaseUrl()).toBe('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim quotes when setting URL from N8N_EDITOR_BASE_URL', () => {
|
||||||
|
config.set('editorBaseUrl', '"https://example.com"');
|
||||||
|
process.env.WEBHOOK_URL = undefined;
|
||||||
|
const urlService = new UrlService(mock<GlobalConfig>());
|
||||||
|
expect(urlService.getInstanceBaseUrl()).toBe('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim quotes when setting URL from WEBHOOK_URL', () => {
|
||||||
|
config.set('editorBaseUrl', '');
|
||||||
|
process.env.WEBHOOK_URL = '"https://example.com/"';
|
||||||
|
const urlService = new UrlService(mock<GlobalConfig>());
|
||||||
|
expect(urlService.getInstanceBaseUrl()).toBe('https://example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -14,7 +14,7 @@ export class UrlService {
|
||||||
|
|
||||||
/** Returns the base URL of the webhooks */
|
/** Returns the base URL of the webhooks */
|
||||||
getWebhookBaseUrl() {
|
getWebhookBaseUrl() {
|
||||||
let urlBaseWebhook = process.env.WEBHOOK_URL ?? this.baseUrl;
|
let urlBaseWebhook = this.trimQuotes(process.env.WEBHOOK_URL) || this.baseUrl;
|
||||||
if (!urlBaseWebhook.endsWith('/')) {
|
if (!urlBaseWebhook.endsWith('/')) {
|
||||||
urlBaseWebhook += '/';
|
urlBaseWebhook += '/';
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export class UrlService {
|
||||||
|
|
||||||
/** Return the n8n instance base URL without trailing slash */
|
/** Return the n8n instance base URL without trailing slash */
|
||||||
getInstanceBaseUrl(): string {
|
getInstanceBaseUrl(): string {
|
||||||
const n8nBaseUrl = config.getEnv('editorBaseUrl') || this.getWebhookBaseUrl();
|
const n8nBaseUrl = this.trimQuotes(config.getEnv('editorBaseUrl')) || this.getWebhookBaseUrl();
|
||||||
|
|
||||||
return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl;
|
return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl;
|
||||||
}
|
}
|
||||||
|
@ -36,4 +36,9 @@ export class UrlService {
|
||||||
}
|
}
|
||||||
return `${protocol}://${host}:${port}${path}`;
|
return `${protocol}://${host}:${port}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove leading and trailing double quotes from a URL. */
|
||||||
|
private trimQuotes(url?: string) {
|
||||||
|
return url?.replace(/^["]+|["]+$/g, '') ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "1.65.0",
|
"version": "1.66.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-design-system",
|
"name": "n8n-design-system",
|
||||||
"version": "1.55.0",
|
"version": "1.56.0",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"import": "src/main.ts",
|
"import": "src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "1.65.0",
|
"version": "1.66.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -52,7 +52,6 @@ import type {
|
||||||
AI_NODE_CREATOR_VIEW,
|
AI_NODE_CREATOR_VIEW,
|
||||||
CREDENTIAL_EDIT_MODAL_KEY,
|
CREDENTIAL_EDIT_MODAL_KEY,
|
||||||
SignInType,
|
SignInType,
|
||||||
FAKE_DOOR_FEATURES,
|
|
||||||
TRIGGER_NODE_CREATOR_VIEW,
|
TRIGGER_NODE_CREATOR_VIEW,
|
||||||
REGULAR_NODE_CREATOR_VIEW,
|
REGULAR_NODE_CREATOR_VIEW,
|
||||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||||
|
@ -62,7 +61,6 @@ import type { BulkCommand, Undoable } from '@/models/history';
|
||||||
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
||||||
|
|
||||||
import type { ProjectSharingData } from '@/types/projects.types';
|
import type { ProjectSharingData } from '@/types/projects.types';
|
||||||
import type { BaseTextKey } from './plugins/i18n';
|
|
||||||
|
|
||||||
export * from 'n8n-design-system/types';
|
export * from 'n8n-design-system/types';
|
||||||
|
|
||||||
|
@ -1036,24 +1034,6 @@ export interface NotificationOptions extends Partial<ElementNotificationOptions>
|
||||||
message: string | ElementNotificationOptions['message'];
|
message: string | ElementNotificationOptions['message'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IFakeDoor = {
|
|
||||||
id: FAKE_DOOR_FEATURES;
|
|
||||||
featureName: BaseTextKey;
|
|
||||||
icon?: string;
|
|
||||||
infoText?: BaseTextKey;
|
|
||||||
actionBoxTitle: BaseTextKey;
|
|
||||||
actionBoxDescription: BaseTextKey;
|
|
||||||
actionBoxButtonLabel?: BaseTextKey;
|
|
||||||
linkURL: string;
|
|
||||||
uiLocations: IFakeDoorLocation[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IFakeDoorLocation =
|
|
||||||
| 'settings'
|
|
||||||
| 'settings/users'
|
|
||||||
| 'credentialsModal'
|
|
||||||
| 'workflowShareModal';
|
|
||||||
|
|
||||||
export type NodeFilterType =
|
export type NodeFilterType =
|
||||||
| typeof REGULAR_NODE_CREATOR_VIEW
|
| typeof REGULAR_NODE_CREATOR_VIEW
|
||||||
| typeof TRIGGER_NODE_CREATOR_VIEW
|
| typeof TRIGGER_NODE_CREATOR_VIEW
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import Assignment from '../Assignment.vue';
|
import Assignment from './Assignment.vue';
|
||||||
import { defaultSettings } from '@/__tests__/defaults';
|
import { defaultSettings } from '@/__tests__/defaults';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
|
@ -4,7 +4,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { fireEvent, within } from '@testing-library/vue';
|
import { fireEvent, within } from '@testing-library/vue';
|
||||||
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
||||||
import AssignmentCollection from '../AssignmentCollection.vue';
|
import AssignmentCollection from './AssignmentCollection.vue';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ describe('AssignmentCollection.vue', () => {
|
||||||
await dropAssignment({ key: 'objectKey', value: {}, dropArea });
|
await dropAssignment({ key: 'objectKey', value: {}, dropArea });
|
||||||
await dropAssignment({ key: 'arrayKey', value: [], dropArea });
|
await dropAssignment({ key: 'arrayKey', value: [], dropArea });
|
||||||
|
|
||||||
let assignments = await findAllByTestId('assignment');
|
const assignments = await findAllByTestId('assignment');
|
||||||
|
|
||||||
expect(assignments.length).toBe(5);
|
expect(assignments.length).toBe(5);
|
||||||
expect(getAssignmentType(assignments[0])).toEqual('Boolean');
|
expect(getAssignmentType(assignments[0])).toEqual('Boolean');
|
|
@ -98,10 +98,10 @@ function getIssues(index: number): string[] {
|
||||||
return issues.value[`${props.parameter.name}.${index}`] ?? [];
|
return issues.value[`${props.parameter.name}.${index}`] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function optionSelected(action: 'clearAll' | 'addAll') {
|
function optionSelected(action: string) {
|
||||||
if (action === 'clearAll') {
|
if (action === 'clearAll') {
|
||||||
state.paramValue.assignments = [];
|
state.paramValue.assignments = [];
|
||||||
} else {
|
} else if (action === 'addAll' && inputData.value) {
|
||||||
const newAssignments = inputDataToAssignments(inputData.value);
|
const newAssignments = inputDataToAssignments(inputData.value);
|
||||||
state.paramValue.assignments = state.paramValue.assignments.concat(newAssignments);
|
state.paramValue.assignments = state.paramValue.assignments.concat(newAssignments);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import TypeSelect from '../TypeSelect.vue';
|
import TypeSelect from './TypeSelect.vue';
|
||||||
|
|
||||||
const DEFAULT_SETUP = {
|
const DEFAULT_SETUP = {
|
||||||
pinia: createTestingPinia(),
|
pinia: createTestingPinia(),
|
|
@ -1,6 +1,6 @@
|
||||||
import { CompletionContext } from '@codemirror/autocomplete';
|
import { CompletionContext } from '@codemirror/autocomplete';
|
||||||
import { EditorSelection, EditorState } from '@codemirror/state';
|
import { EditorSelection, EditorState } from '@codemirror/state';
|
||||||
import { useItemFieldCompletions } from '../itemField.completions';
|
import { useItemFieldCompletions } from './itemField.completions';
|
||||||
|
|
||||||
describe('inputMethodCompletions', () => {
|
describe('inputMethodCompletions', () => {
|
||||||
test('should return completions for $input.item.|', () => {
|
test('should return completions for $input.item.|', () => {
|
|
@ -1,6 +1,6 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import CollectionParameter from '../CollectionParameter.vue';
|
import CollectionParameter from './CollectionParameter.vue';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CollectionParameter, {
|
const renderComponent = createComponentRenderer(CollectionParameter, {
|
||||||
pinia: createTestingPinia(),
|
pinia: createTestingPinia(),
|
|
@ -1,5 +1,5 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import CommunityPackageInstallModal from '../CommunityPackageInstallModal.vue';
|
import CommunityPackageInstallModal from './CommunityPackageInstallModal.vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, STORES } from '@/constants';
|
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, STORES } from '@/constants';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
|
@ -22,7 +22,6 @@ import { NodeHelpers } from 'n8n-workflow';
|
||||||
import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue';
|
import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue';
|
||||||
import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue';
|
import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue';
|
||||||
import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue';
|
import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue';
|
||||||
import FeatureComingSoon from '@/components/FeatureComingSoon.vue';
|
|
||||||
import InlineNameEdit from '@/components/InlineNameEdit.vue';
|
import InlineNameEdit from '@/components/InlineNameEdit.vue';
|
||||||
import Modal from '@/components/Modal.vue';
|
import Modal from '@/components/Modal.vue';
|
||||||
import SaveButton from '@/components/SaveButton.vue';
|
import SaveButton from '@/components/SaveButton.vue';
|
||||||
|
@ -518,14 +517,13 @@ async function loadCurrentCredential() {
|
||||||
|
|
||||||
function onTabSelect(tab: string) {
|
function onTabSelect(tab: string) {
|
||||||
activeTab.value = tab;
|
activeTab.value = tab;
|
||||||
const tabName: string = tab.replaceAll('coming-soon/', '');
|
|
||||||
const credType: string = credentialType.value ? credentialType.value.name : '';
|
const credType: string = credentialType.value ? credentialType.value.name : '';
|
||||||
const activeNode: INode | null = ndvStore.activeNode;
|
const activeNode: INode | null = ndvStore.activeNode;
|
||||||
|
|
||||||
telemetry.track('User viewed credential tab', {
|
telemetry.track('User viewed credential tab', {
|
||||||
credential_type: credType,
|
credential_type: credType,
|
||||||
node_type: activeNode ? activeNode.type : null,
|
node_type: activeNode ? activeNode.type : null,
|
||||||
tab: tabName,
|
tab,
|
||||||
workflow_id: workflowsStore.workflowId,
|
workflow_id: workflowsStore.workflowId,
|
||||||
credential_id: credentialId.value,
|
credential_id: credentialId.value,
|
||||||
sharing_enabled: EnterpriseEditionFeature.Sharing,
|
sharing_enabled: EnterpriseEditionFeature.Sharing,
|
||||||
|
@ -1130,9 +1128,6 @@ function resetCredentialData(): void {
|
||||||
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
|
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
|
||||||
<CredentialInfo :current-credential="currentCredential" />
|
<CredentialInfo :current-credential="currentCredential" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
|
|
||||||
<FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
import { EnterpriseEditionFeature } from '@/constants';
|
import { EnterpriseEditionFeature } from '@/constants';
|
||||||
import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
|
import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
|
||||||
import type { PermissionsRecord } from '@/permissions';
|
import type { PermissionsRecord } from '@/permissions';
|
||||||
|
@ -39,6 +40,8 @@ const settingsStore = useSettingsStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const rolesStore = useRolesStore();
|
const rolesStore = useRolesStore();
|
||||||
|
|
||||||
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
|
||||||
const sharedWithProjects = ref([...(props.credential?.sharedWithProjects ?? [])]);
|
const sharedWithProjects = ref([...(props.credential?.sharedWithProjects ?? [])]);
|
||||||
|
|
||||||
const isSharingEnabled = computed(
|
const isSharingEnabled = computed(
|
||||||
|
@ -107,7 +110,7 @@ onMounted(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function goToUpgrade() {
|
function goToUpgrade() {
|
||||||
void uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
|
void pageRedirectionHelper.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import CredentialIcon from '@/components/CredentialIcon.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import { useNodeTypesStore } from '../../stores/nodeTypes.store';
|
import { useNodeTypesStore } from '../stores/nodeTypes.store';
|
||||||
|
|
||||||
describe('CredentialIcon', () => {
|
describe('CredentialIcon', () => {
|
||||||
const renderComponent = createComponentRenderer(CredentialIcon, {
|
const renderComponent = createComponentRenderer(CredentialIcon, {
|
|
@ -4,7 +4,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { fireEvent } from '@testing-library/vue';
|
import { fireEvent } from '@testing-library/vue';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import DropArea from '../DropArea.vue';
|
import DropArea from './DropArea.vue';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(DropArea, {
|
const renderComponent = createComponentRenderer(DropArea, {
|
||||||
pinia: createTestingPinia(),
|
pinia: createTestingPinia(),
|
|
@ -1,82 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { mapStores } from 'pinia';
|
|
||||||
import type { IFakeDoor } from '@/Interface';
|
|
||||||
import { useRootStore } from '@/stores/root.store';
|
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'FeatureComingSoon',
|
|
||||||
props: {
|
|
||||||
featureId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
showTitle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useRootStore, useSettingsStore, useUIStore, useUsersStore),
|
|
||||||
userId(): string {
|
|
||||||
return this.usersStore.currentUserId || '';
|
|
||||||
},
|
|
||||||
instanceId(): string {
|
|
||||||
return this.rootStore.instanceId;
|
|
||||||
},
|
|
||||||
featureInfo(): IFakeDoor | undefined {
|
|
||||||
return this.uiStore.fakeDoorsById[this.featureId];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openLinkPage() {
|
|
||||||
if (this.featureInfo) {
|
|
||||||
window.open(
|
|
||||||
`${this.featureInfo.linkURL}&u=${this.instanceId}#${this.userId}&v=${this.rootStore.versionCli}`,
|
|
||||||
'_blank',
|
|
||||||
);
|
|
||||||
this.$telemetry.track('user clicked feature waiting list button', {
|
|
||||||
feature: this.featureId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="featureInfo" :class="[$style.container]">
|
|
||||||
<div v-if="showTitle" class="mb-2xl">
|
|
||||||
<n8n-heading size="2xlarge">
|
|
||||||
{{ $locale.baseText(featureInfo.featureName) }}
|
|
||||||
</n8n-heading>
|
|
||||||
</div>
|
|
||||||
<div v-if="featureInfo.infoText" class="mb-l">
|
|
||||||
<n8n-info-tip theme="info" type="note">
|
|
||||||
<span v-n8n-html="$locale.baseText(featureInfo.infoText)"></span>
|
|
||||||
</n8n-info-tip>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.actionBoxContainer">
|
|
||||||
<n8n-action-box
|
|
||||||
:description="$locale.baseText(featureInfo.actionBoxDescription)"
|
|
||||||
:button-text="
|
|
||||||
$locale.baseText(featureInfo.actionBoxButtonLabel || 'fakeDoor.actionBox.button.label')
|
|
||||||
"
|
|
||||||
@click:button="openLinkPage"
|
|
||||||
>
|
|
||||||
<template #heading>
|
|
||||||
<span v-n8n-html="$locale.baseText(featureInfo.actionBoxTitle)" />
|
|
||||||
</template>
|
|
||||||
</n8n-action-box>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.actionBoxContainer {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -6,7 +6,7 @@ import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { within, waitFor } from '@testing-library/vue';
|
import { within, waitFor } from '@testing-library/vue';
|
||||||
import { getFilterOperator } from '../utils';
|
import { getFilterOperator } from './utils';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
const DEFAULT_SETUP = {
|
const DEFAULT_SETUP = {
|
|
@ -1,4 +1,4 @@
|
||||||
import { getFilterOperator, handleOperatorChange, inferOperatorType } from '../utils';
|
import { getFilterOperator, handleOperatorChange, inferOperatorType } from './utils';
|
||||||
|
|
||||||
describe('FilterConditions > utils', () => {
|
describe('FilterConditions > utils', () => {
|
||||||
describe('handleOperatorChange', () => {
|
describe('handleOperatorChange', () => {
|
|
@ -7,7 +7,7 @@ import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
||||||
import { userEvent } from '@testing-library/user-event';
|
import { userEvent } from '@testing-library/user-event';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { htmlEditorEventBus } from '../../event-bus';
|
import { htmlEditorEventBus } from '../event-bus';
|
||||||
|
|
||||||
const DEFAULT_SETUP = {
|
const DEFAULT_SETUP = {
|
||||||
props: {
|
props: {
|
|
@ -1,6 +1,6 @@
|
||||||
import { renderComponent } from '@/__tests__/render';
|
import { renderComponent } from '@/__tests__/render';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import InlineExpressionEditorOutput from '../InlineExpressionEditorOutput.vue';
|
import InlineExpressionEditorOutput from './InlineExpressionEditorOutput.vue';
|
||||||
|
|
||||||
describe('InlineExpressionEditorOutput.vue', () => {
|
describe('InlineExpressionEditorOutput.vue', () => {
|
||||||
test('should render duplicate segments correctly', async () => {
|
test('should render duplicate segments correctly', async () => {
|
|
@ -1,5 +1,5 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import InputTriple from '../InputTriple.vue';
|
import InputTriple from './InputTriple.vue';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(InputTriple);
|
const renderComponent = createComponentRenderer(InputTriple);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { createFormEventBus, createEventBus } from 'n8n-design-system/utils';
|
import { createFormEventBus, createEventBus } from 'n8n-design-system/utils';
|
||||||
import { useClipboard } from '@/composables/useClipboard';
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
|
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
clipboard,
|
clipboard,
|
||||||
...useToast(),
|
...useToast(),
|
||||||
|
...usePageRedirectionHelper(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -277,7 +279,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToUpgradeAdvancedPermissions() {
|
goToUpgradeAdvancedPermissions() {
|
||||||
void this.uiStore.goToUpgrade('advanced-permissions', 'upgrade-advanced-permissions');
|
void this.goToUpgrade('advanced-permissions', 'upgrade-advanced-permissions');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,6 +56,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||||
import { useNodeViewVersionSwitcher } from '@/composables/useNodeViewVersionSwitcher';
|
import { useNodeViewVersionSwitcher } from '@/composables/useNodeViewVersionSwitcher';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
@ -89,6 +90,7 @@ const message = useMessage();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
|
||||||
const isTagsEditEnabled = ref(false);
|
const isTagsEditEnabled = ref(false);
|
||||||
const isNameEditEnabled = ref(false);
|
const isNameEditEnabled = ref(false);
|
||||||
|
@ -584,11 +586,11 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToUpgrade() {
|
function goToUpgrade() {
|
||||||
void uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
|
void pageRedirectionHelper.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToWorkflowHistoryUpgrade() {
|
function goToWorkflowHistoryUpgrade() {
|
||||||
void uiStore.goToUpgrade('workflow-history', 'upgrade-workflow-history');
|
void pageRedirectionHelper.goToUpgrade('workflow-history', 'upgrade-workflow-history');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCreateWorkflowSuccessToast(id?: string) {
|
function showCreateWorkflowSuccessToast(id?: string) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { useUserHelpers } from '@/composables/useUserHelpers';
|
||||||
|
|
||||||
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
|
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
|
||||||
import { useBugReporting } from '@/composables/useBugReporting';
|
import { useBugReporting } from '@/composables/useBugReporting';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore();
|
const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore();
|
||||||
const cloudPlanStore = useCloudPlanStore();
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
|
@ -38,6 +39,7 @@ const locale = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
const { getReportingURL } = useBugReporting();
|
const { getReportingURL } = useBugReporting();
|
||||||
|
|
||||||
useUserHelpers(router, route);
|
useUserHelpers(router, route);
|
||||||
|
@ -260,7 +262,7 @@ const handleSelect = (key: string) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'cloud-admin': {
|
case 'cloud-admin': {
|
||||||
void cloudPlanStore.redirectToDashboard();
|
void pageRedirectionHelper.goToDashboard();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'quickstart':
|
case 'quickstart':
|
||||||
|
|
|
@ -271,7 +271,7 @@ const nodeClass = computed(() => {
|
||||||
const nodeExecutionStatus = computed(() => {
|
const nodeExecutionStatus = computed(() => {
|
||||||
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[props.name];
|
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[props.name];
|
||||||
if (nodeExecutionRunData) {
|
if (nodeExecutionRunData) {
|
||||||
return nodeExecutionRunData.filter(Boolean)[0]?.executionStatus ?? '';
|
return nodeExecutionRunData.filter(Boolean)?.[0]?.executionStatus ?? '';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { screen } from '@testing-library/vue';
|
import { screen } from '@testing-library/vue';
|
||||||
import CategoryItem from '../ItemTypes/CategoryItem.vue';
|
import CategoryItem from './ItemTypes/CategoryItem.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CategoryItem);
|
const renderComponent = createComponentRenderer(CategoryItem);
|
|
@ -8,7 +8,7 @@ import {
|
||||||
mockActionCreateElement,
|
mockActionCreateElement,
|
||||||
mockViewCreateElement,
|
mockViewCreateElement,
|
||||||
mockSectionCreateElement,
|
mockSectionCreateElement,
|
||||||
} from './utils';
|
} from './__tests__/utils';
|
||||||
import ItemsRenderer from '@/components/Node/NodeCreator/Renderers/ItemsRenderer.vue';
|
import ItemsRenderer from '@/components/Node/NodeCreator/Renderers/ItemsRenderer.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
|
@ -29,6 +29,7 @@ import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue'
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
nodeTypeSelected: [value: [actionKey: string, nodeName: string] | [nodeName: string]];
|
nodeTypeSelected: [value: [actionKey: string, nodeName: string] | [nodeName: string]];
|
||||||
|
@ -47,6 +48,8 @@ const {
|
||||||
actionsCategoryLocales,
|
actionsCategoryLocales,
|
||||||
} = useActions();
|
} = useActions();
|
||||||
|
|
||||||
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
|
|
||||||
// We only inject labels if search is empty
|
// We only inject labels if search is empty
|
||||||
const parsedTriggerActions = computed(() =>
|
const parsedTriggerActions = computed(() =>
|
||||||
parseActions(actions.value, actionsCategoryLocales.value.triggers, false),
|
parseActions(actions.value, actionsCategoryLocales.value.triggers, false),
|
||||||
|
@ -182,7 +185,7 @@ function trackActionsView() {
|
||||||
};
|
};
|
||||||
|
|
||||||
void useExternalHooks().run('nodeCreateList.onViewActions', trackingPayload);
|
void useExternalHooks().run('nodeCreateList.onViewActions', trackingPayload);
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload);
|
nodeCreatorStore.onViewActions(trackingPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearch() {
|
function resetSearch() {
|
||||||
|
@ -206,7 +209,7 @@ function addHttpNode() {
|
||||||
void useExternalHooks().run('nodeCreateList.onActionsCustmAPIClicked', {
|
void useExternalHooks().run('nodeCreateList.onActionsCustmAPIClicked', {
|
||||||
app_identifier,
|
app_identifier,
|
||||||
});
|
});
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
nodeCreatorStore.onActionsCustomAPIClicked({ app_identifier });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anonymous component to handle triggers and actions rendering order
|
// Anonymous component to handle triggers and actions rendering order
|
||||||
|
|
|
@ -23,7 +23,6 @@ import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
|
||||||
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
|
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
|
||||||
import NoResults from '../Panel/NoResults.vue';
|
import NoResults from '../Panel/NoResults.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils';
|
import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
|
@ -36,11 +35,10 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const telemetry = useTelemetry();
|
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
|
|
||||||
const { mergedNodes, actions } = useNodeCreatorStore();
|
const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore();
|
||||||
const { pushViewStack, popViewStack } = useViewStacks();
|
const { pushViewStack, popViewStack } = useViewStacks();
|
||||||
|
|
||||||
const { registerKeyHook } = useKeyboardNavigation();
|
const { registerKeyHook } = useKeyboardNavigation();
|
||||||
|
@ -83,7 +81,7 @@ function onSelected(item: INodeCreateElement) {
|
||||||
sections: item.properties.sections,
|
sections: item.properties.sections,
|
||||||
});
|
});
|
||||||
|
|
||||||
telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
|
onSubcategorySelected({
|
||||||
subcategory: item.key,
|
subcategory: item.key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -153,9 +151,6 @@ function onSelected(item: INodeCreateElement) {
|
||||||
|
|
||||||
if (item.type === 'link') {
|
if (item.type === 'link') {
|
||||||
window.open(item.properties.url, '_blank');
|
window.open(item.properties.url, '_blank');
|
||||||
telemetry.trackNodesPanel('nodeCreateList.onLinkSelected', {
|
|
||||||
link: item.properties.url,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { createPinia } from 'pinia';
|
||||||
import { screen, fireEvent } from '@testing-library/vue';
|
import { screen, fireEvent } from '@testing-library/vue';
|
||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { mockSimplifiedNodeType } from './utils';
|
import { mockSimplifiedNodeType } from './__tests__/utils';
|
||||||
import NodesListPanel from '../Panel/NodesListPanel.vue';
|
import NodesListPanel from './Panel/NodesListPanel.vue';
|
||||||
import { REGULAR_NODE_CREATOR_VIEW } from '@/constants';
|
import { REGULAR_NODE_CREATOR_VIEW } from '@/constants';
|
||||||
import type { NodeFilterType } from '@/Interface';
|
import type { NodeFilterType } from '@/Interface';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
|
@ -17,12 +17,15 @@ import SearchBar from './SearchBar.vue';
|
||||||
import ActionsRenderer from '../Modes/ActionsMode.vue';
|
import ActionsRenderer from '../Modes/ActionsMode.vue';
|
||||||
import NodesRenderer from '../Modes/NodesMode.vue';
|
import NodesRenderer from '../Modes/NodesMode.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const { callDebounced } = useDebounce();
|
||||||
|
|
||||||
const { mergedNodes } = useNodeCreatorStore();
|
const { mergedNodes } = useNodeCreatorStore();
|
||||||
const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks();
|
const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks();
|
||||||
const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation();
|
const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation();
|
||||||
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
|
|
||||||
const activeViewStack = computed(() => useViewStacks().activeViewStack);
|
const activeViewStack = computed(() => useViewStacks().activeViewStack);
|
||||||
|
|
||||||
|
@ -55,6 +58,19 @@ function onSearch(value: string) {
|
||||||
if (activeViewStack.value.uuid) {
|
if (activeViewStack.value.uuid) {
|
||||||
updateCurrentViewStack({ search: value });
|
updateCurrentViewStack({ search: value });
|
||||||
void setActiveItemIndex(getDefaultActiveIndex(value));
|
void setActiveItemIndex(getDefaultActiveIndex(value));
|
||||||
|
if (value.length) {
|
||||||
|
callDebounced(
|
||||||
|
nodeCreatorStore.onNodeFilterChanged,
|
||||||
|
{ trailing: true, debounceTime: 2000 },
|
||||||
|
{
|
||||||
|
newValue: value,
|
||||||
|
filteredNodes: activeViewStack.value.items ?? [],
|
||||||
|
filterMode: activeViewStack.value.rootView ?? 'Regular',
|
||||||
|
subcategory: activeViewStack.value.subcategory,
|
||||||
|
title: activeViewStack.value.title,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,6 +315,7 @@ function onBackButton() {
|
||||||
margin-top: var(--spacing-4xs);
|
margin-top: var(--spacing-4xs);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
line-height: 19px;
|
line-height: 19px;
|
||||||
|
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
||||||
import { useViewStacks } from '../composables/useViewStacks';
|
import { useViewStacks } from '../composables/useViewStacks';
|
||||||
import ItemsRenderer from './ItemsRenderer.vue';
|
import ItemsRenderer from './ItemsRenderer.vue';
|
||||||
import CategoryItem from '../ItemTypes/CategoryItem.vue';
|
import CategoryItem from '../ItemTypes/CategoryItem.vue';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
elements: INodeCreateElement[];
|
elements: INodeCreateElement[];
|
||||||
|
@ -24,10 +24,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
elements: () => [],
|
elements: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
|
||||||
const { popViewStack } = useViewStacks();
|
const { popViewStack } = useViewStacks();
|
||||||
const { registerKeyHook } = useKeyboardNavigation();
|
const { registerKeyHook } = useKeyboardNavigation();
|
||||||
const { workflowId } = useWorkflowsStore();
|
const { workflowId } = useWorkflowsStore();
|
||||||
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
|
|
||||||
const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId);
|
const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId);
|
||||||
const actionCount = computed(() => props.elements.filter(({ type }) => type === 'action').length);
|
const actionCount = computed(() => props.elements.filter(({ type }) => type === 'action').length);
|
||||||
|
@ -38,10 +38,11 @@ function toggleExpanded() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setExpanded(isExpanded: boolean) {
|
function setExpanded(isExpanded: boolean) {
|
||||||
|
const prev = expanded.value;
|
||||||
expanded.value = isExpanded;
|
expanded.value = isExpanded;
|
||||||
|
|
||||||
if (expanded.value) {
|
if (expanded.value && !prev) {
|
||||||
telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
|
nodeCreatorStore.onCategoryExpanded({
|
||||||
category_name: props.category,
|
category_name: props.category,
|
||||||
workflow_id: workflowId,
|
workflow_id: workflowId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -339,7 +339,11 @@ export const useActions = () => {
|
||||||
return storeWatcher;
|
return storeWatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackActionSelected(action: IUpdateInformation, telemetry: Telemetry, rootView: string) {
|
function trackActionSelected(
|
||||||
|
action: IUpdateInformation,
|
||||||
|
_telemetry: Telemetry,
|
||||||
|
rootView: string,
|
||||||
|
) {
|
||||||
const payload = {
|
const payload = {
|
||||||
node_type: action.key,
|
node_type: action.key,
|
||||||
action: action.name,
|
action: action.name,
|
||||||
|
@ -347,7 +351,7 @@ export const useActions = () => {
|
||||||
resource: (action.value as INodeParameters).resource || '',
|
resource: (action.value as INodeParameters).resource || '',
|
||||||
};
|
};
|
||||||
void useExternalHooks().run('nodeCreateList.addAction', payload);
|
void useExternalHooks().run('nodeCreateList.addAction', payload);
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.addAction', payload);
|
useNodeCreatorStore().onAddActions(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useActions } from '../composables/useActions';
|
import { useActions } from './composables/useActions';
|
||||||
import {
|
import {
|
||||||
AGENT_NODE_TYPE,
|
AGENT_NODE_TYPE,
|
||||||
GITHUB_TRIGGER_NODE_TYPE,
|
GITHUB_TRIGGER_NODE_TYPE,
|
|
@ -1,5 +1,5 @@
|
||||||
import { NodeConnectionType, type INodeProperties, type INodeTypeDescription } from 'n8n-workflow';
|
import { NodeConnectionType, type INodeProperties, type INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { useActionsGenerator } from '../composables/useActionsGeneration';
|
import { useActionsGenerator } from './composables/useActionsGeneration';
|
||||||
|
|
||||||
describe('useActionsGenerator', () => {
|
describe('useActionsGenerator', () => {
|
||||||
const { generateMergedNodesAndActions } = useActionsGenerator();
|
const { generateMergedNodesAndActions } = useActionsGenerator();
|
|
@ -1,6 +1,6 @@
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed } from 'vue';
|
||||||
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
import { useKeyboardNavigation } from './composables/useKeyboardNavigation';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import type { SectionCreateElement } from '@/Interface';
|
import type { SectionCreateElement } from '@/Interface';
|
||||||
import { formatTriggerActionName, groupItemsInSections, sortNodeCreateElements } from '../utils';
|
import { formatTriggerActionName, groupItemsInSections, sortNodeCreateElements } from './utils';
|
||||||
import { mockActionCreateElement, mockNodeCreateElement, mockSectionCreateElement } from './utils';
|
import {
|
||||||
|
mockActionCreateElement,
|
||||||
|
mockNodeCreateElement,
|
||||||
|
mockSectionCreateElement,
|
||||||
|
} from './__tests__/utils';
|
||||||
|
|
||||||
describe('NodeCreator - utils', () => {
|
describe('NodeCreator - utils', () => {
|
||||||
describe('groupItemsInSections', () => {
|
describe('groupItemsInSections', () => {
|
|
@ -124,7 +124,7 @@ const currentWorkflow = computed(() =>
|
||||||
);
|
);
|
||||||
const hasForeignCredential = computed(() => props.foreignCredentials.length > 0);
|
const hasForeignCredential = computed(() => props.foreignCredentials.length > 0);
|
||||||
const isHomeProjectTeam = computed(
|
const isHomeProjectTeam = computed(
|
||||||
() => currentWorkflow.value.homeProject?.type === ProjectTypes.Team,
|
() => currentWorkflow.value?.homeProject?.type === ProjectTypes.Team,
|
||||||
);
|
);
|
||||||
const isReadOnly = computed(
|
const isReadOnly = computed(
|
||||||
() => props.readOnly || (hasForeignCredential.value && !isHomeProjectTeam.value),
|
() => props.readOnly || (hasForeignCredential.value && !isHomeProjectTeam.value),
|
||||||
|
|
|
@ -100,14 +100,15 @@ const isTriggerNode = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasAiMetadata = computed(() => {
|
const hasAiMetadata = computed(() => {
|
||||||
|
if (isNodeRunning.value || !workflowRunData.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (node.value) {
|
if (node.value) {
|
||||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(node.value.name);
|
const connectedSubNodes = props.workflow.getParentNodes(node.value.name, 'ALL_NON_MAIN');
|
||||||
|
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
|
||||||
|
|
||||||
if (!resultData || !Array.isArray(resultData) || resultData.length === 0) {
|
return resultData && Array.isArray(resultData) && resultData.length > 0;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!resultData[resultData.length - 1].metadata;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
@ -295,6 +296,7 @@ const activatePane = () => {
|
||||||
:block-u-i="blockUI"
|
:block-u-i="blockUI"
|
||||||
:is-production-execution-preview="isProductionExecutionPreview"
|
:is-production-execution-preview="isProductionExecutionPreview"
|
||||||
:is-pane-active="isPaneActive"
|
:is-pane-active="isPaneActive"
|
||||||
|
:hide-pagination="outputMode === 'logs'"
|
||||||
pane-type="output"
|
pane-type="output"
|
||||||
:data-output-type="outputMode"
|
:data-output-type="outputMode"
|
||||||
@activate-pane="activatePane"
|
@activate-pane="activatePane"
|
||||||
|
@ -368,7 +370,7 @@ const activatePane = () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="outputMode === 'logs' && node" #content>
|
<template v-if="outputMode === 'logs' && node" #content>
|
||||||
<RunDataAi :node="node" :run-index="runIndex" />
|
<RunDataAi :node="node" :run-index="runIndex" :workflow="workflow" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #recovered-artificial-output-data>
|
<template #recovered-artificial-output-data>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import type { useNodeTypesStore } from '../../stores/nodeTypes.store';
|
import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
let mockNdvState: Partial<ReturnType<typeof useNDVStore>>;
|
let mockNdvState: Partial<ReturnType<typeof useNDVStore>>;
|
123
packages/editor-ui/src/components/ParameterOptions.test.ts
Normal file
123
packages/editor-ui/src/components/ParameterOptions.test.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { renderComponent } from '@/__tests__/render';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { within } from '@testing-library/vue';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import ParameterOptions from './ParameterOptions.vue';
|
||||||
|
|
||||||
|
const DEFAULT_PARAMETER = {
|
||||||
|
displayName: 'Fields to Set',
|
||||||
|
name: 'assignments',
|
||||||
|
type: 'assignmentCollection',
|
||||||
|
default: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ParameterOptions', () => {
|
||||||
|
it('renders default options properly', () => {
|
||||||
|
const { getByTestId } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('parameter-options-container')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('action-toggle')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('radio-button-fixed')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('radio-button-expression')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't render expression with showExpression set to false", () => {
|
||||||
|
const { getByTestId, queryByTestId, container } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
isReadOnly: false,
|
||||||
|
showExpressionSelector: false,
|
||||||
|
value: 'manual',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('parameter-options-container')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('action-toggle')).toBeInTheDocument();
|
||||||
|
expect(queryByTestId('radio-button-fixed')).not.toBeInTheDocument();
|
||||||
|
expect(queryByTestId('radio-button-expression')).not.toBeInTheDocument();
|
||||||
|
expect(container.querySelector('.noExpressionSelector')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render loading state', () => {
|
||||||
|
const CUSTOM_LOADING_MESSAGE = 'Loading...';
|
||||||
|
const { getByTestId, getByText } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
isReadOnly: false,
|
||||||
|
showExpressionSelector: false,
|
||||||
|
value: 'manual',
|
||||||
|
loading: true,
|
||||||
|
loadingMessage: CUSTOM_LOADING_MESSAGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('parameter-options-loader')).toBeInTheDocument();
|
||||||
|
expect(getByText(CUSTOM_LOADING_MESSAGE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render horizontal icon', () => {
|
||||||
|
const { container } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
value: 'manual',
|
||||||
|
isReadOnly: false,
|
||||||
|
iconOrientation: 'horizontal',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(container.querySelector('[data-icon="ellipsis-h"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom actions', async () => {
|
||||||
|
const CUSTOM_ACTIONS = [
|
||||||
|
{ label: 'Action 1', value: 'action1' },
|
||||||
|
{ label: 'Action 2', value: 'action2' },
|
||||||
|
];
|
||||||
|
const { getByTestId } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
value: 'manual',
|
||||||
|
isReadOnly: false,
|
||||||
|
customActions: CUSTOM_ACTIONS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const actionToggle = getByTestId('action-toggle');
|
||||||
|
const actionToggleButton = within(actionToggle).getByRole('button');
|
||||||
|
expect(actionToggleButton).toBeVisible();
|
||||||
|
await userEvent.click(actionToggle);
|
||||||
|
const actionToggleId = actionToggleButton.getAttribute('aria-controls');
|
||||||
|
const actionDropdown = document.getElementById(actionToggleId as string) as HTMLElement;
|
||||||
|
expect(actionDropdown).toBeInTheDocument();
|
||||||
|
// All custom actions should be rendered
|
||||||
|
CUSTOM_ACTIONS.forEach((action) => {
|
||||||
|
expect(within(actionDropdown).getByText(action.label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit update:modelValue when changing to expression', async () => {
|
||||||
|
const { emitted, getByTestId } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
value: 'manual',
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('radio-button-expression')).toBeInTheDocument();
|
||||||
|
await userEvent.click(getByTestId('radio-button-expression'));
|
||||||
|
await waitFor(() => expect(emitted('update:modelValue')).toEqual([['addExpression']]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit update:modelValue when changing to fixed', async () => {
|
||||||
|
const { emitted, getByTestId } = renderComponent(ParameterOptions, {
|
||||||
|
props: {
|
||||||
|
parameter: DEFAULT_PARAMETER,
|
||||||
|
value: '=manual',
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('radio-button-fixed')).toBeInTheDocument();
|
||||||
|
await userEvent.click(getByTestId('radio-button-fixed'));
|
||||||
|
await waitFor(() => expect(emitted('update:modelValue')).toEqual([['removeExpression']]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,169 +1,134 @@
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||||
import { i18n } from '@/plugins/i18n';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
interface Props {
|
||||||
name: 'ParameterOptions',
|
parameter: INodeProperties;
|
||||||
props: {
|
isReadOnly: boolean;
|
||||||
parameter: {
|
value: NodeParameterValueType;
|
||||||
type: Object as PropType<INodeProperties>,
|
showOptions?: boolean;
|
||||||
required: true,
|
showExpressionSelector?: boolean;
|
||||||
},
|
customActions?: Array<{ label: string; value: string; disabled?: boolean }>;
|
||||||
isReadOnly: {
|
iconOrientation?: 'horizontal' | 'vertical';
|
||||||
type: Boolean,
|
loading?: boolean;
|
||||||
},
|
loadingMessage?: string;
|
||||||
value: {
|
}
|
||||||
type: [Object, String, Number, Boolean, Array] as PropType<NodeParameterValueType>,
|
|
||||||
},
|
|
||||||
showOptions: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showExpressionSelector: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
customActions: {
|
|
||||||
type: Array as PropType<Array<{ label: string; value: string; disabled?: boolean }>>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
iconOrientation: {
|
|
||||||
type: String,
|
|
||||||
default: 'vertical',
|
|
||||||
validator: (value: string): boolean => ['horizontal', 'vertical'].includes(value),
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
loadingMessage: {
|
|
||||||
type: String,
|
|
||||||
default() {
|
|
||||||
return i18n.baseText('genericHelpers.loading');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'menu-expanded'],
|
|
||||||
computed: {
|
|
||||||
isDefault(): boolean {
|
|
||||||
return this.parameter.default === this.value;
|
|
||||||
},
|
|
||||||
isValueExpression(): boolean {
|
|
||||||
return isValueExpression(this.parameter, this.value);
|
|
||||||
},
|
|
||||||
isHtmlEditor(): boolean {
|
|
||||||
return this.getArgument('editor') === 'htmlEditor';
|
|
||||||
},
|
|
||||||
shouldShowExpressionSelector(): boolean {
|
|
||||||
return this.parameter.noDataExpression !== true && this.showExpressionSelector;
|
|
||||||
},
|
|
||||||
shouldShowOptions(): boolean {
|
|
||||||
if (this.isReadOnly) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.parameter.type === 'collection' || this.parameter.type === 'credentialsSelect') {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
return false;
|
showOptions: true,
|
||||||
}
|
showExpressionSelector: true,
|
||||||
|
customActions: () => [],
|
||||||
if (['codeNodeEditor', 'sqlEditor'].includes(this.parameter.typeOptions?.editor ?? '')) {
|
iconOrientation: 'vertical',
|
||||||
return false;
|
loading: false,
|
||||||
}
|
loadingMessage: () => useI18n().baseText('genericHelpers.loading'),
|
||||||
|
|
||||||
if (this.showOptions) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
selectedView() {
|
|
||||||
if (this.isValueExpression) {
|
|
||||||
return 'expression';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'fixed';
|
|
||||||
},
|
|
||||||
hasRemoteMethod(): boolean {
|
|
||||||
return !!this.getArgument('loadOptionsMethod') || !!this.getArgument('loadOptions');
|
|
||||||
},
|
|
||||||
actions(): Array<{ label: string; value: string; disabled?: boolean }> {
|
|
||||||
if (Array.isArray(this.customActions) && this.customActions.length > 0) {
|
|
||||||
return this.customActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isHtmlEditor && !this.isValueExpression) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: this.$locale.baseText('parameterInput.formatHtml'),
|
|
||||||
value: 'formatHtml',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
label: this.$locale.baseText('parameterInput.resetValue'),
|
|
||||||
value: 'resetValue',
|
|
||||||
disabled: this.isDefault,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.hasRemoteMethod ||
|
|
||||||
(this.parameter.type === 'resourceLocator' &&
|
|
||||||
isResourceLocatorValue(this.value) &&
|
|
||||||
this.value.mode === 'list')
|
|
||||||
) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: this.$locale.baseText('parameterInput.refreshList'),
|
|
||||||
value: 'refreshOptions',
|
|
||||||
},
|
|
||||||
...actions,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onMenuToggle(visible: boolean) {
|
|
||||||
this.$emit('menu-expanded', visible);
|
|
||||||
},
|
|
||||||
onViewSelected(selected: string) {
|
|
||||||
if (selected === 'expression') {
|
|
||||||
this.$emit(
|
|
||||||
'update:modelValue',
|
|
||||||
this.isValueExpression ? 'openExpression' : 'addExpression',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected === 'fixed' && this.isValueExpression) {
|
|
||||||
this.$emit('update:modelValue', 'removeExpression');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getArgument(argumentName: string): string | number | boolean | undefined {
|
|
||||||
if (this.parameter.typeOptions === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.parameter.typeOptions[argumentName] === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.parameter.typeOptions[argumentName];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string];
|
||||||
|
'menu-expanded': [visible: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const isDefault = computed(() => props.parameter.default === props.value);
|
||||||
|
const isValueAnExpression = computed(() => isValueExpression(props.parameter, props.value));
|
||||||
|
const isHtmlEditor = computed(() => getArgument('editor') === 'htmlEditor');
|
||||||
|
const shouldShowExpressionSelector = computed(
|
||||||
|
() => !props.parameter.noDataExpression && props.showExpressionSelector,
|
||||||
|
);
|
||||||
|
const shouldShowOptions = computed(() => {
|
||||||
|
if (props.isReadOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.parameter.type === 'collection' || props.parameter.type === 'credentialsSelect') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['codeNodeEditor', 'sqlEditor'].includes(props.parameter.typeOptions?.editor ?? '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.showOptions) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
||||||
|
const hasRemoteMethod = computed(
|
||||||
|
() =>
|
||||||
|
!!props.parameter.typeOptions?.loadOptionsMethod || !!props.parameter.typeOptions?.loadOptions,
|
||||||
|
);
|
||||||
|
const actions = computed(() => {
|
||||||
|
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
||||||
|
return props.customActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHtmlEditor.value && !isValueAnExpression.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: i18n.baseText('parameterInput.formatHtml'),
|
||||||
|
value: 'formatHtml',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterActions = [
|
||||||
|
{
|
||||||
|
label: i18n.baseText('parameterInput.resetValue'),
|
||||||
|
value: 'resetValue',
|
||||||
|
disabled: isDefault.value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasRemoteMethod.value ||
|
||||||
|
(props.parameter.type === 'resourceLocator' &&
|
||||||
|
isResourceLocatorValue(props.value) &&
|
||||||
|
props.value.mode === 'list')
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: i18n.baseText('parameterInput.refreshList'),
|
||||||
|
value: 'refreshOptions',
|
||||||
|
},
|
||||||
|
...parameterActions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameterActions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onMenuToggle = (visible: boolean) => emit('menu-expanded', visible);
|
||||||
|
const onViewSelected = (selected: string) => {
|
||||||
|
if (selected === 'expression') {
|
||||||
|
emit('update:modelValue', isValueAnExpression.value ? 'openExpression' : 'addExpression');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected === 'fixed' && isValueAnExpression.value) {
|
||||||
|
emit('update:modelValue', 'removeExpression');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getArgument = (argumentName: string) => {
|
||||||
|
if (props.parameter.typeOptions === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.parameter.typeOptions[argumentName] === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.parameter.typeOptions[argumentName];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container" data-test-id="parameter-options-container">
|
||||||
<div v-if="loading" :class="$style.loader">
|
<div v-if="loading" :class="$style.loader" data-test-id="parameter-options-loader">
|
||||||
<n8n-text v-if="loading" size="small">
|
<n8n-text v-if="loading" size="small">
|
||||||
<n8n-icon icon="sync-alt" size="xsmall" :spin="true" />
|
<n8n-icon icon="sync-alt" size="xsmall" :spin="true" />
|
||||||
{{ loadingMessage }}
|
{{ loadingMessage }}
|
||||||
|
@ -193,8 +158,8 @@ export default defineComponent({
|
||||||
:model-value="selectedView"
|
:model-value="selectedView"
|
||||||
:disabled="isReadOnly"
|
:disabled="isReadOnly"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: $locale.baseText('parameterInput.fixed'), value: 'fixed' },
|
{ label: i18n.baseText('parameterInput.fixed'), value: 'fixed' },
|
||||||
{ label: $locale.baseText('parameterInput.expression'), value: 'expression' },
|
{ label: i18n.baseText('parameterInput.expression'), value: 'expression' },
|
||||||
]"
|
]"
|
||||||
@update:model-value="onViewSelected"
|
@update:model-value="onViewSelected"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { createRouter, createMemoryHistory, useRouter } from 'vue-router';
|
||||||
import { createProjectListItem } from '@/__tests__/data/projects';
|
import { createProjectListItem } from '@/__tests__/data/projects';
|
||||||
import ProjectsNavigation from '@/components/Projects//ProjectNavigation.vue';
|
import ProjectsNavigation from '@/components/Projects//ProjectNavigation.vue';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
import type { Project } from '@/types/projects.types';
|
import type { Project } from '@/types/projects.types';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
vi.mock('vue-router', async () => {
|
vi.mock('vue-router', async () => {
|
||||||
const actual = await vi.importActual('vue-router');
|
const actual = await vi.importActual('vue-router');
|
||||||
|
@ -36,6 +36,15 @@ vi.mock('@/composables/useToast', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('@/composables/usePageRedirectionHelper', () => {
|
||||||
|
const goToUpgrade = vi.fn();
|
||||||
|
return {
|
||||||
|
usePageRedirectionHelper: () => ({
|
||||||
|
goToUpgrade,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(ProjectsNavigation, {
|
const renderComponent = createComponentRenderer(ProjectsNavigation, {
|
||||||
global: {
|
global: {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -56,7 +65,7 @@ const renderComponent = createComponentRenderer(ProjectsNavigation, {
|
||||||
let router: ReturnType<typeof useRouter>;
|
let router: ReturnType<typeof useRouter>;
|
||||||
let toast: ReturnType<typeof useToast>;
|
let toast: ReturnType<typeof useToast>;
|
||||||
let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;
|
let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;
|
||||||
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
let pageRedirectionHelper: ReturnType<typeof usePageRedirectionHelper>;
|
||||||
|
|
||||||
const personalProjects = Array.from({ length: 3 }, createProjectListItem);
|
const personalProjects = Array.from({ length: 3 }, createProjectListItem);
|
||||||
const teamProjects = Array.from({ length: 3 }, () => createProjectListItem('team'));
|
const teamProjects = Array.from({ length: 3 }, () => createProjectListItem('team'));
|
||||||
|
@ -67,9 +76,9 @@ describe('ProjectsNavigation', () => {
|
||||||
|
|
||||||
router = useRouter();
|
router = useRouter();
|
||||||
toast = useToast();
|
toast = useToast();
|
||||||
|
pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
|
||||||
projectsStore = mockedStore(useProjectsStore);
|
projectsStore = mockedStore(useProjectsStore);
|
||||||
uiStore = mockedStore(useUIStore);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw an error', () => {
|
it('should not throw an error', () => {
|
||||||
|
@ -144,7 +153,7 @@ describe('ProjectsNavigation', () => {
|
||||||
expect(getByText(/You have reached the Free plan limit of 3/)).toBeVisible();
|
expect(getByText(/You have reached the Free plan limit of 3/)).toBeVisible();
|
||||||
await userEvent.click(getByText('View plans'));
|
await userEvent.click(getByText('View plans'));
|
||||||
|
|
||||||
expect(uiStore.goToUpgrade).toHaveBeenCalledWith('rbac', 'upgrade-rbac');
|
expect(pageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith('rbac', 'upgrade-rbac');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Projects" title and Personal project when the feature is enabled', async () => {
|
it('should show "Projects" title and Personal project when the feature is enabled', async () => {
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { VIEWS } from '@/constants';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { ProjectListItem } from '@/types/projects.types';
|
import type { ProjectListItem } from '@/types/projects.types';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { sortByProperty } from '@/utils/sortUtils';
|
import { sortByProperty } from '@/utils/sortUtils';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
@ -21,7 +21,7 @@ const router = useRouter();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const uiStore = useUIStore();
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
|
||||||
const isCreatingProject = ref(false);
|
const isCreatingProject = ref(false);
|
||||||
const isComponentMounted = ref(false);
|
const isComponentMounted = ref(false);
|
||||||
|
@ -99,7 +99,7 @@ const canCreateProjects = computed(
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToUpgrade = async () => {
|
const goToUpgrade = async () => {
|
||||||
await uiStore.goToUpgrade('rbac', 'upgrade-rbac');
|
await pageRedirectionHelper.goToUpgrade('rbac', 'upgrade-rbac');
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
limit: number;
|
limit: number;
|
||||||
|
@ -9,11 +9,11 @@ type Props = {
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const visible = defineModel<boolean>();
|
const visible = defineModel<boolean>();
|
||||||
const uiStore = useUIStore();
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
|
||||||
const goToUpgrade = async () => {
|
const goToUpgrade = async () => {
|
||||||
await uiStore.goToUpgrade('rbac', 'upgrade-rbac');
|
await pageRedirectionHelper.goToUpgrade('rbac', 'upgrade-rbac');
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
DEFAULT_SETUP,
|
DEFAULT_SETUP,
|
||||||
MAPPING_COLUMNS_RESPONSE,
|
MAPPING_COLUMNS_RESPONSE,
|
||||||
UPDATED_SCHEMA,
|
UPDATED_SCHEMA,
|
||||||
} from './utils/ResourceMapper.utils';
|
} from './__tests__/utils/ResourceMapper.utils';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { cleanupAppModals, createAppModals, waitAllPromises } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, waitAllPromises } from '@/__tests__/utils';
|
||||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
|
@ -304,6 +304,7 @@ defineExpose({
|
||||||
:loading="props.refreshInProgress"
|
:loading="props.refreshInProgress"
|
||||||
:loading-message="fetchingFieldsLabel"
|
:loading-message="fetchingFieldsLabel"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
|
:value="props.paramValue"
|
||||||
@update:model-value="onParameterActionSelected"
|
@update:model-value="onParameterActionSelected"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -187,6 +187,7 @@ defineExpose({
|
||||||
:loading="props.refreshInProgress"
|
:loading="props.refreshInProgress"
|
||||||
:loading-message="fetchingFieldsLabel"
|
:loading-message="fetchingFieldsLabel"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
|
:value="state.selected"
|
||||||
@update:model-value="onParameterActionSelected"
|
@update:model-value="onParameterActionSelected"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -162,6 +162,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
hidePagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
@ -1743,6 +1747,7 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
|
hidePagination === false &&
|
||||||
hasNodeRun &&
|
hasNodeRun &&
|
||||||
!hasRunError &&
|
!hasRunError &&
|
||||||
displayMode !== 'binary' &&
|
displayMode !== 'binary' &&
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import type { ITaskSubRunMetadata, ITaskDataConnections } from 'n8n-workflow';
|
import type { ITaskDataConnections, NodeConnectionType, Workflow, ITaskData } from 'n8n-workflow';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
|
||||||
import type { IAiData, IAiDataContent, INodeUi } from '@/Interface';
|
import type { IAiData, IAiDataContent, INodeUi } from '@/Interface';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
@ -28,29 +27,21 @@ export interface Props {
|
||||||
runIndex?: number;
|
runIndex?: number;
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
slim?: boolean;
|
slim?: boolean;
|
||||||
|
workflow: Workflow;
|
||||||
}
|
}
|
||||||
const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
|
const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const selectedRun: Ref<IAiData[]> = ref([]);
|
const selectedRun: Ref<IAiData[]> = ref([]);
|
||||||
|
|
||||||
function isTreeNodeSelected(node: TreeNode) {
|
function isTreeNodeSelected(node: TreeNode) {
|
||||||
return selectedRun.value.some((run) => run.node === node.node && run.runIndex === node.runIndex);
|
return selectedRun.value.some((run) => run.node === node.node && run.runIndex === node.runIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReferencedData(
|
function getReferencedData(
|
||||||
reference: ITaskSubRunMetadata,
|
taskData: ITaskData,
|
||||||
withInput: boolean,
|
withInput: boolean,
|
||||||
withOutput: boolean,
|
withOutput: boolean,
|
||||||
): IAiDataContent[] {
|
): IAiDataContent[] {
|
||||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(reference.node);
|
|
||||||
|
|
||||||
if (!resultData?.[reference.runIndex]) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskData = resultData[reference.runIndex];
|
|
||||||
|
|
||||||
if (!taskData) {
|
if (!taskData) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -98,18 +89,18 @@ function onItemClick(data: TreeNode) {
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedNodeRun = workflowsStore.getWorkflowResultDataByNodeName(data.node)?.[
|
||||||
|
data.runIndex
|
||||||
|
];
|
||||||
|
if (!selectedNodeRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
selectedRun.value = [
|
selectedRun.value = [
|
||||||
{
|
{
|
||||||
node: data.node,
|
node: data.node,
|
||||||
runIndex: data.runIndex,
|
runIndex: data.runIndex,
|
||||||
data: getReferencedData(
|
data: getReferencedData(selectedNodeRun, true, true),
|
||||||
{
|
|
||||||
node: data.node,
|
|
||||||
runIndex: data.runIndex,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -145,21 +136,20 @@ const createNode = (
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
||||||
const { connectionsByDestinationNode } = workflowsStore.getCurrentWorkflow();
|
const connections = props.workflow.connectionsByDestinationNode[nodeName];
|
||||||
const connections = connectionsByDestinationNode[nodeName];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
||||||
const resultData = aiData.value?.filter((data) => data.node === nodeName) ?? [];
|
const resultData = aiData.value?.filter((data) => data.node === nodeName) ?? [];
|
||||||
|
|
||||||
if (!connections) {
|
if (!connections) {
|
||||||
return resultData.map((d) => createNode(nodeName, currentDepth, d));
|
return resultData.map((d) => createNode(nodeName, currentDepth, d));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonMainConnectionsKeys = Object.keys(connections).filter(
|
// Get the first level of children
|
||||||
(key) => key !== NodeConnectionType.Main,
|
const connectedSubNodes = props.workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
|
||||||
);
|
|
||||||
const children = nonMainConnectionsKeys.flatMap((key) =>
|
const children = connectedSubNodes
|
||||||
connections[key][0].flatMap((node) => getTreeNodeData(node.node, currentDepth + 1)),
|
// Only include sub-nodes which have data
|
||||||
);
|
.filter((name) => aiData.value?.find((data) => data.node === name))
|
||||||
|
.flatMap((name) => getTreeNodeData(name, currentDepth + 1));
|
||||||
|
|
||||||
children.sort((a, b) => a.startTime - b.startTime);
|
children.sort((a, b) => a.startTime - b.startTime);
|
||||||
|
|
||||||
|
@ -170,35 +160,49 @@ function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
||||||
return [createNode(nodeName, currentDepth, undefined, children)];
|
return [createNode(nodeName, currentDepth, undefined, children)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiData = computed<AIResult[] | undefined>(() => {
|
const aiData = computed<AIResult[]>(() => {
|
||||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
|
const result: AIResult[] = [];
|
||||||
|
const connectedSubNodes = props.workflow.getParentNodes(props.node.name, 'ALL_NON_MAIN');
|
||||||
|
const rootNodeResult = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
|
||||||
|
const rootNodeStartTime = rootNodeResult?.[0]?.startTime ?? 0;
|
||||||
|
const rootNodeEndTime = rootNodeStartTime + (rootNodeResult?.[0]?.executionTime ?? 0);
|
||||||
|
|
||||||
if (!resultData || !Array.isArray(resultData)) {
|
connectedSubNodes.forEach((nodeName) => {
|
||||||
return;
|
const nodeRunData = workflowsStore.getWorkflowResultDataByNodeName(nodeName) ?? [];
|
||||||
}
|
|
||||||
|
|
||||||
const subRun = resultData[props.runIndex].metadata?.subRun;
|
nodeRunData.forEach((run, runIndex) => {
|
||||||
if (!Array.isArray(subRun)) {
|
const referenceData = {
|
||||||
return;
|
data: getReferencedData(run, false, true)[0],
|
||||||
}
|
node: nodeName,
|
||||||
// Extend the subRun with the data and sort by adding execution time + startTime and comparing them
|
runIndex,
|
||||||
const subRunWithData = subRun.flatMap((run) =>
|
};
|
||||||
getReferencedData(run, false, true).map((data) => ({ ...run, data })),
|
|
||||||
);
|
|
||||||
|
|
||||||
subRunWithData.sort((a, b) => {
|
result.push(referenceData);
|
||||||
const aTime = a.data?.metadata?.startTime || 0;
|
});
|
||||||
const bTime = b.data?.metadata?.startTime || 0;
|
});
|
||||||
|
|
||||||
|
// Sort the data by start time
|
||||||
|
result.sort((a, b) => {
|
||||||
|
const aTime = a.data?.metadata?.startTime ?? 0;
|
||||||
|
const bTime = b.data?.metadata?.startTime ?? 0;
|
||||||
return aTime - bTime;
|
return aTime - bTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
return subRunWithData;
|
// Only show data that is within the root node's execution time
|
||||||
|
// This is because sub-node could be connected to multiple root nodes
|
||||||
|
const currentNodeResult = result.filter((r) => {
|
||||||
|
const startTime = r.data?.metadata?.startTime ?? 0;
|
||||||
|
|
||||||
|
return startTime >= rootNodeStartTime && startTime <= rootNodeEndTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentNodeResult;
|
||||||
});
|
});
|
||||||
|
|
||||||
const executionTree = computed<TreeNode[]>(() => {
|
const executionTree = computed<TreeNode[]>(() => {
|
||||||
const rootNode = props.node;
|
const rootNode = props.node;
|
||||||
|
|
||||||
const tree = getTreeNodeData(rootNode.name, 0);
|
const tree = getTreeNodeData(rootNode.name, 1);
|
||||||
return tree || [];
|
return tree || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,7 +210,7 @@ watch(() => props.runIndex, selectFirst, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="aiData" :class="$style.container">
|
<div v-if="aiData.length > 0" :class="$style.container">
|
||||||
<div :class="{ [$style.tree]: true, [$style.slim]: slim }">
|
<div :class="{ [$style.tree]: true, [$style.slim]: slim }">
|
||||||
<ElTree
|
<ElTree
|
||||||
:data="executionTree"
|
:data="executionTree"
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { ABOUT_MODAL_KEY, VIEWS } from '@/constants';
|
import { ABOUT_MODAL_KEY, VIEWS } from '@/constants';
|
||||||
import { useUserHelpers } from '@/composables/useUserHelpers';
|
import { useUserHelpers } from '@/composables/useUserHelpers';
|
||||||
import type { IFakeDoor } from '@/Interface';
|
|
||||||
import type { IMenuItem } from 'n8n-design-system';
|
import type { IMenuItem } from 'n8n-design-system';
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
|
@ -26,23 +24,6 @@ const rootStore = useRootStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
|
||||||
const settingsFakeDoorFeatures = computed<IFakeDoor[]>(() =>
|
|
||||||
Object.keys(uiStore.fakeDoorsByLocation)
|
|
||||||
.filter((location: string) => location.includes('settings'))
|
|
||||||
.map((location) => uiStore.fakeDoorsByLocation[location]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelect = (key: string) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'users': // Fakedoor feature added via hooks when user management is disabled on cloud
|
|
||||||
case 'logging':
|
|
||||||
router.push({ name: VIEWS.FAKE_DOOR, params: { featureId: key } }).catch(() => {});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sidebarMenuItems = computed<IMenuItem[]>(() => {
|
const sidebarMenuItems = computed<IMenuItem[]>(() => {
|
||||||
const menuItems: IMenuItem[] = [
|
const menuItems: IMenuItem[] = [
|
||||||
{
|
{
|
||||||
|
@ -122,19 +103,6 @@ const sidebarMenuItems = computed<IMenuItem[]>(() => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const item of settingsFakeDoorFeatures.value) {
|
|
||||||
if (item.uiLocations.includes('settings')) {
|
|
||||||
menuItems.push({
|
|
||||||
id: item.id,
|
|
||||||
icon: item.icon ?? 'question',
|
|
||||||
label: i18n.baseText(item.featureName as BaseTextKey),
|
|
||||||
position: 'top',
|
|
||||||
available: true,
|
|
||||||
activateOnRoutePaths: [`/settings/coming-soon/${item.id}`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
id: 'settings-log-streaming',
|
id: 'settings-log-streaming',
|
||||||
icon: 'sign-in-alt',
|
icon: 'sign-in-alt',
|
||||||
|
@ -159,7 +127,7 @@ const sidebarMenuItems = computed<IMenuItem[]>(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<n8n-menu :items="sidebarMenuItems" @select="handleSelect">
|
<n8n-menu :items="sidebarMenuItems">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="$style.returnButton" data-test-id="settings-back" @click="emit('return')">
|
<div :class="$style.returnButton" data-test-id="settings-back" @click="emit('return')">
|
||||||
<i class="mr-xs">
|
<i class="mr-xs">
|
||||||
|
|
|
@ -1,82 +1,75 @@
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { type PropType, defineComponent } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import TemplateCard from './TemplateCard.vue';
|
import TemplateCard from './TemplateCard.vue';
|
||||||
import type { ITemplatesWorkflow } from '@/Interface';
|
import type { ITemplatesWorkflow } from '@/Interface';
|
||||||
|
|
||||||
export default defineComponent({
|
interface Props {
|
||||||
name: 'TemplateList',
|
workflows?: ITemplatesWorkflow[];
|
||||||
components: {
|
infiniteScrollEnabled?: boolean;
|
||||||
TemplateCard,
|
loading?: boolean;
|
||||||
},
|
useWorkflowButton?: boolean;
|
||||||
props: {
|
totalWorkflows?: number;
|
||||||
infiniteScrollEnabled: {
|
simpleView?: boolean;
|
||||||
type: Boolean,
|
totalCount?: number;
|
||||||
default: false,
|
}
|
||||||
},
|
|
||||||
loading: {
|
const emit = defineEmits<{
|
||||||
type: Boolean,
|
loadMore: [];
|
||||||
},
|
openTemplate: [{ event: MouseEvent; id: number }];
|
||||||
useWorkflowButton: {
|
useWorkflow: [{ event: MouseEvent; id: number }];
|
||||||
type: Boolean,
|
}>();
|
||||||
default: false,
|
|
||||||
},
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
workflows: {
|
infiniteScrollEnabled: false,
|
||||||
type: Array as PropType<ITemplatesWorkflow[]>,
|
loading: false,
|
||||||
default: () => [],
|
useWorkflowButton: false,
|
||||||
},
|
workflows: () => [],
|
||||||
totalWorkflows: {
|
totalWorkflows: 0,
|
||||||
type: Number,
|
simpleView: false,
|
||||||
default: 0,
|
totalCount: 0,
|
||||||
},
|
});
|
||||||
simpleView: {
|
|
||||||
type: Boolean,
|
const loader = ref<HTMLElement | null>(null);
|
||||||
default: false,
|
|
||||||
},
|
onMounted(() => {
|
||||||
totalCount: {
|
if (props.infiniteScrollEnabled) {
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.infiniteScrollEnabled) {
|
|
||||||
const content = document.getElementById('content');
|
|
||||||
if (content) {
|
|
||||||
content.addEventListener('scroll', this.onScroll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
const content = document.getElementById('content');
|
const content = document.getElementById('content');
|
||||||
if (content) {
|
if (content) {
|
||||||
content.removeEventListener('scroll', this.onScroll);
|
content.addEventListener('scroll', onScroll);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
onScroll() {
|
|
||||||
const loaderRef = this.$refs.loader as HTMLElement | undefined;
|
|
||||||
if (!loaderRef || this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = loaderRef.getBoundingClientRect();
|
|
||||||
const inView =
|
|
||||||
rect.top >= 0 &&
|
|
||||||
rect.left >= 0 &&
|
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
||||||
|
|
||||||
if (inView) {
|
|
||||||
this.$emit('loadMore');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCardClick(event: MouseEvent, id: number) {
|
|
||||||
this.$emit('openTemplate', { event, id });
|
|
||||||
},
|
|
||||||
onUseWorkflow(event: MouseEvent, id: number) {
|
|
||||||
this.$emit('useWorkflow', { event, id });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const content = document.getElementById('content');
|
||||||
|
if (content) {
|
||||||
|
content.removeEventListener('scroll', onScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
const loaderRef = loader.value as HTMLElement | undefined;
|
||||||
|
if (!loaderRef || props.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = loaderRef.getBoundingClientRect();
|
||||||
|
const inView =
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
||||||
|
|
||||||
|
if (inView) {
|
||||||
|
emit('loadMore');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onCardClick(event: MouseEvent, id: number) {
|
||||||
|
emit('openTemplate', { event, id });
|
||||||
|
}
|
||||||
|
function onUseWorkflow(event: MouseEvent, id: number) {
|
||||||
|
emit('useWorkflow', { event, id });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,27 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import ModalDrawer from './ModalDrawer.vue';
|
import ModalDrawer from './ModalDrawer.vue';
|
||||||
import TimeAgo from './TimeAgo.vue';
|
import TimeAgo from './TimeAgo.vue';
|
||||||
import VersionCard from './VersionCard.vue';
|
import VersionCard from './VersionCard.vue';
|
||||||
import { VERSIONS_MODAL_KEY } from '../constants';
|
import { VERSIONS_MODAL_KEY } from '../constants';
|
||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
const versionsStore = useVersionsStore();
|
const versionsStore = useVersionsStore();
|
||||||
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const nextVersions = computed(() => {
|
|
||||||
return versionsStore.nextVersions;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentVersion = computed(() => {
|
|
||||||
return versionsStore.currentVersion;
|
|
||||||
});
|
|
||||||
|
|
||||||
const infoUrl = computed(() => {
|
|
||||||
return versionsStore.infoUrl;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -38,22 +27,22 @@ const infoUrl = computed(() => {
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<section :class="$style['description']">
|
<section :class="$style['description']">
|
||||||
<p v-if="currentVersion">
|
<p v-if="versionsStore.currentVersion">
|
||||||
{{
|
{{
|
||||||
i18n.baseText('updatesPanel.youReOnVersion', {
|
i18n.baseText('updatesPanel.youReOnVersion', {
|
||||||
interpolate: { currentVersionName: currentVersion.name },
|
interpolate: { currentVersionName: versionsStore.currentVersion.name },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
<strong>
|
<strong>
|
||||||
<TimeAgo :date="currentVersion.createdAt" />
|
<TimeAgo :date="versionsStore.currentVersion.createdAt" />
|
||||||
</strong>
|
</strong>
|
||||||
{{ i18n.baseText('updatesPanel.andIs') }}
|
{{ i18n.baseText('updatesPanel.andIs') }}
|
||||||
<strong>
|
<strong>
|
||||||
{{
|
{{
|
||||||
i18n.baseText('updatesPanel.version', {
|
i18n.baseText('updatesPanel.version', {
|
||||||
interpolate: {
|
interpolate: {
|
||||||
numberOfVersions: nextVersions.length,
|
numberOfVersions: versionsStore.nextVersions.length,
|
||||||
howManySuffix: nextVersions.length > 1 ? 's' : '',
|
howManySuffix: versionsStore.nextVersions.length > 1 ? 's' : '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
@ -61,15 +50,27 @@ const infoUrl = computed(() => {
|
||||||
{{ i18n.baseText('updatesPanel.behindTheLatest') }}
|
{{ i18n.baseText('updatesPanel.behindTheLatest') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<n8n-link v-if="infoUrl" :to="infoUrl" :bold="true">
|
<n8n-button
|
||||||
|
v-if="versionsStore.infoUrl"
|
||||||
|
:text="true"
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:class="$style['link']"
|
||||||
|
:bold="true"
|
||||||
|
@click="pageRedirectionHelper.goToVersions()"
|
||||||
|
>
|
||||||
<font-awesome-icon icon="info-circle" class="mr-2xs" />
|
<font-awesome-icon icon="info-circle" class="mr-2xs" />
|
||||||
<span>
|
<span>
|
||||||
{{ i18n.baseText('updatesPanel.howToUpdateYourN8nVersion') }}
|
{{ i18n.baseText('updatesPanel.howToUpdateYourN8nVersion') }}
|
||||||
</span>
|
</span>
|
||||||
</n8n-link>
|
</n8n-button>
|
||||||
</section>
|
</section>
|
||||||
<section :class="$style.versions">
|
<section :class="$style.versions">
|
||||||
<div v-for="version in nextVersions" :key="version.name" :class="$style['versions-card']">
|
<div
|
||||||
|
v-for="version in versionsStore.nextVersions"
|
||||||
|
:key="version.name"
|
||||||
|
:class="$style['versions-card']"
|
||||||
|
>
|
||||||
<VersionCard :version="version" />
|
<VersionCard :version="version" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -102,6 +103,15 @@ const infoUrl = computed(() => {
|
||||||
div {
|
div {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
color: var(--prim-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.versions {
|
.versions {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import VariablesRow from '../VariablesRow.vue';
|
import VariablesRow from './VariablesRow.vue';
|
||||||
import { fireEvent } from '@testing-library/vue';
|
import { fireEvent } from '@testing-library/vue';
|
||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
import { afterAll, beforeAll } from 'vitest';
|
import { afterAll, beforeAll } from 'vitest';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue