Skip to content

Commit 8d9cc8d

Browse files
committed
feat(Canvas): copy-paste yaml support
1 parent c88af8b commit 8d9cc8d

42 files changed

Lines changed: 1104 additions & 981 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/ui-tests/cypress/e2e/designer/basicNodeActions/stepCopy.cy.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
1010

1111
cy.selectCopyNode('timer');
1212
cy.assertValueCopiedToClipboard({
13-
type: 'Route',
1413
name: 'from',
1514
definition: {
1615
uri: 'timer:test',
@@ -24,14 +23,12 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
2423

2524
cy.selectCopyNode('setHeader');
2625
cy.assertValueCopiedToClipboard({
27-
type: 'Route',
2826
name: 'setHeader',
2927
definition: { constant: 'test', name: 'test' },
3028
});
3129

3230
cy.selectCopyNode('camel-route');
3331
cy.assertValueCopiedToClipboard({
34-
type: 'Route',
3532
name: 'route',
3633
definition: {
3734
id: 'camel-route',
@@ -53,7 +50,6 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
5350

5451
cy.selectCopyNode('timer-source');
5552
cy.assertValueCopiedToClipboard({
56-
type: 'Pipe',
5753
name: 'timer-source',
5854
definition: {
5955
properties: { message: 'hello' },
@@ -63,14 +59,12 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
6359

6460
cy.selectCopyNode('delay-action');
6561
cy.assertValueCopiedToClipboard({
66-
type: 'Pipe',
6762
name: 'delay-action',
6863
definition: { ref: { apiVersion: 'camel.apache.org/v1', kind: 'Kamelet', name: 'delay-action' } },
6964
});
7065

7166
cy.selectCopyNode('log-sink');
7267
cy.assertValueCopiedToClipboard({
73-
type: 'Pipe',
7468
name: 'log-sink',
7569
definition: { ref: { apiVersion: 'camel.apache.org/v1', kind: 'Kamelet', name: 'log-sink' } },
7670
});
@@ -82,25 +76,48 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
8276

8377
cy.selectCopyNode('kamelet:sink');
8478
cy.assertValueCopiedToClipboard({
85-
type: 'Kamelet',
8679
name: 'to',
8780
definition: { uri: 'kamelet:sink', parameters: {} },
8881
});
8982

9083
cy.selectCopyNode('eip-action');
9184
cy.assertValueCopiedToClipboard({
92-
type: 'Kamelet',
93-
name: 'template',
85+
name: 'kamelet',
9486
definition: {
95-
from: {
96-
id: 'from-1870',
97-
uri: 'timer:user',
98-
parameters: { period: '{{period}}' },
99-
steps: [
100-
{ setBody: { id: 'setBody-3387', expression: { simple: {} } } },
101-
{ marshal: { id: 'marshal-1414' } },
102-
{ to: { uri: 'kamelet:sink', parameters: {} } },
103-
],
87+
apiVersion: 'camel.apache.org/v1alpha1',
88+
kind: 'Kamelet',
89+
metadata: {
90+
annotations: { 'camel.apache.org/kamelet.icon': 'whatever' },
91+
labels: { 'camel.apache.org/kamelet.type': 'action' },
92+
name: 'eip-action',
93+
},
94+
spec: {
95+
definition: {
96+
title: 'kamelet-2082',
97+
type: 'object',
98+
properties: {
99+
period: {
100+
title: 'Period',
101+
description: 'The time interval between two events',
102+
type: 'integer',
103+
default: 5000,
104+
},
105+
},
106+
},
107+
types: { out: { mediaType: 'application/json' } },
108+
dependencies: ['camel:timer', 'camel:http', 'camel:kamelet'],
109+
template: {
110+
from: {
111+
id: 'from-1870',
112+
uri: 'timer:user',
113+
parameters: { period: '{{period}}' },
114+
steps: [
115+
{ setBody: { id: 'setBody-3387', expression: { simple: {} } } },
116+
{ marshal: { id: 'marshal-1414' } },
117+
{ to: { uri: 'kamelet:sink', parameters: {} } },
118+
],
119+
},
120+
},
104121
},
105122
},
106123
});
@@ -112,7 +129,6 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
112129

113130
cy.selectCopyNode('errorHandler');
114131
cy.assertValueCopiedToClipboard({
115-
type: 'Route',
116132
name: 'errorHandler',
117133
definition: {
118134
defaultErrorHandler: {
@@ -137,7 +153,6 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
137153

138154
cy.selectCopyNode('routeConfiguration');
139155
cy.assertValueCopiedToClipboard({
140-
type: 'Route',
141156
name: 'routeConfiguration',
142157
definition: {
143158
id: 'routeConfiguration-6082',

packages/ui-tests/cypress/e2e/designer/basicNodeActions/stepPaste.cy.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,23 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
8787
});
8888
});
8989

90-
it('Design - Paste steps in CamelRoute by adding something to the clipboad', () => {
90+
it('Design - Paste steps in CamelRoute by adding something to the clipboard', () => {
9191
cy.uploadFixture('flows/camelRoute/basic.yaml');
92-
cy.addValueToClipboard({
93-
type: 'Route',
94-
name: 'to',
95-
definition: { id: 'to-1913', uri: 'amqp', parameters: {} },
96-
__kaoto_marker: 'kaoto-node',
92+
cy.window().then(async (win) => {
93+
const yamlContent = `
94+
- to:
95+
id: to-1913
96+
uri: amqp
97+
parameters: {}
98+
`;
99+
if (win.navigator?.clipboard?.writeText) {
100+
await win.navigator.clipboard.writeText(yamlContent);
101+
}
97102
});
98103
cy.openDesignPage();
99104

100105
// workaround for https://github.com/KaotoIO/kaoto/issues/2885
101-
cy.forcePerformNodeAction('marshal', `paste-as-next-step`);
106+
cy.forcePerformNodeAction('marshal', 'paste-as-next-step');
102107
cy.checkNodeExist('amqp', 1);
103108

104109
cy.openSourceCode();
@@ -108,4 +113,29 @@ describe('Tests for Design page', { browser: '!firefox' }, () => {
108113
expect(uriMatches).to.have.lengthOf(1);
109114
});
110115
});
116+
117+
it('Design - Paste step from external YAML source (no Kaoto marker)', () => {
118+
cy.uploadFixture('flows/camelRoute/basic.yaml');
119+
// Simulate clipboard content written by an external editor — raw YAML, no __kaoto_marker
120+
cy.window().then(async (win) => {
121+
const externalYaml = `
122+
- log:
123+
message: hello from external
124+
`;
125+
if (win.navigator?.clipboard?.writeText) {
126+
await win.navigator.clipboard.writeText(externalYaml);
127+
}
128+
});
129+
cy.openDesignPage();
130+
131+
// workaround for https://github.com/KaotoIO/kaoto/issues/2885
132+
cy.forcePerformNodeAction('marshal', 'paste-as-next-step');
133+
cy.checkNodeExist('log', 2);
134+
135+
cy.openSourceCode();
136+
cy.getMonacoValue().then(({ sourceCode }) => {
137+
const logMatches = sourceCode.match(/- log:/g) ?? [];
138+
expect(logMatches).to.have.lengthOf(1);
139+
});
140+
});
111141
});

packages/ui-tests/cypress/support/next-commands/default.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import catalogLibrary from '@kaoto/camel-catalog/index.json';
2+
import { parse } from 'yaml';
23

34
Cypress.Commands.add('openHomePage', () => {
45
const url = Cypress.config().baseUrl;
@@ -391,21 +392,22 @@ Cypress.Commands.add('assertValueCopiedToClipboard', (value) => {
391392
for (const item of text) {
392393
if (item.types.includes('web text/kaoto')) {
393394
const blob = await item.getType('web text/kaoto');
394-
const parsedContent = JSON.parse(await blob.text());
395+
const yamlText = await blob.text();
396+
const parsedContent = parse(yamlText);
395397

396-
expect(parsedContent).to.deep.equal(value);
398+
// Normalize parsed YAML to IClipboardContent format
399+
const clipboardContent = normalizeClipboardContent(parsedContent);
400+
expect(clipboardContent).to.deep.equal(value);
397401
}
398402

399403
if (item.types.includes('text/plain')) {
400404
const blob = await item.getType('text/plain');
401-
const parsedContent = JSON.parse(await blob.text());
405+
const yamlText = await blob.text();
406+
const parsedContent = parse(yamlText);
402407

403-
// Validate the marker to ensure it's Kaoto-specific content
404-
if (parsedContent.__kaoto_marker === 'kaoto-node') {
405-
delete parsedContent.__kaoto_marker;
406-
}
407-
408-
expect(parsedContent).to.deep.equal(value);
408+
// Normalize parsed YAML to IClipboardContent format
409+
const clipboardContent = normalizeClipboardContent(parsedContent);
410+
expect(clipboardContent).to.deep.equal(value);
409411
}
410412
}
411413
});
@@ -416,6 +418,32 @@ Cypress.Commands.add('assertValueCopiedToClipboard', (value) => {
416418
});
417419
});
418420

421+
// Helper to normalize YAML clipboard content to IClipboardContent format
422+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
423+
function normalizeClipboardContent(parsed: any): { name: string; definition: object } | null {
424+
if (!parsed || typeof parsed !== 'object') return null;
425+
426+
// Handle array format: [{ stepName: { ... } }]
427+
if (Array.isArray(parsed)) {
428+
if (parsed.length !== 1 || typeof parsed[0] !== 'object' || parsed[0] === null || Array.isArray(parsed[0])) {
429+
return null;
430+
}
431+
const keys = Object.keys(parsed[0]);
432+
if (keys.length !== 1) return null;
433+
const name = keys[0];
434+
const definition = parsed[0][name];
435+
return { name, definition: definition ?? {} };
436+
}
437+
438+
// Handle object format: { stepName: { ... } }
439+
const keys = Object.keys(parsed);
440+
if (keys.length !== 1) return null;
441+
442+
const name = keys[0];
443+
const definition = parsed[name];
444+
return { name, definition: definition ?? {} };
445+
}
446+
419447
Cypress.Commands.add('addValueToClipboard', (value) => {
420448
cy.window().then(async (win) => {
421449
if (win.navigator?.clipboard?.writeText) {

packages/ui/src/components/DataMapper/on-copy-datamapper.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { IVisualizationNode } from '../../models';
2-
import { SourceSchemaType } from '../../models/camel/source-schema-type';
3-
import { IClipboardCopyObject } from '../../models/visualization/clipboard';
2+
import { IClipboardContent } from '../../models/visualization/clipboard';
43
import { onCopyDataMapper } from './on-copy-datamapper';
54

65
describe('onCopyDataMapper', () => {
@@ -11,8 +10,7 @@ describe('onCopyDataMapper', () => {
1110
});
1211

1312
it('should fix the processor name from kaoto-datamapper to step', () => {
14-
const content: IClipboardCopyObject = {
15-
type: SourceSchemaType.Route,
13+
const content: IClipboardContent = {
1614
name: 'kaoto-datamapper',
1715
definition: {
1816
step: {
@@ -49,8 +47,7 @@ describe('onCopyDataMapper', () => {
4947
});
5048

5149
it('should not modify content if name is not kaoto-datamapper', () => {
52-
const content: IClipboardCopyObject = {
53-
type: SourceSchemaType.Route,
50+
const content: IClipboardContent = {
5451
name: 'log',
5552
definition: {
5653
log: {

packages/ui/src/components/DataMapper/on-copy-datamapper.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { IVisualizationNode } from '../../models';
2-
import { IClipboardCopyObject } from '../../models/visualization/clipboard';
2+
import { IClipboardContent } from '../../models/visualization/clipboard';
33

44
export const onCopyDataMapper = (parameters: {
55
sourceVizNode: IVisualizationNode;
6-
content: IClipboardCopyObject | undefined;
7-
}): IClipboardCopyObject | undefined => {
6+
content: IClipboardContent | undefined;
7+
}): IClipboardContent | undefined => {
88
if (!parameters.content) return undefined;
99

1010
if (parameters.content.name === 'kaoto-datamapper') {

packages/ui/src/components/DataMapper/on-duplicate-datamapper.test.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { IVisualizationNode } from '../../models';
2-
import { SourceSchemaType } from '../../models/camel/source-schema-type';
32
import { DocumentDefinitionType } from '../../models/datamapper';
43
import { IDataMapperMetadata } from '../../models/datamapper/metadata';
5-
import { IClipboardCopyObject } from '../../models/visualization/clipboard';
4+
import { IClipboardContent } from '../../models/visualization/clipboard';
65
import { IMetadataApi } from '../../providers';
76
import { DataMapperMetadataService } from '../../services/datamapper-metadata.service';
87
import { DataMapperStepService } from '../../services/datamapper-step.service';
@@ -49,8 +48,7 @@ describe('onDuplicateDataMapper', () => {
4948
mockApi.getMetadata.mockResolvedValue(originalMetadata);
5049
mockApi.getResourceContent.mockResolvedValue(xsltContent);
5150

52-
const content: IClipboardCopyObject = {
53-
type: SourceSchemaType.Route,
51+
const content: IClipboardContent = {
5452
name: 'step',
5553
definition: {
5654
id: newStepId,
@@ -84,8 +82,7 @@ describe('onDuplicateDataMapper', () => {
8482
});
8583

8684
it('should handle missing new step ID gracefully', async () => {
87-
const content: IClipboardCopyObject = {
88-
type: SourceSchemaType.Route,
85+
const content: IClipboardContent = {
8986
name: 'step',
9087
definition: {},
9188
};
@@ -98,8 +95,7 @@ describe('onDuplicateDataMapper', () => {
9895
it('should create empty metadata and set XSLT URI to empty when original metadata is not found', async () => {
9996
mockApi.getMetadata.mockResolvedValue(undefined);
10097

101-
const content: IClipboardCopyObject = {
102-
type: SourceSchemaType.Route,
98+
const content: IClipboardContent = {
10399
name: 'step',
104100
definition: {
105101
id: newStepId,
@@ -127,8 +123,7 @@ describe('onDuplicateDataMapper', () => {
127123
mockApi.getMetadata.mockResolvedValue(originalMetadata);
128124
mockApi.getResourceContent.mockResolvedValue(undefined);
129125

130-
const content: IClipboardCopyObject = {
131-
type: SourceSchemaType.Route,
126+
const content: IClipboardContent = {
132127
name: 'step',
133128
definition: {
134129
id: newStepId,

packages/ui/src/components/DataMapper/on-duplicate-datamapper.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Step } from '@kaoto/camel-catalog/types';
22

33
import { IVisualizationNode } from '../../models';
44
import { IDataMapperMetadata } from '../../models/datamapper/metadata';
5-
import { IClipboardCopyObject } from '../../models/visualization/clipboard';
5+
import { IClipboardContent } from '../../models/visualization/clipboard';
66
import { IMetadataApi } from '../../providers';
77
import { DataMapperMetadataService } from '../../services/datamapper-metadata.service';
88
import { DataMapperStepService } from '../../services/datamapper-step.service';
@@ -12,9 +12,9 @@ export const onDuplicateDataMapper = async (
1212
api: IMetadataApi | undefined,
1313
parameters: {
1414
sourceVizNode: IVisualizationNode;
15-
content: IClipboardCopyObject | undefined;
15+
content: IClipboardContent | undefined;
1616
},
17-
): Promise<IClipboardCopyObject | undefined> => {
17+
): Promise<IClipboardContent | undefined> => {
1818
if (!parameters.content) return parameters.content;
1919

2020
const stepDef = parameters.content.definition as Step;

0 commit comments

Comments
 (0)