mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat(editor): Implement workflowSelector parameter type (#10482)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
This commit is contained in:
parent
a73b9a38d6
commit
84e54beac7
|
@ -61,6 +61,7 @@ export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model'
|
||||||
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
|
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
|
||||||
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||||
export const WEBHOOK_NODE_NAME = 'Webhook';
|
export const WEBHOOK_NODE_NAME = 'Webhook';
|
||||||
|
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
|
||||||
|
|
||||||
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';
|
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';
|
||||||
|
|
||||||
|
|
82
cypress/e2e/45-workflow-selector-parameter.cy.ts
Normal file
82
cypress/e2e/45-workflow-selector-parameter.cy.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { EXECUTE_WORKFLOW_NODE_NAME } from '../constants';
|
||||||
|
import { WorkflowPage as WorkflowPageClass, NDV } from '../pages';
|
||||||
|
import { getVisiblePopper } from '../utils';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPageClass();
|
||||||
|
const ndv = new NDV();
|
||||||
|
|
||||||
|
describe('Workflow Selector Parameter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetDatabase();
|
||||||
|
cy.signinAsOwner();
|
||||||
|
['Get_Weather', 'Search_DB'].forEach((workflowName) => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
cy.createFixtureWorkflow(`Test_Subworkflow_${workflowName}.json`, workflowName);
|
||||||
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
});
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, {
|
||||||
|
keepNdvOpen: true,
|
||||||
|
action: 'Call Another Workflow',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should render sub-workflows list', () => {
|
||||||
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
|
getVisiblePopper()
|
||||||
|
.should('have.length', 1)
|
||||||
|
.findChildByTestId('rlc-item')
|
||||||
|
.should('have.length', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show required parameter warning', () => {
|
||||||
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
ndv.getters.parameterInputIssues('workflowId').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter sub-workflows list', () => {
|
||||||
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
ndv.getters.resourceLocatorSearch('workflowId').type('Weather');
|
||||||
|
|
||||||
|
getVisiblePopper()
|
||||||
|
.should('have.length', 1)
|
||||||
|
.findChildByTestId('rlc-item')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
ndv.getters
|
||||||
|
.resourceLocatorInput('workflowId')
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'Get_Weather');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render sub-workflow links correctly', () => {
|
||||||
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
|
getVisiblePopper().findChildByTestId('rlc-item').first().click();
|
||||||
|
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
|
||||||
|
cy.getByTestId('radio-button-expression').eq(1).click();
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').find('a').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch to ID mode on expression', () => {
|
||||||
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
|
getVisiblePopper().findChildByTestId('rlc-item').first().click();
|
||||||
|
ndv.getters
|
||||||
|
.resourceLocatorModeSelector('workflowId')
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'From list');
|
||||||
|
cy.getByTestId('radio-button-expression').eq(1).click();
|
||||||
|
ndv.getters
|
||||||
|
.resourceLocatorModeSelector('workflowId')
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'By ID');
|
||||||
|
});
|
||||||
|
});
|
53
cypress/fixtures/Test_Subworkflow_Get_Weather.json
Normal file
53
cypress/fixtures/Test_Subworkflow_Get_Weather.json
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"name": "Get Weather",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "82eed1ba-179b-4f8f-8a85-b45f0d4e5857",
|
||||||
|
"name": "Execute Workflow Trigger",
|
||||||
|
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
560,
|
||||||
|
340
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
|
||||||
|
"name": "response",
|
||||||
|
"value": "Weather is sunny",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "8f3e00f6-fc92-4aba-817b-93d206158bda",
|
||||||
|
"name": "Edit Fields",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
780,
|
||||||
|
340
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Execute Workflow Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
cypress/fixtures/Test_Subworkflow_Search_DB.json
Normal file
64
cypress/fixtures/Test_Subworkflow_Search_DB.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"name": "Search DB",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "64465f9b-63de-43f9-8d90-b5b2eb7a2dc7",
|
||||||
|
"name": "Execute Workflow Trigger",
|
||||||
|
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
640,
|
||||||
|
380
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
|
||||||
|
"name": "response",
|
||||||
|
"value": "10 results found",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "b580fd2b-00c8-4a52-8acb-024f204c0947",
|
||||||
|
"name": "Edit Fields",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
860,
|
||||||
|
380
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Execute Workflow Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "6026f7a4-f5dc-4c27-9f83-3a02fc6e33ae",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||||
|
},
|
||||||
|
"id": "BFFhCdBZmNSkx4qf",
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
SupplyData,
|
SupplyData,
|
||||||
|
INodeParameterResourceLocator,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers';
|
import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers';
|
||||||
|
@ -41,7 +42,7 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
name: 'retrieverWorkflow',
|
name: 'retrieverWorkflow',
|
||||||
icon: 'fa:box-open',
|
icon: 'fa:box-open',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: [1, 1.1],
|
||||||
description: 'Use an n8n Workflow as Retriever',
|
description: 'Use an n8n Workflow as Retriever',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Workflow Retriever',
|
name: 'Workflow Retriever',
|
||||||
|
@ -105,12 +106,26 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
source: ['database'],
|
source: ['database'],
|
||||||
|
'@version': [{ _cnd: { eq: 1 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'The workflow to execute',
|
description: 'The workflow to execute',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Workflow',
|
||||||
|
name: 'workflowId',
|
||||||
|
type: 'workflowSelector',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
source: ['database'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// source:parameter
|
// source:parameter
|
||||||
|
@ -301,11 +316,21 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
|
|
||||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||||
if (source === 'database') {
|
if (source === 'database') {
|
||||||
// Read workflow from database
|
const nodeVersion = this.executeFunctions.getNode().typeVersion;
|
||||||
workflowInfo.id = this.executeFunctions.getNodeParameter(
|
if (nodeVersion === 1) {
|
||||||
'workflowId',
|
workflowInfo.id = this.executeFunctions.getNodeParameter(
|
||||||
itemIndex,
|
'workflowId',
|
||||||
) as string;
|
itemIndex,
|
||||||
|
) as string;
|
||||||
|
} else {
|
||||||
|
const { value } = this.executeFunctions.getNodeParameter(
|
||||||
|
'workflowId',
|
||||||
|
itemIndex,
|
||||||
|
{},
|
||||||
|
) as INodeParameterResourceLocator;
|
||||||
|
workflowInfo.id = value as string;
|
||||||
|
}
|
||||||
|
|
||||||
baseMetadata.workflowId = workflowInfo.id;
|
baseMetadata.workflowId = workflowInfo.id;
|
||||||
} else if (source === 'parameter') {
|
} else if (source === 'parameter') {
|
||||||
// Read workflow from parameter
|
// Read workflow from parameter
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type {
|
||||||
SupplyData,
|
SupplyData,
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
INodeParameterResourceLocator,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
|
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||||
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
|
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
|
||||||
|
@ -32,7 +33,7 @@ export class ToolWorkflow implements INodeType {
|
||||||
name: 'toolWorkflow',
|
name: 'toolWorkflow',
|
||||||
icon: 'fa:network-wired',
|
icon: 'fa:network-wired',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: [1, 1.1],
|
version: [1, 1.1, 1.2],
|
||||||
description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.',
|
description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Call n8n Workflow Tool',
|
name: 'Call n8n Workflow Tool',
|
||||||
|
@ -142,6 +143,7 @@ export class ToolWorkflow implements INodeType {
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
source: ['database'],
|
source: ['database'],
|
||||||
|
'@version': [{ _cnd: { lte: 1.1 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -150,6 +152,20 @@ export class ToolWorkflow implements INodeType {
|
||||||
hint: 'Can be found in the URL of the workflow',
|
hint: 'Can be found in the URL of the workflow',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Workflow',
|
||||||
|
name: 'workflowId',
|
||||||
|
type: 'workflowSelector',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
source: ['database'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// source:parameter
|
// source:parameter
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -368,7 +384,17 @@ export class ToolWorkflow implements INodeType {
|
||||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||||
if (source === 'database') {
|
if (source === 'database') {
|
||||||
// Read workflow from database
|
// Read workflow from database
|
||||||
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
|
if (nodeVersion <= 1.1) {
|
||||||
|
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
||||||
|
} else {
|
||||||
|
const { value } = this.getNodeParameter(
|
||||||
|
'workflowId',
|
||||||
|
itemIndex,
|
||||||
|
{},
|
||||||
|
) as INodeParameterResourceLocator;
|
||||||
|
workflowInfo.id = value as string;
|
||||||
|
}
|
||||||
} else if (source === 'parameter') {
|
} else if (source === 'parameter') {
|
||||||
// Read workflow from parameter
|
// Read workflow from parameter
|
||||||
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle">
|
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle">
|
||||||
<ResourceLocator
|
<ResourceLocator
|
||||||
v-if="isResourceLocatorParameter"
|
v-if="parameter.type === 'resourceLocator'"
|
||||||
ref="resourceLocator"
|
ref="resourceLocator"
|
||||||
:parameter="parameter"
|
:parameter="parameter"
|
||||||
:model-value="modelValueResourceLocator"
|
:model-value="modelValueResourceLocator"
|
||||||
|
@ -36,6 +36,25 @@
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@drop="onResourceLocatorDrop"
|
@drop="onResourceLocatorDrop"
|
||||||
/>
|
/>
|
||||||
|
<WorkflowSelectorParameterInput
|
||||||
|
v-else-if="parameter.type === 'workflowSelector'"
|
||||||
|
ref="resourceLocator"
|
||||||
|
:parameter="parameter"
|
||||||
|
:model-value="modelValueResourceLocator"
|
||||||
|
:dependent-parameters-values="dependentParametersValues"
|
||||||
|
:display-title="displayTitle"
|
||||||
|
:expression-display-value="expressionDisplayValue"
|
||||||
|
:expression-computed-value="expressionEvaluated"
|
||||||
|
:is-value-expression="isModelValueExpression"
|
||||||
|
:expression-edit-dialog-visible="expressionEditDialogVisible"
|
||||||
|
:path="path"
|
||||||
|
:parameter-issues="getIssues"
|
||||||
|
@update:model-value="valueChanged"
|
||||||
|
@modal-opener-click="openExpressionEditorModal"
|
||||||
|
@focus="setFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
@drop="onResourceLocatorDrop"
|
||||||
|
/>
|
||||||
<ExpressionParameterInput
|
<ExpressionParameterInput
|
||||||
v-else-if="isModelValueExpression || forceShowExpression"
|
v-else-if="isModelValueExpression || forceShowExpression"
|
||||||
ref="inputField"
|
ref="inputField"
|
||||||
|
@ -939,7 +958,7 @@ const shortPath = computed<string>(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isResourceLocatorParameter = computed<boolean>(() => {
|
const isResourceLocatorParameter = computed<boolean>(() => {
|
||||||
return props.parameter.type === 'resourceLocator';
|
return props.parameter.type === 'resourceLocator' || props.parameter.type === 'workflowSelector';
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSecretParameter = computed<boolean>(() => {
|
const isSecretParameter = computed<boolean>(() => {
|
||||||
|
|
|
@ -137,7 +137,9 @@ const node = computed(() => ndvStore.activeNode);
|
||||||
const hint = computed(() => i18n.nodeText().hint(props.parameter, props.path));
|
const hint = computed(() => i18n.nodeText().hint(props.parameter, props.path));
|
||||||
const isInputTypeString = computed(() => props.parameter.type === 'string');
|
const isInputTypeString = computed(() => props.parameter.type === 'string');
|
||||||
const isInputTypeNumber = computed(() => props.parameter.type === 'number');
|
const isInputTypeNumber = computed(() => props.parameter.type === 'number');
|
||||||
const isResourceLocator = computed(() => props.parameter.type === 'resourceLocator');
|
const isResourceLocator = computed(
|
||||||
|
() => props.parameter.type === 'resourceLocator' || props.parameter.type === 'workflowSelector',
|
||||||
|
);
|
||||||
const isDropDisabled = computed(
|
const isDropDisabled = computed(
|
||||||
() =>
|
() =>
|
||||||
props.parameter.noDataExpression ||
|
props.parameter.noDataExpression ||
|
||||||
|
|
|
@ -828,125 +828,5 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
$--mode-selector-width: 92px;
|
@import './resourceLocator.scss';
|
||||||
|
|
||||||
.modeSelector {
|
|
||||||
--input-background-color: initial;
|
|
||||||
--input-font-color: initial;
|
|
||||||
--input-border-color: initial;
|
|
||||||
flex-basis: $--mode-selector-width;
|
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: var(--border-radius-base) 0 0 var(--border-radius-base);
|
|
||||||
border-right: none;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-right: var(--border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resourceLocator {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
--input-issues-width: 28px;
|
|
||||||
|
|
||||||
.inputContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
--input-border-top-left-radius: 0;
|
|
||||||
--input-border-bottom-left-radius: 0;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.background {
|
|
||||||
position: absolute;
|
|
||||||
background-color: var(--color-background-input-triple);
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: var(--input-issues-width);
|
|
||||||
border: 1px solid var(--border-color-base);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.multipleModes {
|
|
||||||
.inputContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-basis: calc(100% - $--mode-selector-width);
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.droppable {
|
|
||||||
--input-border-color: var(--color-secondary-tint-1);
|
|
||||||
--input-border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activeDrop {
|
|
||||||
--input-border-color: var(--color-success);
|
|
||||||
--input-background-color: var(--color-success-tint-2);
|
|
||||||
--input-border-style: solid;
|
|
||||||
|
|
||||||
textarea,
|
|
||||||
input {
|
|
||||||
cursor: grabbing !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectInput input {
|
|
||||||
padding-right: 30px !important;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectIcon {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
transform: rotateZ(0);
|
|
||||||
|
|
||||||
&.isReverse {
|
|
||||||
transform: rotateZ(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.listModeInputContainer * {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
max-width: 170px;
|
|
||||||
word-break: normal;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openResourceLink {
|
|
||||||
width: 25px !important;
|
|
||||||
padding-left: var(--spacing-2xs);
|
|
||||||
padding-top: var(--spacing-4xs);
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter-issues {
|
|
||||||
width: 25px !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
$--mode-selector-width: 92px;
|
||||||
|
|
||||||
|
.modeSelector {
|
||||||
|
--input-background-color: initial;
|
||||||
|
--input-font-color: initial;
|
||||||
|
--input-border-color: initial;
|
||||||
|
flex-basis: $--mode-selector-width;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: var(--border-radius-base) 0 0 var(--border-radius-base);
|
||||||
|
border-right: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-right: var(--border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resourceLocator {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
--input-issues-width: 28px;
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
--input-border-top-left-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-background-input-triple);
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: var(--input-issues-width);
|
||||||
|
border: 1px solid var(--border-color-base);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multipleModes {
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-basis: calc(100% - $--mode-selector-width);
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.droppable {
|
||||||
|
--input-border-color: var(--color-secondary-tint-1);
|
||||||
|
--input-border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeDrop {
|
||||||
|
--input-border-color: var(--color-success);
|
||||||
|
--input-background-color: var(--color-success-tint-2);
|
||||||
|
--input-border-style: solid;
|
||||||
|
|
||||||
|
textarea,
|
||||||
|
input {
|
||||||
|
cursor: grabbing !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectInput input {
|
||||||
|
padding-right: 30px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectIcon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
transform: rotateZ(0);
|
||||||
|
|
||||||
|
&.isReverse {
|
||||||
|
transform: rotateZ(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.listModeInputContainer * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
max-width: 170px;
|
||||||
|
word-break: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openResourceLink {
|
||||||
|
width: 25px !important;
|
||||||
|
padding-left: var(--spacing-2xs);
|
||||||
|
padding-top: var(--spacing-4xs);
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-issues {
|
||||||
|
width: 25px !important;
|
||||||
|
}
|
|
@ -0,0 +1,315 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ComponentInstance } from 'vue';
|
||||||
|
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import type { EventBus } from 'n8n-design-system/utils';
|
||||||
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
|
import type {
|
||||||
|
INodeParameterResourceLocator,
|
||||||
|
INodeProperties,
|
||||||
|
NodeParameterValue,
|
||||||
|
ResourceLocatorModes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import ResourceLocatorDropdown from '@/components/ResourceLocator/ResourceLocatorDropdown.vue';
|
||||||
|
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||||
|
import { onClickOutside } from '@vueuse/core';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useWorkflowResourceLocatorDropdown } from './useWorkflowResourceLocatorDropdown';
|
||||||
|
import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorModes';
|
||||||
|
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: INodeParameterResourceLocator;
|
||||||
|
eventBus?: EventBus;
|
||||||
|
inputSize: 'small' | 'mini' | 'medium' | 'large' | 'xlarge';
|
||||||
|
isValueExpression?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
path: string;
|
||||||
|
expressionDisplayValue?: string;
|
||||||
|
forceShowExpression?: boolean;
|
||||||
|
parameterIssues?: string[];
|
||||||
|
parameter: INodeProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
eventBus: () => createEventBus(),
|
||||||
|
inputSize: 'small',
|
||||||
|
isValueExpression: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
forceShowExpression: false,
|
||||||
|
expressionDisplayValue: '',
|
||||||
|
parameterIssues: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: INodeParameterResourceLocator];
|
||||||
|
drop: [data: string];
|
||||||
|
modalOpenerClick: [];
|
||||||
|
focus: [];
|
||||||
|
blur: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const container = ref<HTMLDivElement>();
|
||||||
|
const dropdown = ref<ComponentInstance<typeof ResourceLocatorDropdown>>();
|
||||||
|
|
||||||
|
const width = ref(0);
|
||||||
|
const inputRef = ref<HTMLInputElement | undefined>();
|
||||||
|
|
||||||
|
const { isListMode, getUpdatedModePayload, selectedMode, supportedModes, getModeLabel } =
|
||||||
|
useWorkflowResourceLocatorModes(
|
||||||
|
computed(() => props.modelValue),
|
||||||
|
router,
|
||||||
|
);
|
||||||
|
const { hideDropdown, isDropdownVisible, showDropdown } = useWorkflowResourceLocatorDropdown(
|
||||||
|
isListMode,
|
||||||
|
inputRef,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasMoreWorkflowsToLoad,
|
||||||
|
isLoadingResources,
|
||||||
|
filteredResources,
|
||||||
|
onSearchFilter,
|
||||||
|
searchFilter,
|
||||||
|
getWorkflowName,
|
||||||
|
populateNextWorkflowsPage,
|
||||||
|
setWorkflowsResources,
|
||||||
|
getWorkflowUrl,
|
||||||
|
} = useWorkflowResourcesLocator(router);
|
||||||
|
|
||||||
|
const valueToDisplay = computed<NodeParameterValue>(() => {
|
||||||
|
if (typeof props.modelValue !== 'object') {
|
||||||
|
return props.modelValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isListMode.value) {
|
||||||
|
return props.modelValue ? props.modelValue.cachedResultName ?? props.modelValue.value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.modelValue ? props.modelValue.value : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholder = computed(() => {
|
||||||
|
if (isListMode.value) {
|
||||||
|
return i18n.baseText('resourceLocator.mode.list.placeholder');
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.baseText('resourceLocator.id.placeholder');
|
||||||
|
});
|
||||||
|
|
||||||
|
function setWidth() {
|
||||||
|
const containerRef = container.value as HTMLElement | undefined;
|
||||||
|
if (containerRef) {
|
||||||
|
width.value = containerRef?.offsetWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputChange(value: string): void {
|
||||||
|
const params: INodeParameterResourceLocator = { __rl: true, value, mode: selectedMode.value };
|
||||||
|
if (isListMode.value) {
|
||||||
|
const resource = workflowsStore.getWorkflowById(value);
|
||||||
|
if (resource?.name) {
|
||||||
|
params.cachedResultName = getWorkflowName(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit('update:modelValue', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onListItemSelected(value: string) {
|
||||||
|
onInputChange(value);
|
||||||
|
hideDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputFocus(): void {
|
||||||
|
setWidth();
|
||||||
|
showDropdown();
|
||||||
|
emit('focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputBlur(): void {
|
||||||
|
emit('blur');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDrop(data: string) {
|
||||||
|
emit('drop', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onModeSwitched(mode: ResourceLocatorModes) {
|
||||||
|
emit('update:modelValue', getUpdatedModePayload(mode));
|
||||||
|
}
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (isDropdownVisible.value) {
|
||||||
|
props.eventBus.emit('keyDown', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWorkflow() {
|
||||||
|
window.open(getWorkflowUrl(props.modelValue.value?.toString() ?? ''), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', setWidth);
|
||||||
|
setWidth();
|
||||||
|
void setWorkflowsResources();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', setWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isValueExpression,
|
||||||
|
(isValueExpression) => {
|
||||||
|
// Expressions are always in ID mode
|
||||||
|
if (isValueExpression) {
|
||||||
|
onModeSwitched('id');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onClickOutside(dropdown, () => {
|
||||||
|
isDropdownVisible.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="container"
|
||||||
|
:class="$style.container"
|
||||||
|
:data-test-id="`resource-locator-${parameter.name}`"
|
||||||
|
>
|
||||||
|
<ResourceLocatorDropdown
|
||||||
|
ref="dropdown"
|
||||||
|
:show="isDropdownVisible"
|
||||||
|
:filterable="true"
|
||||||
|
:filter-required="false"
|
||||||
|
:resources="filteredResources"
|
||||||
|
:loading="isLoadingResources"
|
||||||
|
:filter="searchFilter"
|
||||||
|
:has-more="hasMoreWorkflowsToLoad"
|
||||||
|
:error-view="false"
|
||||||
|
:width="width"
|
||||||
|
:event-bus="eventBus"
|
||||||
|
@update:model-value="onListItemSelected"
|
||||||
|
@filter="onSearchFilter"
|
||||||
|
@load-more="populateNextWorkflowsPage"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div :class="$style.error" data-test-id="rlc-error-container">
|
||||||
|
<n8n-text color="text-dark" align="center" tag="div">
|
||||||
|
{{ i18n.baseText('resourceLocator.mode.list.error.title') }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
[$style.resourceLocator]: true,
|
||||||
|
[$style.multipleModes]: true,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div :class="$style.background"></div>
|
||||||
|
<div :class="$style.modeSelector">
|
||||||
|
<n8n-select
|
||||||
|
:model-value="selectedMode"
|
||||||
|
:size="inputSize"
|
||||||
|
:disabled="isReadOnly"
|
||||||
|
:placeholder="i18n.baseText('resourceLocator.modeSelector.placeholder')"
|
||||||
|
data-test-id="rlc-mode-selector"
|
||||||
|
@update:model-value="onModeSwitched"
|
||||||
|
>
|
||||||
|
<n8n-option
|
||||||
|
v-for="mode in supportedModes"
|
||||||
|
:key="mode.name"
|
||||||
|
:value="mode.name"
|
||||||
|
:label="getModeLabel(mode)"
|
||||||
|
:disabled="isValueExpression && mode.name === 'list'"
|
||||||
|
:title="
|
||||||
|
isValueExpression && mode.name === 'list'
|
||||||
|
? i18n.baseText('resourceLocator.mode.list.disabled.title')
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ getModeLabel(mode) }}
|
||||||
|
</n8n-option>
|
||||||
|
</n8n-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.inputContainer" data-test-id="rlc-input-container">
|
||||||
|
<DraggableTarget
|
||||||
|
type="mapping"
|
||||||
|
:sticky="true"
|
||||||
|
:sticky-offset="isValueExpression ? [26, 3] : [3, 3]"
|
||||||
|
@drop="onDrop"
|
||||||
|
>
|
||||||
|
<template #default="{ droppable, activeDrop }">
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
[$style.listModeInputContainer]: isListMode,
|
||||||
|
[$style.droppable]: droppable,
|
||||||
|
[$style.activeDrop]: activeDrop,
|
||||||
|
}"
|
||||||
|
@keydown.stop="onKeyDown"
|
||||||
|
>
|
||||||
|
<ExpressionParameterInput
|
||||||
|
v-if="isValueExpression || forceShowExpression"
|
||||||
|
ref="input"
|
||||||
|
:model-value="expressionDisplayValue"
|
||||||
|
:path="path"
|
||||||
|
:rows="3"
|
||||||
|
@update:model-value="onInputChange"
|
||||||
|
@modal-opener-click="emit('modalOpenerClick')"
|
||||||
|
/>
|
||||||
|
<n8n-input
|
||||||
|
v-else
|
||||||
|
ref="input"
|
||||||
|
:class="{ [$style.selectInput]: isListMode }"
|
||||||
|
:size="inputSize"
|
||||||
|
:model-value="valueToDisplay"
|
||||||
|
:disabled="isReadOnly"
|
||||||
|
:readonly="isListMode"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
type="text"
|
||||||
|
data-test-id="rlc-input"
|
||||||
|
@update:model-value="onInputChange"
|
||||||
|
@click="showDropdown"
|
||||||
|
@focus="onInputFocus"
|
||||||
|
@blur="onInputBlur"
|
||||||
|
>
|
||||||
|
<template v-if="isListMode" #suffix>
|
||||||
|
<i
|
||||||
|
:class="{
|
||||||
|
['el-input__icon']: true,
|
||||||
|
['el-icon-arrow-down']: true,
|
||||||
|
[$style.selectIcon]: true,
|
||||||
|
[$style.isReverse]: isDropdownVisible,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</n8n-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DraggableTarget>
|
||||||
|
|
||||||
|
<ParameterIssues
|
||||||
|
v-if="parameterIssues && parameterIssues.length"
|
||||||
|
:issues="parameterIssues"
|
||||||
|
:class="$style['parameter-issues']"
|
||||||
|
/>
|
||||||
|
<div v-if="!isValueExpression && modelValue.value" :class="$style.openResourceLink">
|
||||||
|
<n8n-link theme="text" @click.stop="openWorkflow()">
|
||||||
|
<font-awesome-icon icon="external-link-alt" :title="'Open resource link'" />
|
||||||
|
</n8n-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResourceLocatorDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
@import '@/components/ResourceLocator/resourceLocator.scss';
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
export function useWorkflowResourceLocatorDropdown(
|
||||||
|
isListMode: Ref<boolean>,
|
||||||
|
inputRef: Ref<HTMLInputElement | undefined>,
|
||||||
|
) {
|
||||||
|
const isDropdownVisible = ref(false);
|
||||||
|
const resourceDropdownHiding = ref(false);
|
||||||
|
|
||||||
|
function showDropdown() {
|
||||||
|
if (!isListMode.value || resourceDropdownHiding.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDropdownVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDropdown() {
|
||||||
|
isDropdownVisible.value = false;
|
||||||
|
|
||||||
|
resourceDropdownHiding.value = true;
|
||||||
|
void nextTick(() => {
|
||||||
|
inputRef.value?.blur?.();
|
||||||
|
resourceDropdownHiding.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDropdownVisible,
|
||||||
|
showDropdown,
|
||||||
|
hideDropdown,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import type {
|
||||||
|
INodeParameterResourceLocator,
|
||||||
|
INodePropertyMode,
|
||||||
|
ResourceLocatorModes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import type { Router } from 'vue-router';
|
||||||
|
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
||||||
|
|
||||||
|
export function useWorkflowResourceLocatorModes(
|
||||||
|
modelValue: Ref<INodeParameterResourceLocator>,
|
||||||
|
router: Router,
|
||||||
|
) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const { getWorkflowName } = useWorkflowResourcesLocator(router);
|
||||||
|
|
||||||
|
const supportedModes = computed<INodePropertyMode[]>(() => [
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
displayName: i18n.baseText('resourceLocator.mode.list'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'id',
|
||||||
|
displayName: i18n.baseText('resourceLocator.mode.id'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedMode = computed(() => modelValue.value?.mode || 'list');
|
||||||
|
const isListMode = computed(() => selectedMode.value === 'list');
|
||||||
|
|
||||||
|
function getUpdatedModePayload(value: ResourceLocatorModes): INodeParameterResourceLocator {
|
||||||
|
if (typeof modelValue !== 'object') {
|
||||||
|
return { __rl: true, value: modelValue, mode: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 'id' && selectedMode.value === 'list' && modelValue.value.value) {
|
||||||
|
return { __rl: true, mode: value, value: modelValue.value.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__rl: true,
|
||||||
|
mode: value,
|
||||||
|
value: modelValue.value.value,
|
||||||
|
cachedResultName: getWorkflowName(modelValue.value.value?.toString() ?? ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModeLabel(mode: INodePropertyMode): string | null {
|
||||||
|
if (mode.name === 'id' || mode.name === 'list') {
|
||||||
|
return i18n.baseText(`resourceLocator.mode.${mode.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
supportedModes,
|
||||||
|
selectedMode,
|
||||||
|
isListMode,
|
||||||
|
getUpdatedModePayload,
|
||||||
|
getModeLabel,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { sortBy } from 'lodash-es';
|
||||||
|
import type { Router } from 'vue-router';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
|
||||||
|
export function useWorkflowResourcesLocator(router: Router) {
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const workflowsResources = ref<Array<{ name: string; value: string; url: string }>>([]);
|
||||||
|
const isLoadingResources = ref(true);
|
||||||
|
const searchFilter = ref('');
|
||||||
|
const PAGE_SIZE = 40;
|
||||||
|
|
||||||
|
const sortedWorkflows = computed(() =>
|
||||||
|
sortBy(workflowsStore.allWorkflows, (workflow) =>
|
||||||
|
new Date(workflow.updatedAt).valueOf(),
|
||||||
|
).reverse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasMoreWorkflowsToLoad = computed(
|
||||||
|
() => workflowsStore.allWorkflows.length > workflowsResources.value.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredResources = computed(() => {
|
||||||
|
if (!searchFilter.value) return workflowsResources.value;
|
||||||
|
|
||||||
|
return workflowsStore.allWorkflows
|
||||||
|
.filter((resource) => resource.name.toLowerCase().includes(searchFilter.value.toLowerCase()))
|
||||||
|
.map(workflowDbToResourceMapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function populateNextWorkflowsPage() {
|
||||||
|
if (workflowsStore.allWorkflows.length <= 1) {
|
||||||
|
await workflowsStore.fetchAllWorkflows();
|
||||||
|
}
|
||||||
|
const nextPage = sortedWorkflows.value.slice(
|
||||||
|
workflowsResources.value.length,
|
||||||
|
workflowsResources.value.length + PAGE_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
workflowsResources.value.push(...nextPage.map(workflowDbToResourceMapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setWorkflowsResources() {
|
||||||
|
isLoadingResources.value = true;
|
||||||
|
await populateNextWorkflowsPage();
|
||||||
|
isLoadingResources.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function workflowDbToResourceMapper(workflow: IWorkflowDb) {
|
||||||
|
return {
|
||||||
|
name: getWorkflowName(workflow.id),
|
||||||
|
value: workflow.id,
|
||||||
|
url: getWorkflowUrl(workflow.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowUrl(workflowId: string) {
|
||||||
|
const { href } = router.resolve({ name: VIEWS.WORKFLOW, params: { name: workflowId } });
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowName(id: string): string {
|
||||||
|
const workflow = workflowsStore.getWorkflowById(id);
|
||||||
|
if (workflow) {
|
||||||
|
// Add the project name if it's not a personal project
|
||||||
|
if (workflow.homeProject && workflow.homeProject.type !== 'personal') {
|
||||||
|
return `${workflow.homeProject.name} — ${workflow.name}`;
|
||||||
|
}
|
||||||
|
return workflow.name;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchFilter(filter: string) {
|
||||||
|
searchFilter.value = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflowsResources,
|
||||||
|
isLoadingResources,
|
||||||
|
hasMoreWorkflowsToLoad,
|
||||||
|
filteredResources,
|
||||||
|
searchFilter,
|
||||||
|
getWorkflowUrl,
|
||||||
|
onSearchFilter,
|
||||||
|
getWorkflowName,
|
||||||
|
populateNextWorkflowsPage,
|
||||||
|
setWorkflowsResources,
|
||||||
|
};
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
icon: 'fa:sign-in-alt',
|
icon: 'fa:sign-in-alt',
|
||||||
iconColor: 'orange-red',
|
iconColor: 'orange-red',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: [1, 1.1],
|
||||||
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
|
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
|
||||||
description: 'Execute another workflow',
|
description: 'Execute another workflow',
|
||||||
defaults: {
|
defaults: {
|
||||||
|
@ -79,6 +79,7 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
source: ['database'],
|
source: ['database'],
|
||||||
|
'@version': [1],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -87,7 +88,20 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
description:
|
description:
|
||||||
"Note on using an expression here: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
|
"Note on using an expression here: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Workflow',
|
||||||
|
name: 'workflowId',
|
||||||
|
type: 'workflowSelector',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
source: ['database'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
hint: "Note on using an expression here: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
|
||||||
|
},
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// source:localFile
|
// source:localFile
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
|
@ -1,13 +1,27 @@
|
||||||
import { readFile as fsReadFile } from 'fs/promises';
|
import { readFile as fsReadFile } from 'fs/promises';
|
||||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||||
import type { IExecuteFunctions, IExecuteWorkflowInfo, IRequestOptions } from 'n8n-workflow';
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
IExecuteWorkflowInfo,
|
||||||
|
INodeParameterResourceLocator,
|
||||||
|
IRequestOptions,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export async function getWorkflowInfo(this: IExecuteFunctions, source: string, itemIndex = 0) {
|
export async function getWorkflowInfo(this: IExecuteFunctions, source: string, itemIndex = 0) {
|
||||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||||
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
if (source === 'database') {
|
if (source === 'database') {
|
||||||
// Read workflow from database
|
// Read workflow from database
|
||||||
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
if (nodeVersion === 1) {
|
||||||
|
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
||||||
|
} else {
|
||||||
|
const { value } = this.getNodeParameter(
|
||||||
|
'workflowId',
|
||||||
|
itemIndex,
|
||||||
|
{},
|
||||||
|
) as INodeParameterResourceLocator;
|
||||||
|
workflowInfo.id = value as string;
|
||||||
|
}
|
||||||
} else if (source === 'localFile') {
|
} else if (source === 'localFile') {
|
||||||
// Read workflow from filesystem
|
// Read workflow from filesystem
|
||||||
const workflowPath = this.getNodeParameter('workflowPath', itemIndex) as string;
|
const workflowPath = this.getNodeParameter('workflowPath', itemIndex) as string;
|
||||||
|
|
|
@ -1204,7 +1204,8 @@ export type NodePropertyTypes =
|
||||||
| 'resourceMapper'
|
| 'resourceMapper'
|
||||||
| 'filter'
|
| 'filter'
|
||||||
| 'assignmentCollection'
|
| 'assignmentCollection'
|
||||||
| 'credentials';
|
| 'credentials'
|
||||||
|
| 'workflowSelector';
|
||||||
|
|
||||||
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
||||||
|
|
||||||
|
|
|
@ -1580,7 +1580,7 @@ export function addToIssuesIfMissing(
|
||||||
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
|
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
|
||||||
(nodeProperties.type === 'dateTime' && value === undefined) ||
|
(nodeProperties.type === 'dateTime' && value === undefined) ||
|
||||||
(nodeProperties.type === 'options' && (value === '' || value === undefined)) ||
|
(nodeProperties.type === 'options' && (value === '' || value === undefined)) ||
|
||||||
(nodeProperties.type === 'resourceLocator' &&
|
((nodeProperties.type === 'resourceLocator' || nodeProperties.type === 'workflowSelector') &&
|
||||||
!isValidResourceLocatorParameterValue(value as INodeParameterResourceLocator))
|
!isValidResourceLocatorParameterValue(value as INodeParameterResourceLocator))
|
||||||
) {
|
) {
|
||||||
// Parameter is required but empty
|
// Parameter is required but empty
|
||||||
|
@ -1654,7 +1654,10 @@ export function getParameterIssues(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeProperties.type === 'resourceLocator' && isDisplayed) {
|
if (
|
||||||
|
(nodeProperties.type === 'resourceLocator' || nodeProperties.type === 'workflowSelector') &&
|
||||||
|
isDisplayed
|
||||||
|
) {
|
||||||
const value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
|
const value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
|
||||||
if (isINodeParameterResourceLocator(value)) {
|
if (isINodeParameterResourceLocator(value)) {
|
||||||
const mode = nodeProperties.modes?.find((option) => option.name === value.mode);
|
const mode = nodeProperties.modes?.find((option) => option.name === value.mode);
|
||||||
|
|
Loading…
Reference in a new issue