@sap-ux/control-property-editor 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Control Property Editor",
4
4
  "description": "Control Property Editor",
5
5
  "license": "Apache-2.0",
6
- "version": "0.6.2",
6
+ "version": "0.6.4",
7
7
  "main": "dist/app.js",
8
8
  "repository": {
9
9
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "@types/react": "16.14.55",
27
27
  "body-parser": "1.20.3",
28
28
  "eslint-plugin-react": "7.33.2",
29
- "http-proxy-middleware": "2.0.7",
29
+ "http-proxy-middleware": "2.0.9",
30
30
  "i18next": "20.6.1",
31
31
  "npm-run-all2": "6.2.0",
32
32
  "react": "16.14.0",
@@ -50,8 +50,8 @@
50
50
  "postcss": "8.5.1",
51
51
  "yargs-parser": "21.1.1",
52
52
  "uuid": "11.0.5",
53
- "@sap-ux/ui-components": "1.26.0",
54
- "@sap-ux-private/control-property-editor-common": "0.6.1"
53
+ "@sap-ux/ui-components": "1.26.1",
54
+ "@sap-ux-private/control-property-editor-common": "0.6.3"
55
55
  },
56
56
  "scripts": {
57
57
  "clean": "rimraf --glob dist coverage *.tsbuildinfo",
@@ -43,6 +43,8 @@ export function NestedQuickActionListItem({
43
43
  null
44
44
  );
45
45
 
46
+ const flattened = flattenAction(action);
47
+
46
48
  /**
47
49
  * Build menu items for nested quick actions.
48
50
  *
@@ -55,13 +57,12 @@ export function NestedQuickActionListItem({
55
57
  nestedLevel: number[] = []
56
58
  ): UIContextualMenuItem[] {
57
59
  return children.map((child, index) => {
58
- const hasChildren = child?.children?.length > 1;
59
- const value = child?.children?.length === 1 ? `${child.label}-${child.children[0].label}` : child.label;
60
+ const hasChildren = child.children.length > 1;
60
61
  return {
61
- key: `${value}-${index}`,
62
- text: value,
62
+ key: `${child.label}-${index}`,
63
+ text: child.label,
63
64
  disabled: !child.enabled,
64
- title: child?.tooltip ?? value,
65
+ title: child.tooltip ?? child.label,
65
66
  subMenuProps: hasChildren
66
67
  ? {
67
68
  directionalHint: UIDirectionalHint.leftTopEdge,
@@ -72,7 +73,7 @@ export function NestedQuickActionListItem({
72
73
  dispatch(
73
74
  executeQuickAction({
74
75
  kind: action.kind,
75
- path: nestedLevel.length ? `${nestedLevel.join('/')}/${index}` : index.toString(),
76
+ path: child.path,
76
77
  id: action.id
77
78
  })
78
79
  );
@@ -84,26 +85,28 @@ export function NestedQuickActionListItem({
84
85
  const buttonId = `quick-action-children-button-${groupIndex}-${actionIndex}`;
85
86
  return (
86
87
  <div className="quick-action-item">
87
- {action.children.length === 1 && (
88
+ {flattened.children.length === 1 && (
88
89
  <UILink
89
90
  underline={false}
90
- disabled={isDisabled || !action.children[0].enabled}
91
+ disabled={isDisabled || !flattened.children[0].enabled}
91
92
  title={
92
- action.children[0].tooltip ?? action.tooltip ?? `${action.title} - ${action.children[0].label}`
93
+ flattened.children[0].tooltip ??
94
+ action.tooltip ??
95
+ `${action.title} - ${flattened.children[0].label}`
93
96
  }
94
97
  onClick={(): void => {
95
98
  dispatch(
96
99
  executeQuickAction({
97
100
  kind: action.kind,
98
101
  id: action.id,
99
- path: [0].join('/')
102
+ path: flattened.children[0].path
100
103
  })
101
104
  );
102
105
  }}>
103
106
  <span className="link-text">{action.title}</span>
104
107
  </UILink>
105
108
  )}
106
- {action.children.length > 1 && (
109
+ {flattened.children.length > 1 && (
107
110
  <>
108
111
  <UILink
109
112
  title={action.tooltip ?? action.title}
@@ -132,7 +135,7 @@ export function NestedQuickActionListItem({
132
135
  showSubmenuBeneath={true}
133
136
  target={target}
134
137
  isBeakVisible={true}
135
- items={buildMenuItems(action.children)}
138
+ items={buildMenuItems(flattened.children)}
136
139
  directionalHint={UIDirectionalHint.bottomRightEdge}
137
140
  onDismiss={() => setShowContextualMenu(false)}
138
141
  iconToLeft={true}
@@ -143,3 +146,74 @@ export function NestedQuickActionListItem({
143
146
  </div>
144
147
  );
145
148
  }
149
+
150
+ /**
151
+ * Flatten nested quick action children.
152
+ *
153
+ * @param action - Nested quick action.
154
+ * @returns Quick action with flattened children.
155
+ */
156
+ function flattenAction(action: NestedQuickAction): NestedQuickAction {
157
+ const result = structuredClone(action);
158
+ const stack: { node: NestedQuickActionChild; parent?: NestedQuickActionChild }[] = result.children.map((child) => ({
159
+ node: child,
160
+ parent: undefined
161
+ }));
162
+ const mergeTuples: [string | undefined, string, string][] = [];
163
+ const lookup = new Map<string, NestedQuickActionChild>();
164
+
165
+ while (stack.length > 0) {
166
+ const current = stack.pop();
167
+ if (!current) {
168
+ continue;
169
+ }
170
+ const { node, parent } = current;
171
+ lookup.set(node.path, node);
172
+ if (node.children.length === 1) {
173
+ const child = node.children[0];
174
+ mergeTuples.push([parent?.path ?? '', node.path, child.path]);
175
+ }
176
+ for (const child of node.children) {
177
+ stack.push({ node: child, parent: node });
178
+ }
179
+ }
180
+
181
+ mergeNodes(result, mergeTuples, lookup);
182
+
183
+ if (result.children.length === 1 && result.children[0].children.length > 0) {
184
+ const parent = result.children[0];
185
+ result.children = parent.children.map((child) => ({
186
+ ...child,
187
+ label: `${parent.label}-${child.label}`
188
+ }));
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Merge nodes in the nested quick action.
196
+ *
197
+ * @param result - Nested quick action.
198
+ * @param mergeTuples - Array of tuples containing parent, start and end node paths.
199
+ * @param lookup - Lookup map of node paths to nested quick action children.
200
+ */
201
+ function mergeNodes(
202
+ result: NestedQuickAction,
203
+ mergeTuples: [string | undefined, string, string][],
204
+ lookup: Map<string, NestedQuickActionChild>
205
+ ): void {
206
+ for (const [parent, start, end] of mergeTuples) {
207
+ const parentNode = parent ? lookup.get(parent) : result;
208
+ const startNode = lookup.get(start);
209
+ const endNode = lookup.get(end);
210
+ if (!parentNode || !startNode || !endNode) {
211
+ continue;
212
+ }
213
+ endNode.label = `${startNode.label}-${endNode.label}`;
214
+ const index = parentNode.children.findIndex((parentChild) => parentChild === startNode);
215
+ if (index !== -1) {
216
+ parentNode.children[index] = endNode;
217
+ }
218
+ }
219
+ }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { screen, fireEvent } from '@testing-library/react';
3
3
 
4
4
  import { render } from '../../utils';
5
- import type { NestedQuickActionChild } from '@sap-ux-private/control-property-editor-common';
5
+ import type { NestedQuickAction, NestedQuickActionChild } from '@sap-ux-private/control-property-editor-common';
6
6
  import { executeQuickAction } from '@sap-ux-private/control-property-editor-common';
7
7
  import { QuickActionList } from '../../../../src/panels/quick-actions';
8
8
 
@@ -10,20 +10,24 @@ describe('QuickActionList', () => {
10
10
  test('check if quick action list rendered', () => {
11
11
  const children: NestedQuickActionChild[] = [
12
12
  {
13
+ path: '0',
13
14
  label: 'submenu1',
14
15
  enabled: true,
15
16
  children: []
16
17
  },
17
18
  {
19
+ path: '1',
18
20
  label: 'submenu2',
19
21
  enabled: true,
20
22
  children: [
21
23
  {
24
+ path: '1/0',
22
25
  label: 'submenu2-submenu1',
23
26
  enabled: true,
24
27
  children: []
25
28
  },
26
29
  {
30
+ path: '1/1',
27
31
  label: 'submenu2-submenu2',
28
32
  enabled: true,
29
33
  children: []
@@ -128,6 +132,7 @@ describe('QuickActionList', () => {
128
132
  test('disable actions in navigation mode', () => {
129
133
  const children = [
130
134
  {
135
+ path: '0',
131
136
  label: 'submenu1',
132
137
  enabled: true,
133
138
  children: []
@@ -188,6 +193,7 @@ describe('QuickActionList', () => {
188
193
  test('disable actions in navigation mode', () => {
189
194
  const children1: NestedQuickActionChild[] = [
190
195
  {
196
+ path: '0',
191
197
  label: 'submenu1',
192
198
  enabled: false,
193
199
  children: [],
@@ -197,11 +203,13 @@ describe('QuickActionList', () => {
197
203
 
198
204
  const children2: NestedQuickActionChild[] = [
199
205
  {
206
+ path: '0',
200
207
  label: 'submenu1',
201
208
  enabled: true,
202
209
  children: []
203
210
  },
204
211
  {
212
+ path: '1',
205
213
  label: 'submenu2',
206
214
  enabled: false,
207
215
  children: [],
@@ -278,4 +286,393 @@ describe('QuickActionList', () => {
278
286
  expect(quickAction4.title).toBe('Disabled action 4');
279
287
  });
280
288
  });
289
+
290
+ describe('nested quick action flattening', () => {
291
+ const fixture: NestedQuickAction = {
292
+ kind: 'nested',
293
+ id: 'root-action',
294
+ title: 'Root Action',
295
+ enabled: true,
296
+ children: [
297
+ {
298
+ label: 'Child 1',
299
+ enabled: true,
300
+ path: 'child1',
301
+ children: [
302
+ {
303
+ label: 'Child 1.1',
304
+ enabled: true,
305
+ path: 'child1.1',
306
+ children: [
307
+ {
308
+ label: 'Child 1.1.1',
309
+ enabled: true,
310
+ path: 'child1.1.1',
311
+ children: []
312
+ },
313
+ {
314
+ label: 'Child 1.1.2',
315
+ enabled: true,
316
+ path: 'child1.1.2',
317
+ children: []
318
+ }
319
+ ]
320
+ },
321
+ {
322
+ label: 'Child 1.2',
323
+ enabled: true,
324
+ path: 'child1.2',
325
+ children: [
326
+ {
327
+ label: 'Child 1.2.1',
328
+ enabled: true,
329
+ path: 'child1.2.1',
330
+ children: []
331
+ },
332
+ {
333
+ label: 'Child 1.2.2',
334
+ enabled: true,
335
+ path: 'child1.2.2',
336
+ children: []
337
+ }
338
+ ]
339
+ }
340
+ ]
341
+ },
342
+ {
343
+ label: 'Child 2',
344
+ enabled: true,
345
+ path: 'child2',
346
+ children: [
347
+ {
348
+ label: 'Child 2.1',
349
+ enabled: true,
350
+ path: 'child2.1',
351
+ children: [
352
+ {
353
+ label: 'Child 2.1.1',
354
+ enabled: true,
355
+ path: 'child2.1.1',
356
+ children: []
357
+ },
358
+ {
359
+ label: 'Child 2.1.2',
360
+ enabled: true,
361
+ path: 'child2.1.2',
362
+ children: []
363
+ }
364
+ ]
365
+ },
366
+ {
367
+ label: 'Child 2.2',
368
+ enabled: true,
369
+ path: 'child2.2',
370
+ children: [
371
+ {
372
+ label: 'Child 2.2.1',
373
+ enabled: true,
374
+ path: 'child2.2.1',
375
+ children: []
376
+ },
377
+ {
378
+ label: 'Child 2.2.2',
379
+ enabled: true,
380
+ path: 'child2.2.2',
381
+ children: []
382
+ }
383
+ ]
384
+ }
385
+ ]
386
+ }
387
+ ]
388
+ };
389
+
390
+ test('drop one level', () => {
391
+ const element = <QuickActionList />;
392
+ render(element, {
393
+ initialState: {
394
+ quickActions: [
395
+ {
396
+ title: 'List Report',
397
+ actions: [
398
+ {
399
+ id: 'quick-action-1',
400
+ enabled: true,
401
+ kind: 'nested',
402
+ title: 'Quick Action 1',
403
+ children: [
404
+ {
405
+ path: '0',
406
+ label: 'submenu0',
407
+ enabled: true,
408
+ children: [
409
+ {
410
+ path: '0/0',
411
+ label: 'submenu0-0',
412
+ enabled: true,
413
+ children: []
414
+ },
415
+ {
416
+ path: '0/1',
417
+ label: 'submenu0-1',
418
+ enabled: true,
419
+ children: []
420
+ }
421
+ ]
422
+ }
423
+ ]
424
+ }
425
+ ]
426
+ }
427
+ ]
428
+ }
429
+ });
430
+
431
+ // nested quick action - multiple children
432
+ let quickAction = screen.getByRole('button', { name: /quick action 1/i });
433
+ quickAction.click();
434
+ quickAction = screen.getByRole('button', { name: /quick action 1/i });
435
+ expect(quickAction).toBeEnabled();
436
+
437
+ const child1 = screen.getByRole('menuitem', { name: /submenu0-submenu0-0/i });
438
+ expect(child1.getAttribute('aria-disabled')).toBe('false');
439
+
440
+ const child2 = screen.getByRole('menuitem', { name: /submenu0-submenu0-1/i });
441
+ expect(child2.getAttribute('aria-disabled')).toBe('false');
442
+ });
443
+
444
+ test('two leaf nodes without siblings', () => {
445
+ const element = <QuickActionList />;
446
+ render(element, {
447
+ initialState: {
448
+ quickActions: [
449
+ {
450
+ title: 'List Report',
451
+ actions: [
452
+ {
453
+ id: 'quick-action-1',
454
+ enabled: true,
455
+ kind: 'nested',
456
+ title: 'Quick Action 1',
457
+ children: [
458
+ {
459
+ path: '0',
460
+ label: 'submenu0',
461
+ enabled: true,
462
+ children: [
463
+ {
464
+ path: '0/0',
465
+ label: 'submenu0.0',
466
+ enabled: true,
467
+ children: []
468
+ }
469
+ ]
470
+ },
471
+ {
472
+ path: '1',
473
+ label: 'submenu1',
474
+ enabled: true,
475
+ children: [
476
+ {
477
+ path: '1/0',
478
+ label: 'submenu1.0',
479
+ enabled: true,
480
+ children: []
481
+ }
482
+ ]
483
+ }
484
+ ]
485
+ }
486
+ ]
487
+ }
488
+ ]
489
+ }
490
+ });
491
+
492
+ // nested quick action - multiple children
493
+ let quickAction = screen.getByRole('button', { name: /quick action 1/i });
494
+ quickAction.click();
495
+ quickAction = screen.getByRole('button', { name: /quick action 1/i });
496
+ expect(quickAction).toBeEnabled();
497
+
498
+ const child1 = screen.getByRole('menuitem', { name: /submenu0-submenu0.0/i });
499
+ expect(child1.getAttribute('aria-disabled')).toBe('false');
500
+
501
+ const child2 = screen.getByRole('menuitem', { name: /submenu1-submenu1.0/i });
502
+ expect(child2.getAttribute('aria-disabled')).toBe('false');
503
+ });
504
+ test('single disabled action', () => {
505
+ const element = <QuickActionList />;
506
+ render(element, {
507
+ initialState: {
508
+ quickActions: [
509
+ {
510
+ title: 'List Report',
511
+ actions: [
512
+ {
513
+ id: 'quick-action-1',
514
+ enabled: true,
515
+ kind: 'nested',
516
+ title: 'Quick Action 1',
517
+ tooltip: 'wrong1',
518
+ children: [
519
+ {
520
+ path: '0',
521
+ label: 'submenu0',
522
+ tooltip: 'wrong2',
523
+ enabled: true,
524
+ children: [
525
+ {
526
+ path: '0/0',
527
+ label: 'submenu0.0',
528
+ tooltip: 'correct',
529
+ enabled: false,
530
+ children: []
531
+ }
532
+ ]
533
+ }
534
+ ]
535
+ }
536
+ ]
537
+ }
538
+ ]
539
+ }
540
+ });
541
+
542
+ const quickAction = screen.getByRole('button', { name: /quick action 1/i });
543
+ expect(quickAction.getAttribute('title')).toStrictEqual('correct');
544
+ expect(quickAction).toBeDisabled();
545
+ });
546
+ test('one leaf nodes without siblings', () => {
547
+ const element = <QuickActionList />;
548
+ render(element, {
549
+ initialState: {
550
+ quickActions: [
551
+ {
552
+ title: 'List Report',
553
+ actions: [
554
+ {
555
+ id: 'quick-action-1',
556
+ enabled: true,
557
+ kind: 'nested',
558
+ title: 'Quick Action 1',
559
+ children: [
560
+ {
561
+ path: '0',
562
+ label: 'submenu0',
563
+ enabled: true,
564
+ children: [
565
+ {
566
+ path: '0/0',
567
+ label: 'submenu0.0',
568
+ enabled: true,
569
+ children: []
570
+ }
571
+ ]
572
+ },
573
+ {
574
+ path: '1',
575
+ label: 'submenu1',
576
+ enabled: true,
577
+ children: [
578
+ {
579
+ path: '1/0',
580
+ label: 'submenu1.0',
581
+ enabled: true,
582
+ children: []
583
+ },
584
+ {
585
+ path: '1/1',
586
+ label: 'submenu1.1',
587
+ enabled: true,
588
+ children: []
589
+ }
590
+ ]
591
+ }
592
+ ]
593
+ }
594
+ ]
595
+ }
596
+ ]
597
+ }
598
+ });
599
+
600
+ // nested quick action - multiple children
601
+ let quickAction = screen.getByRole('button', { name: /quick action 1/i });
602
+ quickAction.click();
603
+ quickAction = screen.getByRole('button', { name: /quick action 1/i });
604
+ expect(quickAction).toBeEnabled();
605
+
606
+ const child1 = screen.getByRole('menuitem', { name: /submenu0-submenu0.0/i });
607
+ expect(child1.getAttribute('aria-disabled')).toBe('false');
608
+
609
+ screen.getByRole('menuitem', { name: /submenu1/i }).click();
610
+
611
+ const child2 = screen.getByRole('menuitem', { name: /submenu1.0/i });
612
+ expect(child2.getAttribute('aria-disabled')).toBe('false');
613
+ const child3 = screen.getByRole('menuitem', { name: /submenu1.1/i });
614
+ expect(child3.getAttribute('aria-disabled')).toBe('false');
615
+ });
616
+
617
+ test('drop two levels', () => {
618
+ const element = <QuickActionList />;
619
+ render(element, {
620
+ initialState: {
621
+ quickActions: [
622
+ {
623
+ title: 'List Report',
624
+ actions: [
625
+ {
626
+ id: 'quick-action-1',
627
+ enabled: true,
628
+ kind: 'nested',
629
+ title: 'Quick Action 1',
630
+ children: [
631
+ {
632
+ path: '0',
633
+ label: 'submenu0',
634
+ enabled: true,
635
+ children: [
636
+ {
637
+ path: '0/0',
638
+ label: 'submenu0.0',
639
+ enabled: true,
640
+ children: [
641
+ {
642
+ path: '0/0/0',
643
+ label: 'submenu0.0.0',
644
+ enabled: true,
645
+ children: []
646
+ },
647
+ {
648
+ path: '0/0/1',
649
+ label: 'submenu0.0.1',
650
+ enabled: true,
651
+ children: []
652
+ }
653
+ ]
654
+ }
655
+ ]
656
+ }
657
+ ]
658
+ }
659
+ ]
660
+ }
661
+ ]
662
+ }
663
+ });
664
+
665
+ // nested quick action - multiple children
666
+ let quickAction = screen.getByRole('button', { name: /quick action 1/i });
667
+ quickAction.click();
668
+ quickAction = screen.getByRole('button', { name: /quick action 1/i });
669
+ expect(quickAction).toBeEnabled();
670
+
671
+ const child1 = screen.getByRole('menuitem', { name: /submenu0-submenu0.0-submenu0.0.0/i });
672
+ expect(child1.getAttribute('aria-disabled')).toBe('false');
673
+
674
+ const child2 = screen.getByRole('menuitem', { name: /submenu0-submenu0.0-submenu0.0.1/i });
675
+ expect(child2.getAttribute('aria-disabled')).toBe('false');
676
+ });
677
+ });
281
678
  });
@@ -8,3 +8,6 @@ mockResizeObserver();
8
8
  initI18n();
9
9
  registerAppIcons();
10
10
  initIcons();
11
+
12
+ // structuredClone is not available in jsdom
13
+ global.structuredClone = (v) => JSON.parse(JSON.stringify(v));
@@ -708,15 +708,18 @@ describe('main redux slice', () => {
708
708
  kind: 'nested',
709
709
  children: [
710
710
  {
711
+ path: '0',
711
712
  label: 'test label',
712
713
  enabled: true,
713
714
  children: [
714
715
  {
716
+ path: '0/0',
715
717
  label: 'test label 2',
716
718
  enabled: true,
717
719
  children: []
718
720
  },
719
721
  {
722
+ path: '0/1',
720
723
  label: 'test label 3',
721
724
  enabled: true,
722
725
  children: []
@@ -752,15 +755,18 @@ describe('main redux slice', () => {
752
755
  kind: 'nested',
753
756
  children: [
754
757
  {
758
+ path: '0',
755
759
  enabled: true,
756
760
  label: 'test label',
757
761
  children: [
758
762
  {
763
+ path: '0/0',
759
764
  enabled: true,
760
765
  label: 'test label 2',
761
766
  children: []
762
767
  },
763
768
  {
769
+ path: '0/1',
764
770
  enabled: true,
765
771
  label: 'test label 3',
766
772
  children: []