diff --git a/cypress/constants.ts b/cypress/constants.ts index 5074c41080..7524168535 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -41,7 +41,7 @@ export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const CODE_NODE_NAME = 'Code'; export const SET_NODE_NAME = 'Set'; export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields'; -export const IF_NODE_NAME = 'IF'; +export const IF_NODE_NAME = 'If'; export const MERGE_NODE_NAME = 'Merge'; export const SWITCH_NODE_NAME = 'Switch'; export const GMAIL_NODE_NAME = 'Gmail'; diff --git a/cypress/e2e/30-if-node.cy.ts b/cypress/e2e/30-if-node.cy.ts new file mode 100644 index 0000000000..95ed1e9a0d --- /dev/null +++ b/cypress/e2e/30-if-node.cy.ts @@ -0,0 +1,58 @@ +import { IF_NODE_NAME } from '../constants'; +import { WorkflowPage, NDV } from '../pages'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); + +const FILTER_PARAM_NAME = 'conditions'; + +describe('If Node (filter component)', () => { + beforeEach(() => { + workflowPage.actions.visit(); + }); + + it('should be able to create and delete multiple conditions', () => { + workflowPage.actions.addInitialNodeToCanvas(IF_NODE_NAME, { keepNdvOpen: true }); + + // Default state + ndv.getters.filterComponent(FILTER_PARAM_NAME).should('exist'); + ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 1); + ndv.getters + .filterConditionOperator(FILTER_PARAM_NAME) + .find('input') + .should('have.value', 'is equal to'); + + // Add + ndv.actions.addFilterCondition(FILTER_PARAM_NAME); + ndv.getters.filterConditionLeft(FILTER_PARAM_NAME, 0).find('input').type('first left'); + ndv.getters.filterConditionLeft(FILTER_PARAM_NAME, 1).find('input').type('second left'); + ndv.actions.addFilterCondition(FILTER_PARAM_NAME); + ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 3); + + // Delete + ndv.actions.removeFilterCondition(FILTER_PARAM_NAME, 0); + ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 2); + ndv.getters + .filterConditionLeft(FILTER_PARAM_NAME, 0) + .find('input') + .should('have.value', 'second left'); + ndv.actions.removeFilterCondition(FILTER_PARAM_NAME, 1); + ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 1); + }); + + it('should correctly evaluate conditions', () => { + cy.fixture('Test_workflow_filter.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + + workflowPage.actions.openNode('Then'); + ndv.getters.outputPanel().contains('3 items').should('exist'); + ndv.actions.close(); + + workflowPage.actions.openNode('Else'); + ndv.getters.outputPanel().contains('1 item').should('exist'); + }); +}); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 828fb27cd3..49ff848cfe 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -2,6 +2,7 @@ import { NodeCreator } from '../pages/features/node-creator'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { NDV } from '../pages/ndv'; import { getVisibleSelect } from '../utils'; +import { IF_NODE_NAME } from '../constants'; const nodeCreatorFeature = new NodeCreator(); const WorkflowPage = new WorkflowPageClass(); @@ -360,7 +361,7 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('i'); - nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF'); + nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw'); @@ -368,11 +369,11 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('i'); - nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF'); + nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('IF'); - nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF'); + nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw'); diff --git a/cypress/fixtures/Test_workflow_filter.json b/cypress/fixtures/Test_workflow_filter.json new file mode 100644 index 0000000000..5166ead381 --- /dev/null +++ b/cypress/fixtures/Test_workflow_filter.json @@ -0,0 +1,153 @@ +{ + "name": "Filter test", + "nodes": [ + { + "parameters": {}, + "id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -60, + 480 + ] + }, + { + "parameters": { + "jsCode": "return [\n {\n \"label\": \"Apple\",\n tags: [],\n meta: {foo: 'bar'}\n },\n {\n \"label\": \"Banana\",\n tags: ['exotic'],\n meta: {}\n },\n {\n \"label\": \"Pear\",\n tags: ['other'],\n meta: {}\n },\n {\n \"label\": \"Orange\",\n meta: {}\n }\n]" + }, + "id": "60697c7f-3948-4790-97ba-8aba03d02ac2", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 160, + 480 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "" + }, + "conditions": [ + { + "leftValue": "={{ $json.tags }}", + "rightValue": "exotic", + "operator": { + "type": "array", + "operation": "contains", + "rightType": "any" + } + }, + { + "leftValue": "={{ $json.meta }}", + "rightValue": "", + "operator": { + "type": "object", + "operation": "notEmpty", + "singleValue": true + } + }, + { + "leftValue": "={{ $json.label }}", + "rightValue": "Pea", + "operator": { + "type": "string", + "operation": "startsWith", + "rightType": "string" + } + } + ], + "combinator": "or" + }, + "options": {} + }, + "id": "7531191b-5ac3-45dc-8afb-27ae83d8f33a", + "name": "If", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 380, + 480 + ] + }, + { + "parameters": {}, + "id": "d8c614ea-0bbf-4b12-ad7d-c9ebe09ce583", + "name": "Then", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 600, + 400 + ] + }, + { + "parameters": {}, + "id": "69364770-60d2-4ef4-9f29-9570718a9a10", + "name": "Else", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 600, + 580 + ] + } + ], + "pinData": {}, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ] + ] + }, + "If": { + "main": [ + [ + { + "node": "Then", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Else", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "a6249f48-d88f-4b80-9ed9-79555e522d48", + "id": "BWUTRs5RHxVgQ4uT", + "meta": { + "instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c" + }, + "tags": [] +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 58749721a4..0eaa1361cf 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -49,9 +49,7 @@ export class NDV extends BasePage { parameterExpressionPreview: (parameterName: string) => this.getters .nodeParameters() - .find( - `[data-test-id="parameter-input-${parameterName}"] + [data-test-id="parameter-expression-preview"]`, - ), + .find(`[data-test-id="parameter-expression-preview-${parameterName}"]`), nodeNameContainer: () => cy.getByTestId('node-title-container'), nodeRenameInput: () => cy.getByTestId('node-rename-input'), executePrevious: () => cy.getByTestId('execute-previous-node'), @@ -79,6 +77,23 @@ export class NDV extends BasePage { cy.getByTestId('columns-parameter-input-options-container'), resourceMapperRemoveAllFieldsOption: () => cy.getByTestId('action-removeAllFields'), sqlEditorContainer: () => cy.getByTestId('sql-editor-container'), + filterComponent: (paramName: string) => cy.getByTestId(`filter-${paramName}`), + filterCombinator: (paramName: string, index = 0) => + this.getters.filterComponent(paramName).getByTestId('filter-combinator-select').eq(index), + filterConditions: (paramName: string) => + this.getters.filterComponent(paramName).getByTestId('filter-condition'), + filterCondition: (paramName: string, index = 0) => + this.getters.filterComponent(paramName).getByTestId('filter-condition').eq(index), + filterConditionLeft: (paramName: string, index = 0) => + this.getters.filterComponent(paramName).getByTestId('filter-condition-left').eq(index), + filterConditionRight: (paramName: string, index = 0) => + this.getters.filterComponent(paramName).getByTestId('filter-condition-right').eq(index), + filterConditionOperator: (paramName: string, index = 0) => + this.getters.filterComponent(paramName).getByTestId('filter-operator-select').eq(index), + filterConditionRemove: (paramName: string, index = 0) => + this.getters.filterComponent(paramName).getByTestId('filter-remove-condition').eq(index), + filterConditionAdd: (paramName: string) => + this.getters.filterComponent(paramName).getByTestId('filter-add-condition'), searchInput: () => cy.getByTestId('ndv-search'), pagination: () => cy.getByTestId('ndv-data-pagination'), nodeVersion: () => cy.getByTestId('node-version'), @@ -199,7 +214,6 @@ export class NDV extends BasePage { .find('span') .should('include.html', asEncodedHTML(value)); }, - refreshResourceMapperColumns: () => { this.getters.resourceMapperSelectColumn().realHover(); this.getters @@ -210,7 +224,12 @@ export class NDV extends BasePage { getVisiblePopper().find('li').last().click(); }, - + addFilterCondition: (paramName: string) => { + this.getters.filterConditionAdd(paramName).click(); + }, + removeFilterCondition: (paramName: string, index: number) => { + this.getters.filterConditionRemove(paramName, index).click(); + }, setInvalidExpression: ({ fieldName, invalidExpression, diff --git a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts index b5c68cde63..a213e8ff3b 100644 --- a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts @@ -7,6 +7,8 @@ import { type SupplyData, } from 'n8n-workflow'; +import type { TextSplitter } from 'langchain/text_splitter'; + import { logWrapper } from '../../../utils/logWrapper'; import { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader'; import { getConnectionHintNoticeField, metadataFilterField } from '../../../utils/sharedFields'; @@ -17,7 +19,6 @@ import { getConnectionHintNoticeField, metadataFilterField } from '../../../util import 'mammoth'; // for docx import 'epub2'; // for epub import 'pdf-parse'; // for pdf -import type { TextSplitter } from 'langchain/text_splitter'; export class DocumentBinaryInputLoader implements INodeType { description: INodeTypeDescription = { diff --git a/packages/core/src/ExtractValue.ts b/packages/core/src/ExtractValue.ts index 61833b1502..17959e37e3 100644 --- a/packages/core/src/ExtractValue.ts +++ b/packages/core/src/ExtractValue.ts @@ -1,18 +1,19 @@ -import type { - INode, - INodeParameters, - INodeProperties, - INodePropertyCollection, - INodePropertyOptions, - INodeType, - NodeParameterValueType, -} from 'n8n-workflow'; +import get from 'lodash/get'; import { - NodeOperationError, - NodeHelpers, - LoggerProxy, - WorkflowOperationError, ApplicationError, + LoggerProxy, + NodeHelpers, + NodeOperationError, + WorkflowOperationError, + executeFilter, + isFilterValue, + type INode, + type INodeParameters, + type INodeProperties, + type INodePropertyCollection, + type INodePropertyOptions, + type INodeType, + type NodeParameterValueType, } from 'n8n-workflow'; function findPropertyFromParameterName( @@ -123,6 +124,26 @@ function extractValueRLC( return executeRegexExtractValue(value.value, regex, parameterName, property.displayName); } +function extractValueFilter( + value: NodeParameterValueType | object, + property: INodeProperties, + parameterName: string, + itemIndex: number, +): NodeParameterValueType | object { + if (!isFilterValue(value)) { + return value; + } + + if (property.extractValue?.type) { + throw new ApplicationError( + `Property "${parameterName}" has an invalid extractValue type. Filter parameters only support extractValue: true`, + { extra: { parameter: parameterName } }, + ); + } + + return executeFilter(value, { itemIndex }); +} + function extractValueOther( value: NodeParameterValueType | object, property: INodeProperties | INodePropertyCollection, @@ -162,6 +183,7 @@ export function extractValue( parameterName: string, node: INode, nodeType: INodeType, + itemIndex = 0, ): NodeParameterValueType | object { let property: INodePropertyOptions | INodeProperties | INodePropertyCollection; try { @@ -174,10 +196,12 @@ export function extractValue( if (property.type === 'resourceLocator') { return extractValueRLC(value, property, parameterName); + } else if (property.type === 'filter') { + return extractValueFilter(value, property, parameterName, itemIndex); } return extractValueOther(value, property, parameterName); } catch (error) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - throw new NodeOperationError(node, error); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment + throw new NodeOperationError(node, error, { description: get(error, 'description') }); } } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 6ff2bbd1bd..71ec3a697b 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -2041,12 +2041,9 @@ const validateResourceMapperValue = ( } if (schemaEntry?.type) { - const validationResult = validateFieldType( - key, - resolvedValue, - schemaEntry.type, - schemaEntry.options, - ); + const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, { + valueOptions: schemaEntry.options, + }); if (!validationResult.valid) { return { ...validationResult, fieldName: key }; } else { @@ -2107,12 +2104,9 @@ const validateCollection = ( for (const key of Object.keys(value)) { if (!validationMap[key]) continue; - const fieldValidationResult = validateFieldType( - key, - value[key], - validationMap[key].type, - validationMap[key].options, - ); + const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, { + valueOptions: validationMap[key].options, + }); if (!fieldValidationResult.valid) { throw new ExpressionError( @@ -2270,7 +2264,7 @@ export function getNodeParameter( // This is outside the try/catch because it throws errors with proper messages if (options?.extractValue) { - returnData = extractValue(returnData, parameterName, node, nodeType); + returnData = extractValue(returnData, parameterName, node, nodeType, itemIndex); } // Validate parameter value if it has a schema defined(RMC) or validateType defined diff --git a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue index f029f16722..11278557c3 100644 --- a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue +++ b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue @@ -190,21 +190,20 @@ export default defineComponent({ opacity: 1; } -.heading { - display: flex; -} - .overflow { overflow-x: hidden; overflow-y: clip; } -.small { - margin-bottom: var(--spacing-5xs); -} +.heading { + display: flex; -.medium { - margin-bottom: var(--spacing-2xs); + &.small { + margin-bottom: var(--spacing-5xs); + } + &.medium { + margin-bottom: var(--spacing-2xs); + } } .underline { diff --git a/packages/design-system/src/components/ResizeObserver/ResizeObserver.vue b/packages/design-system/src/components/ResizeObserver/ResizeObserver.vue index 8262bcf577..9f07369f77 100644 --- a/packages/design-system/src/components/ResizeObserver/ResizeObserver.vue +++ b/packages/design-system/src/components/ResizeObserver/ResizeObserver.vue @@ -38,40 +38,49 @@ export default defineComponent({ return; } - const unsortedBreakpoints = [...(this.breakpoints || [])] as Array<{ - width: number; - bp: string; - }>; + const root = this.$refs.root as HTMLDivElement; - const bps = unsortedBreakpoints.sort((a, b) => a.width - b.width); + if (!root) { + return; + } + + this.bp = this.getBreakpointFromWidth(root.offsetWidth); const observer = new ResizeObserver((entries) => { entries.forEach((entry) => { // We wrap it in requestAnimationFrame to avoid this error - ResizeObserver loop limit exceeded requestAnimationFrame(() => { - const newWidth = entry.contentRect.width; - let newBP = 'default'; - for (let i = 0; i < bps.length; i++) { - if (newWidth < bps[i].width) { - newBP = bps[i].bp; - break; - } - } - this.bp = newBP; + this.bp = this.getBreakpointFromWidth(entry.contentRect.width); }); }); }); this.observer = observer; - - if (this.$refs.root) { - observer.observe(this.$refs.root as HTMLDivElement); - } + observer.observe(root); }, beforeUnmount() { if (this.enabled) { this.observer?.disconnect(); } }, + methods: { + getBreakpointFromWidth(width: number): string { + let newBP = 'default'; + const unsortedBreakpoints = [...(this.breakpoints || [])] as Array<{ + width: number; + bp: string; + }>; + + const bps = unsortedBreakpoints.sort((a, b) => a.width - b.width); + for (let i = 0; i < bps.length; i++) { + if (width < bps[i].width) { + newBP = bps[i].bp; + break; + } + } + + return newBP; + }, + }, }); diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts index 202e0d427f..d917339b5e 100644 --- a/packages/design-system/src/components/index.ts +++ b/packages/design-system/src/components/index.ts @@ -50,4 +50,5 @@ export { default as N8nUserStack } from './N8nUserStack'; export { default as N8nUserInfo } from './N8nUserInfo'; export { default as N8nUserSelect } from './N8nUserSelect'; export { default as N8nUsersList } from './N8nUsersList'; +export { default as N8nResizeObserver } from './ResizeObserver'; export { N8nKeyboardShortcut } from './N8nKeyboardShortcut'; diff --git a/packages/design-system/src/css/common/var.scss b/packages/design-system/src/css/common/var.scss index 12d16555b8..cc0687266c 100644 --- a/packages/design-system/src/css/common/var.scss +++ b/packages/design-system/src/css/common/var.scss @@ -399,8 +399,16 @@ $input-placeholder-color: var(--input-placeholder-color, var(--color-text-light) $input-focus-border: var(--input-focus-border-color, var(--color-secondary)); $input-border-color: var(--input-border-color, var(--border-color-base)); $input-border-style: var(--input-border-style, var(--border-style-base)); -$input-border-width: var(--border-width-base); +$input-border-width: var(--input-border-width, var(--border-width-base)); $input-border: $input-border-color $input-border-style $input-border-width; +$input-border-right-color: var( + --input-border-right-color, + var(--input-border-color, var(--border-color-base)) +); +$input-border-bottom-color: var( + --input-border-bottom-color, + var(--input-border-color, var(--border-color-base)) +); $input-font-size: var(--input-font-size, var(--font-size-s)); /// color||Color|0 @@ -411,6 +419,23 @@ $input-width: 140px; $input-height: 40px; /// borderRadius||Border|2 $input-border-radius: var(--input-border-radius, var(--border-radius-base)); +$input-border-top-left-radius: var( + --input-border-top-left-radius, + var(--input-border-radius, var(--border-radius-base)) +); +$input-border-top-right-radius: var( + --input-border-top-right-radius, + var(--input-border-radius, var(--border-radius-base)), +); +$input-border-bottom-left-radius: var( + --input-border-bottom-left-radius, + var(--input-border-radius, var(--border-radius-base)), +); +$input-border-bottom-right-radius: var( + --input-border-bottom-right-radius, + var(--input-border-radius, var(--border-radius-base)), +); +$input-border-radius: var(--input-border-radius, var(--border-radius-base)); $input-border-color-hover: $border-color-hover; /// color||Color|0 $input-background-color: var(--input-background-color, var(--color-foreground-xlight)); diff --git a/packages/design-system/src/css/input.scss b/packages/design-system/src/css/input.scss index cab4d87b28..d388969e41 100644 --- a/packages/design-system/src/css/input.scss +++ b/packages/design-system/src/css/input.scss @@ -20,6 +20,11 @@ background-color: var.$input-background-color; background-image: none; border-radius: var.$input-border-radius; + border-top-left-radius: var.$input-border-top-left-radius; + border-top-right-radius: var.$input-border-top-right-radius; + border-bottom-left-radius: var.$input-border-bottom-left-radius; + border-bottom-right-radius: var.$input-border-bottom-right-radius; + transition: var.$border-transition-base; &, @@ -108,7 +113,13 @@ background-color: var.$input-background-color; background-image: none; border-radius: var.$input-border-radius; + border-top-left-radius: var.$input-border-top-left-radius; + border-top-right-radius: var.$input-border-top-right-radius; + border-bottom-left-radius: var.$input-border-bottom-left-radius; + border-bottom-right-radius: var.$input-border-bottom-right-radius; border: var.$input-border; + border-right-color: var.$input-border-right-color; + border-bottom-color: var.$input-border-bottom-color; box-sizing: border-box; color: var.$input-font-color; display: inline-block; @@ -145,6 +156,7 @@ } @include mixins.e(suffix-inner) { + display: inline-flex; pointer-events: all; } @@ -286,8 +298,14 @@ vertical-align: middle; display: table-cell; position: relative; - border: var(--border-base); + border: var.$input-border; border-radius: var.$input-border-radius; + border-top-left-radius: var.$input-border-top-left-radius; + border-top-right-radius: var.$input-border-top-right-radius; + border-bottom-left-radius: var.$input-border-bottom-left-radius; + border-bottom-right-radius: var.$input-border-bottom-right-radius; + border-right-color: var.$input-border-right-color; + border-bottom-color: var.$input-border-bottom-color; padding: 0 10px; width: 1px; white-space: nowrap; diff --git a/packages/design-system/src/css/select.scss b/packages/design-system/src/css/select.scss index 7cc8696c1a..f5b6f17d31 100644 --- a/packages/design-system/src/css/select.scss +++ b/packages/design-system/src/css/select.scss @@ -80,6 +80,14 @@ &.is-focus .el-input__inner { border-color: var.$select-input-focus-border-color; } + + &__prefix { + left: var(--spacing-2xs); + } + + &--prefix .el-input__inner { + padding-left: 26px; + } } > .el-input { diff --git a/packages/design-system/src/plugin.ts b/packages/design-system/src/plugin.ts index 6a5ec45bb8..f06d4cf4bb 100644 --- a/packages/design-system/src/plugin.ts +++ b/packages/design-system/src/plugin.ts @@ -51,6 +51,7 @@ import { N8nUserInfo, N8nUserSelect, N8nUsersList, + N8nResizeObserver, N8nKeyboardShortcut, N8nUserStack, } from './components'; @@ -111,6 +112,7 @@ export const N8nPlugin: Plugin = { app.component('n8n-user-info', N8nUserInfo); app.component('n8n-users-list', N8nUsersList); app.component('n8n-user-select', N8nUserSelect); + app.component('n8n-resize-observer', N8nResizeObserver); app.component('n8n-keyboard-shortcut', N8nKeyboardShortcut); }, }; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 31c593cf8b..0759953523 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1215,7 +1215,7 @@ export interface NDVState { isDragging: boolean; type: string; data: string; - canDrop: boolean; + activeTargetId: string | null; stickyPosition: null | XYPosition; }; isMappingOnboarded: boolean; diff --git a/packages/editor-ui/src/components/DraggableTarget.vue b/packages/editor-ui/src/components/DraggableTarget.vue index 3418f60439..a36b4aa9ff 100644 --- a/packages/editor-ui/src/components/DraggableTarget.vue +++ b/packages/editor-ui/src/components/DraggableTarget.vue @@ -9,6 +9,7 @@ import { defineComponent } from 'vue'; import type { PropType } from 'vue'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv.store'; +import { v4 as uuid } from 'uuid'; export default defineComponent({ props: { @@ -29,6 +30,7 @@ export default defineComponent({ data() { return { hovering: false, + id: uuid(), }; }, mounted() { @@ -83,7 +85,12 @@ export default defineComponent({ }, watch: { activeDrop(active) { - this.ndvStore.setDraggableCanDrop(active); + if (active) { + this.ndvStore.setDraggableTargetId(this.id); + } else if (this.ndvStore.draggable.activeTargetId === this.id) { + // Only clear active target if it is this one + this.ndvStore.setDraggableTargetId(null); + } }, }, }); diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue index edc093af53..b3b555fb45 100644 --- a/packages/editor-ui/src/components/ExpressionParameterInput.vue +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -5,20 +5,14 @@ @keydown.tab="onBlur" >
-
+
+import { useI18n } from '@/composables/useI18n'; +import type { FilterTypeCombinator } from 'n8n-workflow'; + +interface Props { + options: FilterTypeCombinator[]; + selected: FilterTypeCombinator; + readOnly: boolean; +} + +defineProps(); + +const emit = defineEmits<{ + (event: 'combinatorChange', value: FilterTypeCombinator): void; +}>(); + +const i18n = useI18n(); + +const onCombinatorChange = (combinator: FilterTypeCombinator): void => { + emit('combinatorChange', combinator); +}; + + + + + diff --git a/packages/editor-ui/src/components/FilterConditions/Condition.vue b/packages/editor-ui/src/components/FilterConditions/Condition.vue new file mode 100644 index 0000000000..a95371c7c0 --- /dev/null +++ b/packages/editor-ui/src/components/FilterConditions/Condition.vue @@ -0,0 +1,451 @@ + + + + + diff --git a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue new file mode 100644 index 0000000000..df1d885be8 --- /dev/null +++ b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/packages/editor-ui/src/components/FilterConditions/OperatorSelect.vue b/packages/editor-ui/src/components/FilterConditions/OperatorSelect.vue new file mode 100644 index 0000000000..432c1991ec --- /dev/null +++ b/packages/editor-ui/src/components/FilterConditions/OperatorSelect.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/packages/editor-ui/src/components/FilterConditions/constants.ts b/packages/editor-ui/src/components/FilterConditions/constants.ts new file mode 100644 index 0000000000..578d74c71a --- /dev/null +++ b/packages/editor-ui/src/components/FilterConditions/constants.ts @@ -0,0 +1,272 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { FilterConditionValue, FilterOptionsValue } from 'n8n-workflow'; +import type { FilterOperator, FilterOperatorGroup } from './types'; + +export const DEFAULT_MAX_CONDITIONS = 10; + +export const DEFAULT_FILTER_OPTIONS: FilterOptionsValue = { + caseSensitive: true, + leftValue: '', + typeValidation: 'strict', +}; + +export const OPERATORS_BY_ID = { + 'string:exists': { + type: 'string', + operation: 'exists', + name: 'filter.operator.exists', + singleValue: true, + }, + 'string:notExists': { + type: 'string', + operation: 'notExists', + name: 'filter.operator.notExists', + singleValue: true, + }, + 'string:equals': { type: 'string', operation: 'equals', name: 'filter.operator.equals' }, + 'string:notEquals': { type: 'string', operation: 'notEquals', name: 'filter.operator.notEquals' }, + 'string:contains': { type: 'string', operation: 'contains', name: 'filter.operator.contains' }, + 'string:notContains': { + type: 'string', + operation: 'notContains', + name: 'filter.operator.notContains', + }, + 'string:startsWith': { + type: 'string', + operation: 'startsWith', + name: 'filter.operator.startsWith', + }, + 'string:notStartsWith': { + type: 'string', + operation: 'notStartsWith', + name: 'filter.operator.notStartsWith', + }, + 'string:endsWith': { type: 'string', operation: 'endsWith', name: 'filter.operator.endsWith' }, + 'string:notEndsWith': { + type: 'string', + operation: 'notEndsWith', + name: 'filter.operator.notEndsWith', + }, + 'string:regex': { type: 'string', operation: 'regex', name: 'filter.operator.regex' }, + 'string:notRegex': { type: 'string', operation: 'notRegex', name: 'filter.operator.notRegex' }, + 'number:exists': { + type: 'number', + operation: 'exists', + name: 'filter.operator.exists', + singleValue: true, + }, + 'number:notExists': { + type: 'number', + operation: 'notExists', + name: 'filter.operator.notExists', + singleValue: true, + }, + 'number:equals': { type: 'number', operation: 'equals', name: 'filter.operator.equals' }, + 'number:notEquals': { type: 'number', operation: 'notEquals', name: 'filter.operator.notEquals' }, + 'number:gt': { type: 'number', operation: 'gt', name: 'filter.operator.gt' }, + 'number:lt': { type: 'number', operation: 'lt', name: 'filter.operator.lt' }, + 'number:gte': { type: 'number', operation: 'gte', name: 'filter.operator.gte' }, + 'number:lte': { type: 'number', operation: 'lte', name: 'filter.operator.lte' }, + 'dateTime:exists': { + type: 'dateTime', + operation: 'exists', + name: 'filter.operator.exists', + singleValue: true, + }, + 'dateTime:notExists': { + type: 'dateTime', + operation: 'notExists', + name: 'filter.operator.notExists', + singleValue: true, + }, + 'dateTime:equals': { type: 'dateTime', operation: 'equals', name: 'filter.operator.equals' }, + 'dateTime:notEquals': { + type: 'dateTime', + operation: 'notEquals', + name: 'filter.operator.notEquals', + }, + 'dateTime:after': { type: 'dateTime', operation: 'after', name: 'filter.operator.after' }, + 'dateTime:before': { type: 'dateTime', operation: 'before', name: 'filter.operator.before' }, + 'dateTime:afterOrEquals': { + type: 'dateTime', + operation: 'afterOrEquals', + name: 'filter.operator.afterOrEquals', + }, + 'dateTime:beforeOrEquals': { + type: 'dateTime', + operation: 'beforeOrEquals', + name: 'filter.operator.beforeOrEquals', + }, + 'boolean:exists': { + type: 'boolean', + operation: 'exists', + name: 'filter.operator.exists', + singleValue: true, + }, + 'boolean:notExists': { + type: 'boolean', + operation: 'notExists', + name: 'filter.operator.notExists', + singleValue: true, + }, + 'boolean:true': { + type: 'boolean', + operation: 'true', + name: 'filter.operator.true', + singleValue: true, + }, + 'boolean:false': { + type: 'boolean', + operation: 'false', + name: 'filter.operator.false', + singleValue: true, + }, + 'boolean:equals': { type: 'boolean', operation: 'equals', name: 'filter.operator.equals' }, + 'boolean:notEquals': { + type: 'boolean', + operation: 'notEquals', + name: 'filter.operator.notEquals', + }, + 'array:exists': { + type: 'array', + operation: 'exists', + name: 'filter.operator.exists', + singleValue: true, + }, + 'array:notExists': { + type: 'array', + operation: 'notExists', + name: 'filter.operator.notExists', + singleValue: true, + }, + 'array:empty': { + type: 'array', + operation: 'empty', + name: 'filter.operator.empty', + singleValue: true, + }, + 'array:notEmpty': { + type: 'array', + operation: 'notEmpty', + name: 'filter.operator.notEmpty', + singleValue: true, + }, + 'array:contains': { + type: 'array', + operation: 'contains', + name: 'filter.operator.contains', + rightType: 'any', + }, + 'array:notContains': { + type: 'array', + operation: 'notContains', + name: 'filter.operator.notContains', + rightType: 'any', + }, + 'array:lengthEquals': { + type: 'array', + operation: 'lengthEquals', + name: 'filter.operator.lengthEquals', + rightType: 'number', + }, + 'array:lengthNotEquals': { + type: 'array', + operation: 'lengthNotEquals', + name: 'filter.operator.lengthNotEquals', + rightType: 'number', + }, + 'array:lengthGt': { + type: 'array', + operation: 'lengthGt', + name: 'filter.operator.lengthGt', + rightType: 'number', + }, + 'array:lengthLt': { + type: 'array', + operation: 'lengthLt', + name: 'filter.operator.lengthLt', + rightType: 'number', + }, + 'array:lengthGte': { + type: 'array', + operation: 'lengthGte', + name: 'filter.operator.lengthGte', + rightType: 'number', + }, + 'array:lengthLte': { + type: 'array', + operation: 'lengthLte', + name: 'filter.operator.lengthLte', + rightType: 'number', + }, + 'object:exists': { + type: 'object', + operation: 'exists', + name: 'filter.operator.exists', + singleValue: true, + }, + 'object:notExists': { + type: 'object', + operation: 'notExists', + name: 'filter.operator.notExists', + singleValue: true, + }, + 'object:empty': { + type: 'object', + operation: 'empty', + name: 'filter.operator.empty', + singleValue: true, + }, + 'object:notEmpty': { + type: 'object', + operation: 'notEmpty', + name: 'filter.operator.notEmpty', + singleValue: true, + }, +} as const satisfies Record; + +export const OPERATORS = Object.values(OPERATORS_BY_ID); + +export type FilterOperatorId = keyof typeof OPERATORS_BY_ID; + +export const DEFAULT_OPERATOR_VALUE: FilterConditionValue['operator'] = + OPERATORS_BY_ID['string:equals']; + +export const OPERATOR_GROUPS: FilterOperatorGroup[] = [ + { + id: 'string', + name: 'filter.operatorGroup.string', + icon: 'font', + children: OPERATORS.filter((operator) => operator.type === 'string'), + }, + { + id: 'number', + name: 'filter.operatorGroup.number', + icon: 'hashtag', + children: OPERATORS.filter((operator) => operator.type === 'number'), + }, + { + id: 'dateTime', + name: 'filter.operatorGroup.date', + icon: 'calendar', + children: OPERATORS.filter((operator) => operator.type === 'dateTime'), + }, + { + id: 'boolean', + name: 'filter.operatorGroup.boolean', + icon: 'check-square', + children: OPERATORS.filter((operator) => operator.type === 'boolean'), + }, + { + id: 'array', + name: 'filter.operatorGroup.array', + icon: 'list', + children: OPERATORS.filter((operator) => operator.type === 'array'), + }, + { + id: 'object', + name: 'filter.operatorGroup.object', + icon: 'cube', + children: OPERATORS.filter((operator) => operator.type === 'object'), + }, +]; diff --git a/packages/editor-ui/src/components/FilterConditions/types.ts b/packages/editor-ui/src/components/FilterConditions/types.ts new file mode 100644 index 0000000000..d23d4eff65 --- /dev/null +++ b/packages/editor-ui/src/components/FilterConditions/types.ts @@ -0,0 +1,13 @@ +import type { BaseTextKey } from '@/plugins/i18n'; +import type { FilterOperatorValue } from 'n8n-workflow'; + +export interface FilterOperator extends FilterOperatorValue { + name: BaseTextKey; +} + +export interface FilterOperatorGroup { + id: string; + name: BaseTextKey; + icon?: string; + children: FilterOperator[]; +} diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index 3ce36f6f5b..b9090eb006 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -28,28 +28,32 @@ :class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'" >
- -
- - -
+ > + +
- + >
-
+
.parameter-item-wrapper > .delete-option { - display: block; + opacity: 1; } .parameter-item { @@ -411,11 +413,4 @@ export default defineComponent({ .no-items-exist { margin: var(--spacing-xs) 0; } - -.sort-icon { - display: flex; - flex-direction: column; - margin-left: 1px; - margin-top: 0.5em; -} diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts b/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts index 3a92747e19..f6dd6aed26 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts +++ b/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts @@ -27,9 +27,17 @@ export const inputTheme = ({ isSingleLine } = { isSingleLine: false }) => { borderWidth: 'var(--border-width-base)', borderStyle: 'var(--input-border-style, var(--border-style-base))', borderColor: 'var(--input-border-color, var(--border-color-base))', + borderRightColor: + 'var(--input-border-right-color,var(--input-border-color, var(--border-color-base)))', + borderBottomColor: + 'var(--input-border-bottom-color,var(--input-border-color, var(--border-color-base)))', borderRadius: 'var(--input-border-radius, var(--border-radius-base))', - borderTopLeftRadius: '0', - borderBottomLeftRadius: '0', + borderTopLeftRadius: 0, + borderTopRightRadius: + 'var(--input-border-top-right-radius, var(--input-border-radius, var(--border-radius-base)))', + borderBottomLeftRadius: 0, + borderBottomRightRadius: + 'var(--input-border-bottom-right-radius, var(--input-border-radius, var(--border-radius-base)))', backgroundColor: 'white', }, '.cm-scroller': { diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index b7f7908e6a..60c6a83986 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -45,6 +45,7 @@ :modelValue="expressionDisplayValue" :title="displayTitle" :isReadOnly="isReadOnly" + :isSingleLine="isSingleLine" :path="path" :additional-expression-data="additionalExpressionData" :class="{ 'ph-no-capture': shouldRedactValue }" @@ -209,6 +210,7 @@ v-model="tempValue" ref="inputField" type="datetime" + valueFormat="YYYY-MM-DDTHH:mm:ss" :size="inputSize" :modelValue="displayValue" :title="displayTitle" @@ -447,6 +449,9 @@ export default defineComponent({ isReadOnly: { type: Boolean, }, + isSingleLine: { + type: Boolean, + }, parameter: { type: Object as PropType, }, diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index 7895765316..3cc25907cf 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -1,16 +1,17 @@