diff --git a/packages/ui-tests/stories/canvas/CanvasSideBar.stories.tsx b/packages/ui-tests/stories/canvas/CanvasSideBar.stories.tsx index 3c396a3ad..cbc059258 100644 --- a/packages/ui-tests/stories/canvas/CanvasSideBar.stories.tsx +++ b/packages/ui-tests/stories/canvas/CanvasSideBar.stories.tsx @@ -1,10 +1,13 @@ import { + CamelRouteVisualEntity, CanvasNode, CanvasSideBar, IVisualizationNode, IVisualizationNodeData, NodeIconResolver, + VisibleFlowsProvider, VisualComponentSchema, + camelRouteJson, } from '@kaoto-next/ui/testing'; import { Meta, StoryFn } from '@storybook/react'; import { useState } from 'react'; @@ -53,6 +56,7 @@ const selectedNode: CanvasNode = { }, } as VisualComponentSchema; }, + getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route), } as IVisualizationNode, }, }; @@ -75,6 +79,7 @@ const unknownSelectedNode: CanvasNode = { definition: null, } as VisualComponentSchema; }, + getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route), } as IVisualizationNode, }, }; @@ -87,7 +92,11 @@ export default { const Template: StoryFn = (args) => { const [isModalOpen, setIsModalOpen] = useState(false); const handleClose = () => setIsModalOpen(!isModalOpen); - return ; + return ( + + + + ); }; export const ProcessorNode = Template.bind({}); diff --git a/packages/ui/src/components/Visualization/Canvas/CanvasForm.test.tsx b/packages/ui/src/components/Visualization/Canvas/CanvasForm.test.tsx index 74c4c4d9f..eb91202ad 100644 --- a/packages/ui/src/components/Visualization/Canvas/CanvasForm.test.tsx +++ b/packages/ui/src/components/Visualization/Canvas/CanvasForm.test.tsx @@ -8,43 +8,62 @@ import { CamelCatalogService, CamelRouteVisualEntity, CatalogKind, + ICamelComponentDefinition, ICamelDataformatDefinition, ICamelLanguageDefinition, ICamelLoadBalancerDefinition, ICamelProcessorDefinition, + IKameletDefinition, + KameletVisualEntity, KaotoSchemaDefinition, } from '../../../models'; import { IVisualizationNode, VisualComponentSchema } from '../../../models/visualization/base-visual-entity'; -import { EntitiesContext } from '../../../providers/entities.provider'; +import { VisibleFlowsContext, VisibleFlowsProvider } from '../../../providers'; +import { EntitiesContext, EntitiesProvider } from '../../../providers/entities.provider'; +import { camelRouteJson, kameletJson } from '../../../stubs'; import { SchemaService } from '../../Form'; import { CustomAutoFieldDetector } from '../../Form/CustomAutoField'; import { CanvasForm } from './CanvasForm'; import { CanvasNode } from './canvas.models'; +import { CanvasService } from './canvas.service'; +import { VisualFlowsApi } from '../../../models/visualization/flows/support/flows-visibility'; describe('CanvasForm', () => { + let camelRouteVisualEntity: CamelRouteVisualEntity; + let selectedNode: CanvasNode; + let componentCatalogMap: Record; + let patternCatalogMap: Record; + let kameletCatalogMap: Record; const schemaService = new SchemaService(); - const schema = { - type: 'object', - properties: { - name: { - type: 'string', - }, - }, - } as unknown as KaotoSchemaDefinition['schema']; - beforeAll(async () => { - const patternCatalog = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.patterns.file); - delete patternCatalog.default; + componentCatalogMap = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.components.file); + delete componentCatalogMap.default; + const modelCatalog = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.models.file); + delete modelCatalog.default; + patternCatalogMap = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.patterns.file); + delete patternCatalogMap.default; + kameletCatalogMap = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.kamelets.file); + delete kameletCatalogMap.default; const languageCatalog = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.languages.file); delete languageCatalog.default; const dataformatCatalog = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.dataformats.file); delete dataformatCatalog.default; const loadbalancerCatalog = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.loadbalancers.file); delete loadbalancerCatalog.default; + const entitiesCatalog = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.entities.file); + delete entitiesCatalog.default; + CamelCatalogService.setCatalogKey( + CatalogKind.Component, + componentCatalogMap as unknown as Record, + ); CamelCatalogService.setCatalogKey( CatalogKind.Pattern, - patternCatalog as unknown as Record, + patternCatalogMap as unknown as Record, + ); + CamelCatalogService.setCatalogKey( + CatalogKind.Kamelet, + kameletCatalogMap as unknown as Record, ); CamelCatalogService.setCatalogKey( CatalogKind.Language, @@ -58,31 +77,29 @@ describe('CanvasForm', () => { CatalogKind.Loadbalancer, loadbalancerCatalog as unknown as Record, ); + CamelCatalogService.setCatalogKey( + CatalogKind.Entity, + entitiesCatalog as unknown as Record, + ); }); - it('should render', () => { - const visualComponentSchema: VisualComponentSchema = { - title: 'My Node', - schema, - definition: { - name: 'my node', - }, - }; + beforeEach(() => { + camelRouteVisualEntity = new CamelRouteVisualEntity(camelRouteJson.route); + const { nodes } = CanvasService.getFlowDiagram(camelRouteVisualEntity.toVizNode()); + selectedNode = nodes[2]; // choice + }); - const selectedNode: CanvasNode = { - id: '1', - type: 'node', - data: { - vizNode: { - getComponentSchema: () => visualComponentSchema, - } as IVisualizationNode, - }, - }; + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render', () => { const { container } = render( - - - , + + + + + , ); expect(container).toMatchSnapshot(); @@ -95,13 +112,16 @@ describe('CanvasForm', () => { data: { vizNode: { getComponentSchema: () => undefined, - } as IVisualizationNode, + getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route), + } as unknown as IVisualizationNode, }, }; const { container } = render( - + + + , ); @@ -121,13 +141,16 @@ describe('CanvasForm', () => { data: { vizNode: { getComponentSchema: () => visualComponentSchema, - } as IVisualizationNode, + getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route), + } as unknown as IVisualizationNode, }, }; const { container } = render( - + + + , ); @@ -149,20 +172,88 @@ describe('CanvasForm', () => { data: { vizNode: { getComponentSchema: () => visualComponentSchema, - } as IVisualizationNode, + getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route), + } as unknown as IVisualizationNode, }, }; render( - + + + , ); expect(visualComponentSchema.definition.parameters).toEqual({}); }); + it('should allow consumers to update the Camel Route ID', async () => { + const flowId = camelRouteVisualEntity.id; + const newName = 'MyNewId'; + const dispatchSpy = jest.fn(); + const visualFlowsApi = new VisualFlowsApi(dispatchSpy); + const { nodes } = CanvasService.getFlowDiagram(camelRouteVisualEntity.toVizNode()); + selectedNode = nodes[nodes.length - 1]; + + render( + + + + + , + ); + + const idField = screen.getAllByLabelText('Id', { selector: 'input' })[0]; + act(() => { + fireEvent.change(idField, { target: { value: newName } }); + }); + + const closeSideBarButton = screen.getByTestId('close-side-bar'); + act(() => { + fireEvent.click(closeSideBarButton); + }); + + expect(camelRouteVisualEntity.id).toEqual(newName); + expect(dispatchSpy).toHaveBeenCalledWith({ type: 'renameFlow', flowId, newName }); + }); + + it('should allow consumers to update the Kamelet name', async () => { + const kameletVisualEntity = new KameletVisualEntity(kameletJson); + const flowId = kameletVisualEntity.id; + const newName = 'MyNewName'; + const dispatchSpy = jest.fn(); + const visualFlowsApi = new VisualFlowsApi(dispatchSpy); + const { nodes } = CanvasService.getFlowDiagram(kameletVisualEntity.toVizNode()); + selectedNode = nodes[nodes.length - 1]; + + render( + + + + + , + ); + + const NameField = screen.getByDisplayValue('user-source'); + act(() => { + fireEvent.change(NameField, { target: { value: newName } }); + }); + + const closeSideBarButton = screen.getByTestId('close-side-bar'); + act(() => { + fireEvent.click(closeSideBarButton); + }); + + expect(kameletVisualEntity.id).toEqual(newName); + expect(dispatchSpy).toHaveBeenCalledWith({ type: 'renameFlow', flowId, newName }); + }); + describe('should persists changes from both expression editor and main form', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + it('expression => main form', async () => { const camelRoute = { from: { @@ -189,7 +280,9 @@ describe('CanvasForm', () => { render( - + + + , ); const launchExpression = screen.getByTestId('launch-expression-modal-btn'); @@ -253,7 +346,9 @@ describe('CanvasForm', () => { render( - + + + , ); const filtered = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Name'); @@ -292,6 +387,10 @@ describe('CanvasForm', () => { }); describe('should persists changes from both dataformat editor and main form', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + it('dataformat => main form', async () => { const camelRoute = { from: { @@ -318,7 +417,9 @@ describe('CanvasForm', () => { render( - + + + , ); const button = screen @@ -368,7 +469,9 @@ describe('CanvasForm', () => { render( - + + + , ); const idInput = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Id'); @@ -394,6 +497,10 @@ describe('CanvasForm', () => { }); describe('should persists changes from both loadbalancer editor and main form', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + it('loadbalancer => main form', async () => { const camelRoute = { from: { @@ -420,7 +527,9 @@ describe('CanvasForm', () => { render( - + + + , ); const button = screen @@ -470,7 +579,9 @@ describe('CanvasForm', () => { render( - + + + , ); const idInput = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Id'); @@ -495,76 +606,73 @@ describe('CanvasForm', () => { }); }); - /* - * Exhaustive tests - */ - - it('should render for all component without an error', async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - const componentCatalogMap = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.components.file); - Object.entries(componentCatalogMap).forEach(([name, catalog]) => { - try { - if (name === 'default') return; - /* eslint-disable @typescript-eslint/no-explicit-any */ - const schema = schemaService.getSchemaBridge((catalog as any).propertiesSchema); - render( - - {}}> - - - , - ); - } catch (e) { - /* eslint-disable @typescript-eslint/no-explicit-any */ - throw new Error(`Error rendering ${name} component: ${(e as any).message}`); - } + describe('Exhaustive tests', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); }); - }); - it('should render for all kamelets without an error', async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - const kameletCatalogMap = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.kamelets.file); - Object.entries(kameletCatalogMap).forEach(([name, kamelet]) => { - try { - if (name === 'default') return; - expect(kamelet).toBeDefined(); - /* eslint-disable @typescript-eslint/no-explicit-any */ - const schema = (kamelet as any).propertiesSchema; - const bridge = schemaService.getSchemaBridge(schema); - render( - - {}}> - - - , - ); - } catch (e) { - /* eslint-disable @typescript-eslint/no-explicit-any */ - throw new Error(`Error rendering ${name} component: ${(e as any).message}`); - } + it('should render for all component without an error', async () => { + Object.entries(componentCatalogMap).forEach(([name, catalog]) => { + try { + if (name === 'default') return; + /* eslint-disable @typescript-eslint/no-explicit-any */ + const schema = schemaService.getSchemaBridge((catalog as any).propertiesSchema); + render( + + {}}> + + + , + ); + } catch (e) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + throw new Error(`Error rendering ${name} component: ${(e as any).message}`); + } + }); }); - }); - it('should render for all patterns without an error', async () => { - const patternCatalogMap = await import('@kaoto-next/camel-catalog/' + catalogIndex.catalogs.patterns.file); - Object.entries(patternCatalogMap).forEach(([name, pattern]) => { - try { - if (name === 'default') return; - expect(pattern).toBeDefined(); - /* eslint-disable @typescript-eslint/no-explicit-any */ - const schema = (pattern as any).propertiesSchema; - const bridge = schemaService.getSchemaBridge(schema); - render( - - {}}> - - - , - ); - } catch (e) { - /* eslint-disable @typescript-eslint/no-explicit-any */ - throw new Error(`Error rendering ${name} pattern: ${(e as any).message}`); - } + it('should render for all kamelets without an error', async () => { + Object.entries(kameletCatalogMap).forEach(([name, kamelet]) => { + try { + if (name === 'default') return; + expect(kamelet).toBeDefined(); + /* eslint-disable @typescript-eslint/no-explicit-any */ + const schema = (kamelet as any).propertiesSchema; + const bridge = schemaService.getSchemaBridge(schema); + render( + + {}}> + + + , + ); + } catch (e) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + throw new Error(`Error rendering ${name} component: ${(e as any).message}`); + } + }); + }); + + it('should render for all patterns without an error', async () => { + Object.entries(patternCatalogMap).forEach(([name, pattern]) => { + try { + if (name === 'default') return; + expect(pattern).toBeDefined(); + /* eslint-disable @typescript-eslint/no-explicit-any */ + const schema = (pattern as any).propertiesSchema; + const bridge = schemaService.getSchemaBridge(schema); + render( + + {}}> + + + , + ); + } catch (e) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + throw new Error(`Error rendering ${name} pattern: ${(e as any).message}`); + } + }); }); }); }); diff --git a/packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx b/packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx index 3aa90ed29..d1140a5ed 100644 --- a/packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx +++ b/packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx @@ -1,5 +1,6 @@ import { Card, CardBody, CardHeader } from '@patternfly/react-core'; import { FunctionComponent, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import { VisibleFlowsContext } from '../../../providers'; import { EntitiesContext } from '../../../providers/entities.provider'; import { SchemaBridgeProvider } from '../../../providers/schema-bridge.provider'; import { isDefined, setValue } from '../../../utils'; @@ -20,9 +21,11 @@ interface CanvasFormProps { } export const CanvasForm: FunctionComponent = (props) => { + const { visualFlowsApi } = useContext(VisibleFlowsContext)!; const entitiesContext = useContext(EntitiesContext); const formRef = useRef(null); const divRef = useRef(null); + const flowIdRef = useRef(undefined); const visualComponentSchema = useMemo(() => { const answer = props.selectedNode.data?.vizNode?.getComponentSchema(); @@ -35,6 +38,11 @@ export const CanvasForm: FunctionComponent = (props) => { const model = visualComponentSchema?.definition; const title = visualComponentSchema?.title; + /** Store the flow's initial Id */ + useEffect(() => { + flowIdRef.current = props.selectedNode.data?.vizNode?.getBaseEntity()?.getId(); + }, []); + useEffect(() => { formRef.current?.form.reset(); }, [props.selectedNode.data?.vizNode]); @@ -45,7 +53,7 @@ export const CanvasForm: FunctionComponent = (props) => { return; } - const newModel = props.selectedNode.data?.vizNode?.getComponentSchema()?.definition || {}; + const newModel = props.selectedNode.data.vizNode.getComponentSchema()?.definition || {}; setValue(newModel, path, value); props.selectedNode.data.vizNode.updateModel(newModel); entitiesContext?.updateSourceCodeFromEntities(); @@ -65,6 +73,14 @@ export const CanvasForm: FunctionComponent = (props) => { return { isExpressionAwareStep, isDataFormatAwareStep, isLoadBalanceAwareStep, isUnknownComponent }; }, [visualComponentSchema]); + const onClose = useCallback(() => { + props.onClose?.(); + const newId = props.selectedNode.data?.vizNode?.getBaseEntity()?.getId(); + if (typeof flowIdRef.current === 'string' && typeof newId === 'string' && flowIdRef.current !== newId) { + visualFlowsApi.renameFlow(flowIdRef.current, newId); + } + }, [props, visualFlowsApi]); + return ( This node cannot be configured yet

}> @@ -72,7 +88,7 @@ export const CanvasForm: FunctionComponent = (props) => { diff --git a/packages/ui/src/components/Visualization/Canvas/__snapshots__/CanvasForm.test.tsx.snap b/packages/ui/src/components/Visualization/Canvas/__snapshots__/CanvasForm.test.tsx.snap index 1f30f0e90..1ab441c26 100644 --- a/packages/ui/src/components/Visualization/Canvas/__snapshots__/CanvasForm.test.tsx.snap +++ b/packages/ui/src/components/Visualization/Canvas/__snapshots__/CanvasForm.test.tsx.snap @@ -23,7 +23,8 @@ exports[`CanvasForm should render 1`] = ` > icon

- My Node + choice

+
+ +
+
+
+ + +
+ +
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+
+
+
+ +
+
+
+
+
+
{ + const visualEntity1 = new CamelRouteVisualEntity({ ...camelRouteJson.route, id: 'entity1' }); + const visualEntity2 = new CamelRouteVisualEntity({ ...camelRouteJson.route, id: 'entity2' }); + return { currentSchemaType: SourceSchemaType.Integration, - visualEntities: [{ id: 'entity1' } as CamelRouteVisualEntity, { id: 'entity2' } as CamelRouteVisualEntity], + visualEntities: [visualEntity1, visualEntity2], + updateEntitiesFromCamelResource: jest.fn(), } as unknown as EntitiesContextResult; }; const getVisibleFlowsContextValue = () => { @@ -42,14 +47,14 @@ describe('FlowsList.tsx', () => { visibleFlowsValue = getVisibleFlowsContextValue(); }); - test('should render the existing flows', async () => { + it('should render the existing flows', async () => { const wrapper = render(); const flows = await wrapper.findAllByTestId(/flows-list-row-*/); expect(flows).toHaveLength(2); }); - test('should display an empty state when there is no routes available', async () => { + it('should display an empty state when there is no routes available', async () => { contextValue = { ...contextValue, visualEntities: [] }; visibleFlowsValue = { ...visibleFlowsValue, visibleFlows: {} }; const wrapper = render(); @@ -59,7 +64,7 @@ describe('FlowsList.tsx', () => { expect(emptyState).toBeInTheDocument(); }); - test('should render the flows ids', async () => { + it('should render the flows ids', async () => { const wrapper = render(); const flow1 = await wrapper.findByText('entity1'); const flow2 = await wrapper.findByText('entity2'); @@ -68,7 +73,7 @@ describe('FlowsList.tsx', () => { expect(flow2).toBeInTheDocument(); }); - test('should make the selected flow visible by clicking on its ID', async () => { + it('should make the selected flow visible by clicking on its ID', async () => { let resId = ''; const visFlowApi = new VisualFlowsApi(jest.fn); jest.spyOn(visFlowApi, 'toggleFlowVisible').mockImplementation((id: string) => { @@ -85,7 +90,7 @@ describe('FlowsList.tsx', () => { expect(resId).toBe('entity1'); }); - test('should call onClose when clicking on a flow ID', async () => { + it('should call onClose when clicking on a flow ID', async () => { const onCloseSpy = jest.fn(); const wrapper = render( @@ -101,7 +106,7 @@ describe('FlowsList.tsx', () => { expect(onCloseSpy).toHaveBeenCalledTimes(1); }); - test('should toggle the visibility of a flow clicking on the Eye icon', async () => { + it('should toggle the visibility of a flow clicking on the Eye icon', async () => { let resId = ''; const visFlowApi = new VisualFlowsApi(jest.fn); jest.spyOn(visFlowApi, 'toggleFlowVisible').mockImplementation((id: string) => { @@ -119,7 +124,7 @@ describe('FlowsList.tsx', () => { expect(resId).toEqual('entity1'); }); - test('should render the appropriate Eye icon', async () => { + it('should render the appropriate Eye icon', async () => { const wrapper = render(); const flow1 = await wrapper.findByTestId('toggle-btn-entity1-visible'); expect(flow1).toBeInTheDocument(); @@ -128,4 +133,32 @@ describe('FlowsList.tsx', () => { const flow2 = await wrapper.findByTestId('toggle-btn-entity2-hidden'); expect(flow2).toBeInTheDocument(); }); + + it('should rename a flow', async () => { + const visualFlowsApi = new VisualFlowsApi(jest.fn); + const renameSpy = jest.spyOn(visualFlowsApi, 'renameFlow'); + + visibleFlowsValue = { ...getVisibleFlowsContextValue(), visualFlowsApi }; + const wrapper = render(); + + await act(async () => { + const entityOnePencilIcon = await wrapper.findByTestId('goto-btn-entity1--edit'); + fireEvent.click(entityOnePencilIcon); + }); + + await act(async () => { + const input = await wrapper.findByDisplayValue('entity1'); + fireEvent.change(input, { target: { value: 'new-name' } }); + fireEvent.blur(input); + }); + + await act(async () => { + const entityOnePencilIcon = await wrapper.findByTestId('goto-btn-entity1--save'); + fireEvent.click(entityOnePencilIcon); + }); + + expect(renameSpy).toHaveBeenCalledWith('entity1', 'new-name'); + expect(contextValue.visualEntities[0].id).toEqual('new-name'); + expect(contextValue.updateEntitiesFromCamelResource).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx b/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx index e5aa77af3..5c8b0db22 100644 --- a/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx +++ b/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx @@ -66,6 +66,7 @@ export const FlowsList: FunctionComponent = (props) => { onSelectFlow(flow.id); }} onChange={(name) => { + visualFlowsApi.renameFlow(flow.id, name); flow.setId(name); updateEntitiesFromCamelResource(); }} 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 d1a7a47a5..236d9e300 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,6 +72,8 @@ export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity const updatedValue = CamelComponentSchemaService.getUriSerializedDefinition(path, value); setValue(this.route, path, updatedValue); + + if (isDefined(this.route.id)) this.id = this.route.id; } /** diff --git a/packages/ui/src/models/visualization/flows/kamelet-visual-entity.ts b/packages/ui/src/models/visualization/flows/kamelet-visual-entity.ts index 52501d025..77b18008f 100644 --- a/packages/ui/src/models/visualization/flows/kamelet-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/kamelet-visual-entity.ts @@ -44,6 +44,8 @@ export class KameletVisualEntity extends AbstractCamelVisualEntity { updateModel(path: string | undefined, value: Record): void { if (path === ROOT_PATH) { updateKameletFromCustomSchema(this.kamelet, value); + this.id = this.kamelet.metadata.name; + this.route.id = this.kamelet.metadata.name; return; } diff --git a/packages/ui/src/models/visualization/flows/support/flows-visibility.test.ts b/packages/ui/src/models/visualization/flows/support/flows-visibility.test.ts new file mode 100644 index 000000000..9616b5335 --- /dev/null +++ b/packages/ui/src/models/visualization/flows/support/flows-visibility.test.ts @@ -0,0 +1,59 @@ +import { VisualFlowsApi } from './flows-visibility'; + +describe('VisualFlowsApi', () => { + let dispatch: jest.Mock; + let visualFlowsApi: VisualFlowsApi; + + beforeEach(() => { + dispatch = jest.fn(); + visualFlowsApi = new VisualFlowsApi(dispatch); + }); + + it('should toggle flow visibility', () => { + visualFlowsApi.toggleFlowVisible('flowId'); + + expect(dispatch).toHaveBeenCalledWith({ type: 'toggleFlowVisible', flowId: 'flowId', isVisible: undefined }); + }); + + it('should toggle flow visibility with a given flag', () => { + visualFlowsApi.toggleFlowVisible('flowId', true); + + expect(dispatch).toHaveBeenCalledWith({ type: 'toggleFlowVisible', flowId: 'flowId', isVisible: true }); + }); + + it('should show all flows', () => { + visualFlowsApi.showAllFlows(); + + expect(dispatch).toHaveBeenCalledWith({ type: 'showAllFlows' }); + }); + + it('should hide all flows', () => { + visualFlowsApi.hideAllFlows(); + + expect(dispatch).toHaveBeenCalledWith({ type: 'hideAllFlows' }); + }); + + it('should set visible flows', () => { + visualFlowsApi.setVisibleFlows(['flowId']); + + expect(dispatch).toHaveBeenCalledWith({ type: 'setVisibleFlows', flows: ['flowId'] }); + }); + + it('should clear flows', () => { + visualFlowsApi.clearFlows(); + + expect(dispatch).toHaveBeenCalledWith({ type: 'clearFlows' }); + }); + + it('should init visible flows', () => { + visualFlowsApi.initVisibleFlows({ flowId: true }); + + expect(dispatch).toHaveBeenCalledWith({ type: 'initVisibleFlows', visibleFlows: { flowId: true } }); + }); + + it('should rename flow', () => { + visualFlowsApi.renameFlow('flowId', 'newName'); + + expect(dispatch).toHaveBeenCalledWith({ type: 'renameFlow', flowId: 'flowId', newName: 'newName' }); + }); +}); diff --git a/packages/ui/src/models/visualization/flows/support/flows-visibility.ts b/packages/ui/src/models/visualization/flows/support/flows-visibility.ts index 83061daf8..2355a2471 100644 --- a/packages/ui/src/models/visualization/flows/support/flows-visibility.ts +++ b/packages/ui/src/models/visualization/flows/support/flows-visibility.ts @@ -43,7 +43,8 @@ type VisibleFlowAction = | { type: 'hideAllFlows' } | { type: 'clearFlows' } | { type: 'setVisibleFlows'; flows: string[] } - | { type: 'initVisibleFlows'; visibleFlows: IVisibleFlows }; + | { type: 'initVisibleFlows'; visibleFlows: IVisibleFlows } + | { type: 'renameFlow'; flowId: string; newName: string }; export function VisibleFlowsReducer(state: IVisibleFlows, action: VisibleFlowAction) { let visibleFlows; @@ -84,6 +85,16 @@ export function VisibleFlowsReducer(state: IVisibleFlows, action: VisibleFlowAct return {}; case 'initVisibleFlows': return { ...action.visibleFlows }; + + case 'renameFlow': + // eslint-disable-next-line no-case-declarations + const newState = { + ...state, + [action.newName]: state[action.flowId], + }; + delete newState[action.flowId]; + + return newState; } } @@ -117,4 +128,8 @@ export class VisualFlowsApi { initVisibleFlows(visibleFlows: IVisibleFlows) { this.dispatch({ type: 'initVisibleFlows', visibleFlows: visibleFlows }); } + + renameFlow(flowId: string, newName: string) { + this.dispatch({ type: 'renameFlow', flowId, newName }); + } }