@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/CHANGELOG.md +12 -0
- package/dist/app.css +1 -1
- package/dist/app.css.map +2 -2
- package/dist/app.js +60 -60
- package/dist/app.js.map +3 -3
- package/package.json +4 -4
- package/src/middleware.ts +6 -1
- package/src/panels/changes/UnknownChange.tsx +1 -1
- package/src/panels/outline/OutlinePanel.scss +44 -2
- package/src/panels/outline/Tree.tsx +95 -5
- package/src/panels/outline/utils.ts +2 -1
- package/test/unit/middleware.test.ts +20 -0
- package/test/unit/panels/outline/OutlinePanel.test.tsx +96 -0
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
|
+
"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.
|
|
50
|
+
"postcss": "8.4.31",
|
|
51
51
|
"yargs-parser": "21.1.1",
|
|
52
|
-
"@sap-ux/ui-components": "1.11.
|
|
53
|
-
"@sap-ux-private/control-property-editor-common": "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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
|
@@ -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
|