Skip to content

Commit a18baae

Browse files
committed
Allow creation of groups with same label
1 parent 40947d6 commit a18baae

File tree

5 files changed

+121
-14
lines changed

5 files changed

+121
-14
lines changed

src/topoViewer/webview-ui/managerCopyPaste.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export class CopyPasteManager {
177177
* @param usedIds - Set of ids already used in the graph.
178178
* @param usedNames - Set of names already used in the graph.
179179
*/
180+
// eslint-disable-next-line aggregate-complexity/aggregate-complexity
180181
private createNode(el: any, usedIds: Set<string>, usedNames: Set<string>) {
181182
const isTemplateNode = el.data.id.startsWith('nodeId-');
182183
let newId: string;
@@ -191,11 +192,16 @@ export class CopyPasteManager {
191192
.reduce((max, current) => Math.max(max, current), 0);
192193
newId = `nodeId-${maxId + 1}`;
193194
} else {
194-
newId = this.getUniqueId(el.data.name || el.data.id, usedIds, el.data.topoViewerRole === 'group');
195-
nodeName = newId;
195+
const isGroup = el.data.topoViewerRole === 'group';
196+
newId = this.getUniqueId(el.data.name || el.data.id, usedIds, isGroup);
197+
if (isGroup) {
198+
nodeName = el.data.name || el.data.label || newId.split(':')[0];
199+
} else {
200+
nodeName = newId;
201+
}
196202
}
197203

198-
let nodeLabel = nodeName;
204+
let nodeLabel = el.data.label || nodeName;
199205
if (newId.startsWith('dummy')) {
200206
nodeName = 'dummy';
201207
nodeLabel = 'dummy';

src/topoViewer/webview-ui/managerGroupManagement.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -554,10 +554,19 @@ export class ManagerGroupManagement {
554554
}
555555
}
556556

557-
private ensureUniqueParentId(newParentId: string): void {
558-
if (!this.cy.getElementById(newParentId).empty()) {
559-
throw new Error(`A node with the new parent ID "${newParentId}" already exists.`);
557+
private ensureUniqueParentId(newParentId: string): string {
558+
const [group, levelStr] = newParentId.split(':');
559+
let level = parseInt(levelStr, 10);
560+
if (!level || isNaN(level)) level = 1;
561+
let candidate = `${group}:${level}`;
562+
while (!this.cy.getElementById(candidate).empty()) {
563+
level++;
564+
candidate = `${group}:${level}`;
560565
}
566+
if (candidate !== newParentId) {
567+
log.debug(`Adjusted parent ID to ensure uniqueness: ${candidate}`);
568+
}
569+
return candidate;
561570
}
562571

563572
private replaceParentNode(
@@ -619,7 +628,7 @@ export class ManagerGroupManagement {
619628
throw new Error('Graph group or graph level input is empty.');
620629
}
621630

622-
const newParentId = `${graphGroup}:${graphLevel}`;
631+
let newParentId = `${graphGroup}:${graphLevel}`;
623632
const labelPos = inputs.labelPositionEl.textContent?.trim().toLowerCase() || '';
624633
const style = this.buildParentStyle(newParentId, inputs);
625634

@@ -632,7 +641,7 @@ export class ManagerGroupManagement {
632641
return;
633642
}
634643

635-
this.ensureUniqueParentId(newParentId);
644+
newParentId = this.ensureUniqueParentId(newParentId);
636645
this.replaceParentNode(oldParentNode, parentNodeId, newParentId, graphGroup, graphLevel, style, labelPos);
637646
inputs.parentIdEl.textContent = newParentId;
638647
log.info(`Parent node updated successfully. New parent ID: ${newParentId}`);

src/topoViewer/webview-ui/utilities/idUtils.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ export function generateSpecialNodeId(baseName: string, usedIds: Set<string>): s
4747
}
4848

4949
export function generateRegularNodeId(baseName: string, usedIds: Set<string>, isGroup: boolean): string {
50+
if (isGroup) {
51+
let num = 1;
52+
let id = `${baseName}:${num}`;
53+
while (usedIds.has(id)) {
54+
num++;
55+
id = `${baseName}:${num}`;
56+
}
57+
return id;
58+
}
59+
5060
let i = baseName.length - 1;
5161
while (i >= 0 && baseName[i] >= '0' && baseName[i] <= '9') i--;
5262
const hasNumber = i < baseName.length - 1;
@@ -55,12 +65,7 @@ export function generateRegularNodeId(baseName: string, usedIds: Set<string>, is
5565
if (hasNumber) {
5666
num = parseInt(baseName.slice(i + 1), 10);
5767
} else {
58-
num = isGroup ? 0 : 1;
59-
}
60-
61-
if (isGroup) {
62-
while (usedIds.has(`${base}${num || ''}:1`)) num++;
63-
return `${base}${num || ''}:1`;
68+
num = 1;
6469
}
6570
while (usedIds.has(`${base}${num}`)) num++;
6671
return `${base}${num}`;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-env mocha */
2+
import { describe, it } from 'mocha';
3+
import { expect } from 'chai';
4+
import cytoscape from 'cytoscape';
5+
import { CopyPasteManager } from '../../../src/topoViewer/webview-ui/managerCopyPaste';
6+
import { ManagerGroupStyle } from '../../../src/topoViewer/webview-ui/managerGroupStyle';
7+
import { ManagerFreeText } from '../../../src/topoViewer/webview-ui/managerFreeText';
8+
9+
(globalThis as any).window = globalThis;
10+
11+
describe('CopyPasteManager duplicate groups', () => {
12+
it('keeps label and generates sequential ids on paste', () => {
13+
const cy = cytoscape({ headless: true, elements: [
14+
{ data: { id: 'test:1', name: 'test', label: 'test', topoViewerRole: 'group', extraData: {
15+
clabServerUsername: '', weight: '', name: '', topoViewerGroup: 'test', topoViewerGroupLevel: '1'
16+
} } }
17+
]});
18+
const messageSender = { sendMessageToVscodeEndpointPost: async () => ({}) } as any;
19+
const groupStyle = new ManagerGroupStyle(cy, messageSender);
20+
const freeText = { addFreeTextAnnotation: () => {}, getAnnotations: () => [] } as unknown as ManagerFreeText;
21+
const mgr = new CopyPasteManager(cy, messageSender, groupStyle, freeText);
22+
23+
const copyData = {
24+
elements: [
25+
{ group: 'nodes', data: { id: 'test:1', name: 'test', label: 'test', topoViewerRole: 'group', extraData: {
26+
clabServerUsername: '', weight: '', name: '', topoViewerGroup: 'test', topoViewerGroupLevel: '1'
27+
} }, position: { x: 0, y: 0 } }
28+
],
29+
annotations: { groupStyleAnnotations: [], freeTextAnnotations: [], cloudNodeAnnotations: [], nodeAnnotations: [] },
30+
originalCenter: { x: 0, y: 0 }
31+
};
32+
33+
mgr.performPaste(copyData);
34+
35+
const ids = cy.nodes().map(n => n.id());
36+
expect(ids).to.include('test:1');
37+
expect(ids).to.include('test:2');
38+
const newGroup = cy.getElementById('test:2');
39+
expect(newGroup.data('name')).to.equal('test');
40+
expect(newGroup.data('label')).to.equal('test');
41+
});
42+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-env mocha */
2+
import { describe, it } from 'mocha';
3+
import { expect } from 'chai';
4+
import cytoscape from 'cytoscape';
5+
import { ManagerGroupManagement } from '../../../src/topoViewer/webview-ui/managerGroupManagement';
6+
import { ManagerGroupStyle } from '../../../src/topoViewer/webview-ui/managerGroupStyle';
7+
8+
(globalThis as any).window = globalThis;
9+
10+
describe('ManagerGroupManagement duplicate group labels', () => {
11+
it('allows creating groups with identical labels by generating unique ids', async () => {
12+
const cy = cytoscape({ headless: true, elements: [
13+
{ data: { id: 'group1:1', name: 'group1', topoViewerRole: 'group', extraData: {
14+
clabServerUsername: '', weight: '', name: '', topoViewerGroup: 'group1', topoViewerGroupLevel: '1'
15+
} } },
16+
{ data: { id: 'group2:1', name: 'group2', topoViewerRole: 'group', extraData: {
17+
clabServerUsername: '', weight: '', name: '', topoViewerGroup: 'group2', topoViewerGroupLevel: '1'
18+
} } }
19+
]});
20+
const messageSender = { sendMessageToVscodeEndpointPost: async () => ({}) } as any;
21+
const styleManager = new ManagerGroupStyle(cy, messageSender);
22+
const mgr = new ManagerGroupManagement(cy, styleManager, 'edit');
23+
24+
const elements: Record<string, any> = {
25+
'panel-node-editor-parent-graph-group-id': { textContent: 'group2:1' },
26+
'panel-node-editor-parent-graph-group': { value: 'group1' },
27+
'panel-node-editor-parent-graph-level': { value: '1' },
28+
'panel-node-editor-parent-label-dropdown-button-text': { textContent: 'top-center' },
29+
'panel-node-editor-parent-bg-color': { value: '#d9d9d9' },
30+
'panel-node-editor-parent-border-color': { value: '#DDDDDD' },
31+
'panel-node-editor-parent-border-width': { value: '0.5' },
32+
'panel-node-editor-parent-text-color': { value: '#EBECF0' }
33+
};
34+
(globalThis as any).document = { getElementById: (id: string) => elements[id] } as any;
35+
(globalThis as any).sendMessageToVscodeEndpointPost = async () => {};
36+
37+
await mgr.nodeParentPropertiesUpdate();
38+
39+
const ids = cy.nodes().map(n => n.id());
40+
expect(ids).to.include('group1:1');
41+
expect(ids).to.include('group1:2');
42+
const group1Nodes = cy.nodes().filter(n => n.data('name') === 'group1');
43+
expect(group1Nodes.length).to.equal(2);
44+
});
45+
});

0 commit comments

Comments
 (0)