mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20: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_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||
export const WEBHOOK_NODE_NAME = 'Webhook';
|
||||
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
|
||||
|
||||
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,
|
||||
INodeTypeDescription,
|
||||
SupplyData,
|
||||
INodeParameterResourceLocator,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers';
|
||||
|
@ -41,7 +42,7 @@ export class RetrieverWorkflow implements INodeType {
|
|||
name: 'retrieverWorkflow',
|
||||
icon: 'fa:box-open',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
description: 'Use an n8n Workflow as Retriever',
|
||||
defaults: {
|
||||
name: 'Workflow Retriever',
|
||||
|
@ -105,12 +106,26 @@ export class RetrieverWorkflow implements INodeType {
|
|||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [{ _cnd: { eq: 1 } }],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
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
|
||||
|
@ -301,11 +316,21 @@ export class RetrieverWorkflow implements INodeType {
|
|||
|
||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||
if (source === 'database') {
|
||||
// Read workflow from database
|
||||
workflowInfo.id = this.executeFunctions.getNodeParameter(
|
||||
'workflowId',
|
||||
itemIndex,
|
||||
) as string;
|
||||
const nodeVersion = this.executeFunctions.getNode().typeVersion;
|
||||
if (nodeVersion === 1) {
|
||||
workflowInfo.id = this.executeFunctions.getNodeParameter(
|
||||
'workflowId',
|
||||
itemIndex,
|
||||
) as string;
|
||||
} else {
|
||||
const { value } = this.executeFunctions.getNodeParameter(
|
||||
'workflowId',
|
||||
itemIndex,
|
||||
{},
|
||||
) as INodeParameterResourceLocator;
|
||||
workflowInfo.id = value as string;
|
||||
}
|
||||
|
||||
baseMetadata.workflowId = workflowInfo.id;
|
||||
} else if (source === 'parameter') {
|
||||
// Read workflow from parameter
|
||||
|
|
|
@ -8,6 +8,7 @@ import type {
|
|||
SupplyData,
|
||||
ExecutionError,
|
||||
IDataObject,
|
||||
INodeParameterResourceLocator,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
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',
|
||||
icon: 'fa:network-wired',
|
||||
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.',
|
||||
defaults: {
|
||||
name: 'Call n8n Workflow Tool',
|
||||
|
@ -142,6 +143,7 @@ export class ToolWorkflow implements INodeType {
|
|||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [{ _cnd: { lte: 1.1 } }],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
@ -150,6 +152,20 @@ export class ToolWorkflow implements INodeType {
|
|||
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
|
||||
// ----------------------------------
|
||||
|
@ -368,7 +384,17 @@ export class ToolWorkflow implements INodeType {
|
|||
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||
if (source === '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') {
|
||||
// Read workflow from parameter
|
||||
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle">
|
||||
<ResourceLocator
|
||||
v-if="isResourceLocatorParameter"
|
||||
v-if="parameter.type === 'resourceLocator'"
|
||||
ref="resourceLocator"
|
||||
:parameter="parameter"
|
||||
:model-value="modelValueResourceLocator"
|
||||
|
@ -36,6 +36,25 @@
|
|||
@blur="onBlur"
|
||||
@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
|
||||
v-else-if="isModelValueExpression || forceShowExpression"
|
||||
ref="inputField"
|
||||
|
@ -939,7 +958,7 @@ const shortPath = computed<string>(() => {
|
|||
});
|
||||
|
||||
const isResourceLocatorParameter = computed<boolean>(() => {
|
||||
return props.parameter.type === 'resourceLocator';
|
||||
return props.parameter.type === 'resourceLocator' || props.parameter.type === 'workflowSelector';
|
||||
});
|
||||
|
||||
const isSecretParameter = computed<boolean>(() => {
|
||||
|
|
|
@ -137,7 +137,9 @@ const node = computed(() => ndvStore.activeNode);
|
|||
const hint = computed(() => i18n.nodeText().hint(props.parameter, props.path));
|
||||
const isInputTypeString = computed(() => props.parameter.type === 'string');
|
||||
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(
|
||||
() =>
|
||||
props.parameter.noDataExpression ||
|
||||
|
|
|
@ -828,125 +828,5 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
$--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;
|
||||
}
|
||||
@import './resourceLocator.scss';
|
||||
</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',
|
||||
iconColor: 'orange-red',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
|
||||
description: 'Execute another workflow',
|
||||
defaults: {
|
||||
|
@ -79,6 +79,7 @@ export class ExecuteWorkflow implements INodeType {
|
|||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
@ -87,7 +88,20 @@ export class ExecuteWorkflow implements INodeType {
|
|||
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>.",
|
||||
},
|
||||
|
||||
{
|
||||
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
|
||||
// ----------------------------------
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
import { readFile as fsReadFile } from 'fs/promises';
|
||||
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) {
|
||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
if (source === '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') {
|
||||
// Read workflow from filesystem
|
||||
const workflowPath = this.getNodeParameter('workflowPath', itemIndex) as string;
|
||||
|
|
|
@ -1204,7 +1204,8 @@ export type NodePropertyTypes =
|
|||
| 'resourceMapper'
|
||||
| 'filter'
|
||||
| 'assignmentCollection'
|
||||
| 'credentials';
|
||||
| 'credentials'
|
||||
| 'workflowSelector';
|
||||
|
||||
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
||||
|
||||
|
|
|
@ -1580,7 +1580,7 @@ export function addToIssuesIfMissing(
|
|||
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
|
||||
(nodeProperties.type === 'dateTime' && value === undefined) ||
|
||||
(nodeProperties.type === 'options' && (value === '' || value === undefined)) ||
|
||||
(nodeProperties.type === 'resourceLocator' &&
|
||||
((nodeProperties.type === 'resourceLocator' || nodeProperties.type === 'workflowSelector') &&
|
||||
!isValidResourceLocatorParameterValue(value as INodeParameterResourceLocator))
|
||||
) {
|
||||
// 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);
|
||||
if (isINodeParameterResourceLocator(value)) {
|
||||
const mode = nodeProperties.modes?.find((option) => option.name === value.mode);
|
||||
|
|
Loading…
Reference in a new issue