@sap-ux/control-property-editor 0.2.4 → 0.3.1

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.2.4",
6
+ "version": "0.3.1",
7
7
  "main": "dist/app.js",
8
8
  "repository": {
9
9
  "type": "git",
@@ -47,10 +47,10 @@
47
47
  "esbuild-plugin-copy": "2.1.1",
48
48
  "@esbuild-plugins/node-modules-polyfill": "0.1.4",
49
49
  "autoprefixer": "10.4.7",
50
- "postcss": "8.4.14",
50
+ "postcss": "8.4.31",
51
51
  "yargs-parser": "21.1.1",
52
- "@sap-ux/ui-components": "1.11.19",
53
- "@sap-ux-private/control-property-editor-common": "0.1.0"
52
+ "@sap-ux/ui-components": "1.11.21",
53
+ "@sap-ux-private/control-property-editor-common": "0.2.0"
54
54
  },
55
55
  "scripts": {
56
56
  "clean": "rimraf ./dist ./coverage *.tsbuildinfo",
package/src/middleware.ts CHANGED
@@ -6,7 +6,8 @@ import {
6
6
  startPostMessageCommunication,
7
7
  changeProperty as externalChangeProperty,
8
8
  selectControl,
9
- deletePropertyChanges
9
+ deletePropertyChanges,
10
+ addExtensionPoint
10
11
  } from '@sap-ux-private/control-property-editor-common';
11
12
 
12
13
  import type { Action } from './actions';
@@ -47,6 +48,10 @@ export const communicationMiddleware: Middleware<Dispatch<Action>> = (store: Mid
47
48
  sendAction(action);
48
49
  break;
49
50
  }
51
+ case addExtensionPoint.type: {
52
+ sendAction(action);
53
+ break;
54
+ }
50
55
  default:
51
56
  }
52
57
  return action;
@@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux';
6
6
  import styles from './UnknownChange.module.scss';
7
7
  import { UIIconButton, UiIcons, UIDialog } from '@sap-ux/ui-components';
8
8
  import type { PropertyChangeDeletionDetails } from '@sap-ux-private/control-property-editor-common';
9
- import { deletePropertyChanges, convertCamelCaseToPascalCase } from '@sap-ux-private/control-property-editor-common';
9
+ import { convertCamelCaseToPascalCase, deletePropertyChanges } from '@sap-ux-private/control-property-editor-common';
10
10
  import { getFormattedDateAndTime } from './utils';
11
11
 
12
12
  export interface UnknownChangeProps {
@@ -69,6 +69,50 @@
69
69
  opacity: 0.55;
70
70
  }
71
71
 
72
+ .extension-icon {
73
+ margin-top: 2px;
74
+ }
75
+
76
+ .tooltip-container {
77
+ position: relative;
78
+ }
79
+
80
+ .tooltip {
81
+ background: var(--vscode-badge-background);
82
+ color: #fff;
83
+ width: auto;
84
+ padding: 0px 2px;
85
+ margin-left: -40px; /* Center the tooltip */
86
+ bottom: 95%; /* Position the tooltip above the text */
87
+ left: 50%;
88
+ border-radius: 4px;
89
+ position: absolute;
90
+ text-align: center;
91
+ z-index: 999;
92
+ opacity: 0;
93
+ visibility: hidden;
94
+ transition: opacity 0.3s;
95
+
96
+ button {
97
+ background-color: var(--vscode-textLink-foreground);
98
+ border: none;
99
+ color: white;
100
+ padding: 4px 8px;
101
+ text-align: center;
102
+ text-decoration: none;
103
+ display: inline-block;
104
+ font-size: 11px;
105
+ margin: 4px 2px;
106
+ cursor: pointer;
107
+ border-radius: 4px;
108
+ transition: all 0.1s ease-in-out;
109
+
110
+ &:hover {
111
+ opacity: 0.88;
112
+ }
113
+ }
114
+ }
115
+
72
116
  .tree-row-icon {
73
117
  display: flex;
74
118
  flex-wrap: nowrap;
@@ -94,5 +138,3 @@
94
138
  background-color: var(--vscode-list-activeSelectionBackground);
95
139
  outline: 1px solid var(--vscode-contrastActiveBorder);
96
140
  }
97
-
98
-
@@ -5,7 +5,7 @@ import type { IGroup, IGroupRenderProps, IGroupHeaderProps } from '@fluentui/rea
5
5
  import { Icon } from '@fluentui/react';
6
6
  import { UIList, UiIcons } from '@sap-ux/ui-components';
7
7
 
8
- import { selectControl, reportTelemetry } from '@sap-ux-private/control-property-editor-common';
8
+ import { selectControl, reportTelemetry, addExtensionPoint } from '@sap-ux-private/control-property-editor-common';
9
9
  import type { Control, OutlineNode } from '@sap-ux-private/control-property-editor-common';
10
10
 
11
11
  import type { RootState } from '../../store';
@@ -22,23 +22,29 @@ interface OutlineNodeItem extends OutlineNode {
22
22
 
23
23
  export const Tree = (): ReactElement => {
24
24
  const dispatch = useDispatch();
25
+
26
+ const [collapsed, setCollapsed] = useState<IGroup[]>([]);
25
27
  const [selection, setSelection] = useState<{ group: undefined | IGroup; cell: undefined | OutlineNodeItem }>({
26
28
  group: undefined,
27
29
  cell: undefined
28
30
  });
29
- const [collapsed, setCollapsed] = useState<IGroup[]>([]);
31
+
30
32
  const filterQuery = useSelector<RootState, FilterOptions[]>((state) => state.filterQuery);
31
33
  const selectedControl = useSelector<RootState, Control | undefined>((state) => state.selectedControl);
32
34
  const controlChanges = useSelector<RootState, ControlChanges>((state) => state.changes.controls);
33
35
  const model: OutlineNode[] = useSelector<RootState, OutlineNode[]>((state) => state.outline);
36
+
34
37
  const { groups, items } = useMemo(() => {
35
38
  const items: OutlineNodeItem[] = [];
36
39
  const filteredModel = getFilteredModel(model, filterQuery);
37
40
  return { groups: getGroups(filteredModel, items), items };
38
41
  }, [model, filterQuery, selection]);
42
+
39
43
  const selectedClassName =
40
44
  localStorage.getItem('theme') === 'high contrast' ? 'app-panel-hc-selected-bg' : 'app-panel-selected-bg';
41
45
 
46
+ const tooltipEventListeners: Record<string, (event: MouseEvent) => void> = {};
47
+
42
48
  useEffect(() => {
43
49
  if (selection.cell === undefined && selection.group === undefined && selectedControl !== undefined) {
44
50
  updateSelectionFromPreview(selectedControl);
@@ -73,6 +79,63 @@ export const Tree = (): ReactElement => {
73
79
  }
74
80
  }, []);
75
81
 
82
+ /**
83
+ * Closes a tooltip and removes the associated event listener.
84
+ *
85
+ * @param tooltipId The unique identifier for the tooltip.
86
+ * @param eventListener The specific event listener to remove (optional).
87
+ */
88
+ const closeTooltip = (tooltipId: string, eventListener?: (event: MouseEvent) => void) => {
89
+ const tooltip = document.getElementById(tooltipId);
90
+
91
+ if (tooltip) {
92
+ tooltip.style.visibility = 'hidden';
93
+ tooltip.style.opacity = '0';
94
+ }
95
+
96
+ if (eventListener) {
97
+ document.removeEventListener('click', eventListener);
98
+ } else if (tooltipId in tooltipEventListeners) {
99
+ document.removeEventListener('click', tooltipEventListeners[tooltipId]);
100
+ delete tooltipEventListeners[tooltipId];
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Handles the opening of a tooltip and associates an event listener with it.
106
+ *
107
+ * @param e The click event that triggered the tooltip.
108
+ * @param tooltipId The unique identifier for the tooltip.
109
+ */
110
+ const handleOpenTooltip = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>, tooltipId: string) => {
111
+ e.preventDefault();
112
+ const tooltip = document.getElementById(tooltipId);
113
+
114
+ if (tooltip) {
115
+ tooltip.style.visibility = 'visible';
116
+ tooltip.style.opacity = '1';
117
+
118
+ const handleCloseTooltip = (event: MouseEvent) => {
119
+ if (!tooltip.contains(event.target as Node)) {
120
+ closeTooltip(tooltipId, handleCloseTooltip);
121
+ }
122
+ };
123
+
124
+ tooltipEventListeners[tooltipId] = handleCloseTooltip;
125
+
126
+ document.addEventListener('click', handleCloseTooltip);
127
+ } else {
128
+ console.warn(`Tooltip with id ${tooltipId} not found`);
129
+ }
130
+ };
131
+
132
+ const handleOpenFragmentDialog = (data: OutlineNode, tooltipId: string) => {
133
+ if (data.controlType === 'sap.ui.extensionpoint') {
134
+ closeTooltip(tooltipId);
135
+ dispatch(addExtensionPoint(data));
136
+ }
137
+ };
138
+
76
139
  /**
77
140
  * Find in group.
78
141
  *
@@ -219,7 +282,11 @@ export const Tree = (): ReactElement => {
219
282
  <></>
220
283
  );
221
284
  return item && typeof itemIndex === 'number' && itemIndex > -1 ? (
222
- <div className={classNames.join(' ')} onClick={(): void => onSelectCell(item)} id={item.controlId}>
285
+ <div
286
+ aria-hidden
287
+ id={item.controlId}
288
+ className={classNames.join(' ')}
289
+ onClick={(): void => onSelectCell(item)}>
223
290
  <div
224
291
  {...props}
225
292
  className={`tree-cell`}
@@ -273,6 +340,7 @@ export const Tree = (): ReactElement => {
273
340
  if (selectNode) {
274
341
  refProps.ref = scrollRef;
275
342
  }
343
+ const isExtensionPoint = groupHeaderProps?.group?.data?.controlType === 'sap.ui.extensionpoint';
276
344
  const focus = filterQuery.filter((item) => item.name === FilterName.focusEditable)[0].value as boolean;
277
345
  const focusEditable = !groupHeaderProps?.group?.data?.editable && focus ? 'focusEditable' : '';
278
346
  const controlChange = controlChanges[groupHeaderProps?.group?.key ?? ''];
@@ -281,12 +349,20 @@ export const Tree = (): ReactElement => {
281
349
  ) : (
282
350
  <></>
283
351
  );
352
+
353
+ const tooltipId = `tooltip--${groupName}`;
354
+
284
355
  return (
285
356
  <div
286
357
  {...refProps}
358
+ aria-hidden
287
359
  className={`${selectNode} tree-row ${focusEditable}`}
288
360
  onClick={(): void => onSelectHeader(groupHeaderProps?.group)}>
289
- <span style={{ paddingLeft: paddingValue }} className={`tree-cell`}>
361
+ <span
362
+ style={{ paddingLeft: paddingValue }}
363
+ data-testid="tooltip-container"
364
+ className={`tree-cell ${isExtensionPoint ? 'tooltip-container' : ''}`}
365
+ onContextMenu={(e) => isExtensionPoint && handleOpenTooltip(e, tooltipId)}>
290
366
  {groupHeaderProps?.group?.count !== 0 && (
291
367
  <Icon
292
368
  className={`${chevronTransform}`}
@@ -297,14 +373,27 @@ export const Tree = (): ReactElement => {
297
373
  }}
298
374
  />
299
375
  )}
376
+ {isExtensionPoint && (
377
+ <Icon className={`${chevronTransform} extension-icon`} iconName={UiIcons.DataSource} />
378
+ )}
300
379
  <div
301
380
  style={{
302
381
  cursor: 'pointer',
303
382
  overflow: 'hidden',
304
383
  textOverflow: 'ellipsis'
305
- }}>
384
+ }}
385
+ title={isExtensionPoint ? groupName : ''}>
306
386
  {groupName}
307
387
  </div>
388
+ {isExtensionPoint && (
389
+ <div id={tooltipId} className="tooltip">
390
+ <button
391
+ data-testid="tooltip-dialog-button"
392
+ onClick={() => handleOpenFragmentDialog(groupHeaderProps?.group?.data, tooltipId)}>
393
+ Add Fragment at Extension Point
394
+ </button>
395
+ </div>
396
+ )}
308
397
  </span>
309
398
  <div style={{ marginLeft: '10px', marginRight: '10px' }}>{indicator}</div>
310
399
  </div>
@@ -381,6 +470,7 @@ function getGroups(model: OutlineNode[], items: OutlineNodeItem[], level = 0, pa
381
470
  isCollapsed: count === 0,
382
471
  data: { ...data, path: newPath }
383
472
  };
473
+
384
474
  const shouldCreate = createGroupChild(children);
385
475
  newGroup.children = shouldCreate ? getGroups(children, items, level + 1, newPath) : [];
386
476
  group.push(newGroup);
@@ -23,7 +23,8 @@ const commonVisibleControls = [
23
23
  'sap.m.Dialog',
24
24
  'sap.ui.comp.ValueHelpDialog',
25
25
  'sap.viz.ui5.controls.VizFrame',
26
- 'sap.ovp.ui.Card'
26
+ 'sap.ovp.ui.Card',
27
+ 'sap.ui.extensionpoint'
27
28
  ];
28
29
 
29
30
  /**
@@ -113,4 +113,24 @@ describe('communication middleware', () => {
113
113
  `);
114
114
  expect(sendActionfn).toHaveBeenCalledTimes(1);
115
115
  });
116
+
117
+ test('add extension point - send action', () => {
118
+ const action = common.addExtensionPoint({ controlId: 'control1' } as common.OutlineNode);
119
+ const next = jest.fn().mockReturnValue(action);
120
+ jest.mock('@sap-ux-private/control-property-editor-common', () => {
121
+ return {
122
+ addExtensionPoint: { type: '[ext] add-extension-point' }
123
+ };
124
+ });
125
+ const result = middleWare(next)(action);
126
+ expect(result).toMatchInlineSnapshot(`
127
+ Object {
128
+ "payload": Object {
129
+ "controlId": "control1",
130
+ },
131
+ "type": "[ext] add-extension-point",
132
+ }
133
+ `);
134
+ expect(sendActionfn).toHaveBeenCalledTimes(1);
135
+ });
116
136
  });
@@ -232,6 +232,14 @@ describe('OutlinePanel', () => {
232
232
  controlType: 'sap.ui.comp.smarttable.SmartTable',
233
233
  editable: true,
234
234
  visible: true
235
+ },
236
+ {
237
+ name: 'ExtensionPoint',
238
+ controlId: '04',
239
+ children: [],
240
+ controlType: 'sap.ui.extensionpoint',
241
+ editable: true,
242
+ visible: true
235
243
  }
236
244
  ];
237
245
  const initialState: State = {
@@ -263,6 +271,94 @@ describe('OutlinePanel', () => {
263
271
  const SmartTable = screen.getByText(/SmartTable/i);
264
272
  expect(SmartTable).toBeInTheDocument();
265
273
  });
274
+
275
+ test('handleOpenTooltip should show and hide the tooltip', () => {
276
+ const model: OutlineNode[] = [
277
+ {
278
+ name: 'ExtensionPoint',
279
+ controlId: '04',
280
+ children: [],
281
+ controlType: 'sap.ui.extensionpoint',
282
+ editable: true,
283
+ visible: true
284
+ }
285
+ ];
286
+ const initialState: State = {
287
+ deviceType: DeviceType.Desktop,
288
+ scale: 1,
289
+ outline: model,
290
+ filterQuery: filterInitOptions,
291
+ selectedControl: undefined,
292
+ changes: {
293
+ pending: [],
294
+ saved: [],
295
+ controls: {}
296
+ },
297
+ icons: []
298
+ };
299
+
300
+ const tooltipId = 'tooltip--ExtensionPoint';
301
+
302
+ const { container } = render(<OutlinePanel />, { initialState });
303
+ const spanElement = screen.getByTestId('tooltip-container');
304
+
305
+ // Simulate a right-click event
306
+ fireEvent.contextMenu(spanElement);
307
+
308
+ const tooltip = container.querySelector(`#${tooltipId}`);
309
+
310
+ expect(tooltip).toHaveStyle({ visibility: 'visible', opacity: '1' });
311
+
312
+ // Close the tooltip
313
+ fireEvent.click(document); // Simulate a click outside the tooltip
314
+
315
+ expect(tooltip).toHaveStyle({ visibility: 'hidden', opacity: '0' });
316
+ });
317
+
318
+ test('should show and hide when clicking button to open dialog', () => {
319
+ const model: OutlineNode[] = [
320
+ {
321
+ name: 'ExtensionPoint',
322
+ controlId: '04',
323
+ children: [],
324
+ controlType: 'sap.ui.extensionpoint',
325
+ editable: true,
326
+ visible: true
327
+ }
328
+ ];
329
+ const initialState: State = {
330
+ deviceType: DeviceType.Desktop,
331
+ scale: 1,
332
+ outline: model,
333
+ filterQuery: filterInitOptions,
334
+ selectedControl: undefined,
335
+ changes: {
336
+ pending: [],
337
+ saved: [],
338
+ controls: {}
339
+ },
340
+ icons: []
341
+ };
342
+
343
+ const tooltipId = 'tooltip--ExtensionPoint';
344
+
345
+ const { container } = render(<OutlinePanel />, { initialState });
346
+ const spanElement = screen.getByTestId('tooltip-container');
347
+ const buttonElement = screen.getByTestId('tooltip-dialog-button');
348
+
349
+ // Simulate a right-click event
350
+ fireEvent.contextMenu(spanElement);
351
+
352
+ const tooltip = container.querySelector(`#${tooltipId}`);
353
+
354
+ expect(tooltip).toHaveStyle({ visibility: 'visible', opacity: '1' });
355
+
356
+ // Close the tooltip
357
+ fireEvent.click(buttonElement); // Simulate a click on the tooltip button
358
+
359
+ expect(tooltip).toHaveStyle({ visibility: 'hidden', opacity: '0' });
360
+ });
361
+
266
362
  test('do not expand to previously selected control', () => {
267
363
  const { store, container } = render(<OutlinePanel />);
268
364
  // clear default applied filters