diff --git a/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/multiValueSerialization.cy.ts b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/multiValueSerialization.cy.ts new file mode 100644 index 000000000..bc88e787f --- /dev/null +++ b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/multiValueSerialization.cy.ts @@ -0,0 +1,64 @@ +describe('Tests for Multi Value serialization', () => { + beforeEach(() => { + cy.openHomePage(); + }); + + it('Sidebar add Multi Value step configuration', () => { + cy.uploadFixture('flows/camelRoute/multiValue.yaml'); + cy.openDesignPage(); + // Configure quartz - source step + cy.openStepConfigurationTab('quartz'); + cy.selectFormTab('All'); + + cy.filterFields('Job Parameters'); + cy.expandWrappedSection('parameters.jobParameters'); + cy.get('[data-testid="properties-add-string-property--btn"]').not(':hidden').first().click({ force: true }); + cy.get('[data-testid="parameters.jobParameters--placeholder-name-input"]').should('not.be.disabled'); + cy.get('[data-testid="parameters.jobParameters--placeholder-name-input"]').click({ force: true }); + cy.get('[data-testid="parameters.jobParameters--placeholder-name-input"]').clear().type('jobParametersTest'); + + cy.get('[data-testid="parameters.jobParameters--placeholder-value-input"]').should('not.be.disabled'); + cy.get('[data-testid="parameters.jobParameters--placeholder-value-input"]').click({ force: true }); + cy.get('[data-testid="parameters.jobParameters--placeholder-value-input"]').clear().type('jobParametersValue'); + cy.get('[data-testid="parameters.jobParameters--placeholder-property-edit-confirm--btn"]').click({ force: true }); + cy.closeWrappedSection('parameters.jobParameters'); + + cy.filterFields('Trigger Parameters'); + cy.expandWrappedSection('parameters.triggerParameters'); + cy.get('[data-testid="properties-add-string-property--btn"]').not(':hidden').first().click({ force: true }); + cy.get('[data-testid="parameters.triggerParameters--placeholder-name-input"]').should('not.be.disabled'); + cy.get('[data-testid="parameters.triggerParameters--placeholder-name-input"]').click({ force: true }); + cy.get('[data-testid="parameters.triggerParameters--placeholder-name-input"]').clear().type('triggerParametersTest'); + + cy.get('[data-testid="parameters.triggerParameters--placeholder-value-input"]').should('not.be.disabled'); + cy.get('[data-testid="parameters.triggerParameters--placeholder-value-input"]').click({ force: true }); + cy.get('[data-testid="parameters.triggerParameters--placeholder-value-input"]').clear().type('triggerParametersValue'); + cy.get('[data-testid="parameters.triggerParameters--placeholder-property-edit-confirm--btn"]').click({ force: true }); + cy.closeWrappedSection('parameters.triggerParameters'); + + // CHECK changes are reflected in the code editor + cy.openSourceCode(); + cy.checkCodeSpanLine('job.jobParametersTest: jobParametersValue'); + cy.checkCodeSpanLine('trigger.triggerParametersTest: triggerParametersValue'); + }); + + it('Sidebar delete Multi Value step configuration', () => { + cy.uploadFixture('flows/camelRoute/multiValue.yaml'); + cy.openDesignPage(); + // Configure quartz - source step + cy.openStepConfigurationTab('quartz'); + cy.selectFormTab('All'); + + cy.filterFields('Job Parameters'); + cy.expandWrappedSection('parameters.jobParameters'); + cy.get('[data-testid="parameters.jobParameters-testJob-delete-testJob-btn"]').not(':hidden').first().click({ force: true }); + + cy.filterFields('Trigger Parameters'); + cy.expandWrappedSection('parameters.triggerParameters'); + cy.get('[data-testid="parameters.triggerParameters-testTrigger-delete-testTrigger-btn"]').not(':hidden').first().click({ force: true }); + + cy.openSourceCode(); + cy.checkCodeSpanLine('job.testJob: testJob', 0); + cy.checkCodeSpanLine('trigger.testTrigger: testTrigger', 0); + }); +}); diff --git a/packages/ui-tests/cypress/fixtures/flows/camelRoute/multiValue.yaml b/packages/ui-tests/cypress/fixtures/flows/camelRoute/multiValue.yaml new file mode 100644 index 000000000..326faa3de --- /dev/null +++ b/packages/ui-tests/cypress/fixtures/flows/camelRoute/multiValue.yaml @@ -0,0 +1,12 @@ +- route: + id: route-2553 + from: + id: from-3744 + uri: quartz:Camel/ + parameters: + job.testJob: testJob + trigger.testTrigger: testTrigger + steps: + - log: + id: log-3089 + message: ${body} diff --git a/packages/ui-tests/cypress/support/next-commands/nodeConfiguration.ts b/packages/ui-tests/cypress/support/next-commands/nodeConfiguration.ts index 73d9f3933..e5dd2e060 100644 --- a/packages/ui-tests/cypress/support/next-commands/nodeConfiguration.ts +++ b/packages/ui-tests/cypress/support/next-commands/nodeConfiguration.ts @@ -136,6 +136,7 @@ Cypress.Commands.add('addSingleKVProperty', (propertyName: string, key: string, Cypress.Commands.add('filterFields', (filter: string) => { cy.get('[data-testid="filter-fields"]').within(() => { + cy.get('input.pf-v5-c-text-input-group__text-input').clear(); cy.get('input.pf-v5-c-text-input-group__text-input').type(filter); }); }); diff --git a/packages/ui/src/models/camel-properties-common.ts b/packages/ui/src/models/camel-properties-common.ts index 0b63b09bd..e022b118b 100644 --- a/packages/ui/src/models/camel-properties-common.ts +++ b/packages/ui/src/models/camel-properties-common.ts @@ -5,6 +5,8 @@ export interface CamelPropertyCommon { displayName: string; label?: string; required: boolean; + multiValue?: boolean; + prefix?: string; javaType: string; enum?: string[]; autowired: boolean; diff --git a/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts b/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts index 26d2849a5..d06e512c7 100644 --- a/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts @@ -72,7 +72,8 @@ export abstract class AbstractCamelVisualEntity implements Bas updateModel(path: string | undefined, value: unknown): void { if (!path) return; - const updatedValue = CamelComponentSchemaService.getUriSerializedDefinition(path, value); + let updatedValue = CamelComponentSchemaService.getUriSerializedDefinition(path, value); + updatedValue = CamelComponentSchemaService.getMultiValueSerializedDefinition(path, updatedValue); setValue(this.route, path, updatedValue); } diff --git a/packages/ui/src/models/visualization/flows/support/__snapshots__/camel-component-schema.service.test.ts.snap b/packages/ui/src/models/visualization/flows/support/__snapshots__/camel-component-schema.service.test.ts.snap index 3ef89bf23..0ff5a089b 100644 --- a/packages/ui/src/models/visualization/flows/support/__snapshots__/camel-component-schema.service.test.ts.snap +++ b/packages/ui/src/models/visualization/flows/support/__snapshots__/camel-component-schema.service.test.ts.snap @@ -820,6 +820,7 @@ exports[`CamelComponentSchemaService getVisualComponentSchema should transform a "parameters": { "beanName": "myBean", "method": "hello", + "parameters": {}, }, "uri": "bean", }, @@ -935,6 +936,7 @@ exports[`CamelComponentSchemaService getVisualComponentSchema should transform a "parameters": { "beanName": "myBean", "method": "hello", + "parameters": {}, }, "uri": "bean", }, diff --git a/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.test.ts b/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.test.ts index b25a1d66c..fa51400ae 100644 --- a/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.test.ts +++ b/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.test.ts @@ -559,6 +559,43 @@ describe('CamelComponentSchemaService', () => { }); }); + describe('getMultiValueSerializedDefinition', () => { + it('should return the same parameters if the definition is not a component', () => { + const definition = { log: { message: 'Hello World' } }; + const result = CamelComponentSchemaService.getMultiValueSerializedDefinition('from', definition); + + expect(result).toEqual(definition); + }); + + it('should return the same parameters if the component is not found', () => { + const definition = { + uri: 'unknown-component', + parameters: { jobParameters: { test: 'test' }, triggerParameters: { test: 'test' } }, + }; + const result = CamelComponentSchemaService.getMultiValueSerializedDefinition('from', definition); + + expect(result).toEqual(definition); + }); + + it('should query the catalog service', () => { + const definition = { uri: 'log', parameters: { message: 'Hello World' } }; + const catalogServiceSpy = jest.spyOn(CamelCatalogService, 'getCatalogLookup'); + + CamelComponentSchemaService.getMultiValueSerializedDefinition('from', definition); + expect(catalogServiceSpy).toHaveBeenCalledWith('log'); + }); + + it('should return the serialized definition', () => { + const definition = { + uri: 'quartz', + parameters: { jobParameters: { test: 'test' }, triggerParameters: { test: 'test' } }, + }; + const result = CamelComponentSchemaService.getMultiValueSerializedDefinition('from', definition); + + expect(result).toEqual({ uri: 'quartz', parameters: { 'job.test': 'test', 'trigger.test': 'test' } }); + }); + }); + describe('getUriSerializedDefinition', () => { it('should return the same parameters if the definition is not a component', () => { const definition = { log: { message: 'Hello World' } }; diff --git a/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts b/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts index d3983a7d1..67ebcd95e 100644 --- a/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts +++ b/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts @@ -257,6 +257,42 @@ export class CamelComponentSchemaService { return definition; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static getMultiValueSerializedDefinition(path: string, definition: any): ParsedParameters | undefined { + const camelElementLookup = this.getCamelComponentLookup(path, definition); + if (camelElementLookup.componentName === undefined) { + return definition; + } + + const catalogLookup = CamelCatalogService.getCatalogLookup(camelElementLookup.componentName); + if (catalogLookup.catalogKind === CatalogKind.Component) { + const multiValueParameters: Map = new Map(); + if (catalogLookup.definition?.properties !== undefined) { + Object.entries(catalogLookup.definition.properties).forEach(([key, value]) => { + if (value.multiValue) multiValueParameters.set(key, value.prefix!); + }); + } + const defaultMultiValues: ParsedParameters = {}; + const filteredParameters = definition.parameters; + + if (definition.parameters !== undefined) { + Object.keys(definition.parameters).forEach((key) => { + if (multiValueParameters.has(key)) { + if (definition.parameters[key] === undefined) { + return; + } + Object.keys(definition.parameters[key]).forEach((subKey) => { + defaultMultiValues[multiValueParameters.get(key) + subKey] = definition.parameters[key][subKey]; + }); + delete filteredParameters[key]; + } + }); + } + return Object.assign({}, definition, { parameters: { ...filteredParameters, ...defaultMultiValues } }); + } + return definition; + } + /** * If the processor is a `from` or `to` processor, we need to extract the component name from the uri property * and return both the processor name and the underlying component name to build the combined schema @@ -385,11 +421,42 @@ export class CamelComponentSchemaService { if (camelElementLookup.componentName !== undefined) { updatedDefinition.parameters = updatedDefinition.parameters ?? {}; this.applyParametersFromSyntax(camelElementLookup.componentName, updatedDefinition); + this.readMultiValue(camelElementLookup.componentName, updatedDefinition); } return updatedDefinition; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private static readMultiValue(componentName: string, definition: any) { + const catalogLookup = CamelCatalogService.getCatalogLookup(componentName); + + const multiValueParameters: Map = new Map(); + if (catalogLookup !== undefined && catalogLookup.definition?.properties !== undefined) { + Object.entries(catalogLookup.definition.properties).forEach(([key, value]) => { + if (value.multiValue) multiValueParameters.set(key, value.prefix!); + }); + } + if (multiValueParameters.size > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parameters: any = {}; + const filteredParameters = definition.parameters; + + multiValueParameters.forEach((value, key) => { + const nestParameters: ParsedParameters = {}; + + Object.entries(definition.parameters).forEach(([paramKey, paramValue]) => { + if (paramKey.startsWith(value)) { + nestParameters[paramKey.replace(value, '')] = paramValue as string; + delete filteredParameters[paramKey]; + } + parameters[key] = { ...nestParameters }; + }); + }); + Object.assign(definition, { parameters: { ...filteredParameters, ...parameters } }); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private static applyParametersFromSyntax(componentName: string, definition: any) { const catalogLookup = CamelCatalogService.getCatalogLookup(componentName);