@sap-ux/control-property-editor 0.2.0

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.
Files changed (112) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.js +16 -0
  3. package/CHANGELOG.md +7 -0
  4. package/LICENSE +201 -0
  5. package/README.md +16 -0
  6. package/dist/app.css +2 -0
  7. package/dist/app.css.map +7 -0
  8. package/dist/app.js +347 -0
  9. package/dist/app.js.map +7 -0
  10. package/esbuild.js +25 -0
  11. package/jest.config.js +20 -0
  12. package/package.json +68 -0
  13. package/src/App.scss +57 -0
  14. package/src/App.tsx +136 -0
  15. package/src/Workarounds.scss +79 -0
  16. package/src/actions.ts +3 -0
  17. package/src/components/AppLogo.module.scss +8 -0
  18. package/src/components/AppLogo.tsx +75 -0
  19. package/src/components/ChangeIndicator.tsx +80 -0
  20. package/src/components/Separator.tsx +32 -0
  21. package/src/components/ThemeSelectorCallout.scss +48 -0
  22. package/src/components/ThemeSelectorCallout.tsx +125 -0
  23. package/src/components/ToolBar.scss +39 -0
  24. package/src/components/ToolBar.tsx +26 -0
  25. package/src/components/index.ts +4 -0
  26. package/src/devices.ts +18 -0
  27. package/src/global.d.ts +4 -0
  28. package/src/i18n/i18n.json +68 -0
  29. package/src/i18n.ts +25 -0
  30. package/src/icons.tsx +198 -0
  31. package/src/index.css +1288 -0
  32. package/src/index.tsx +47 -0
  33. package/src/middleware.ts +54 -0
  34. package/src/panels/LeftPanel.scss +17 -0
  35. package/src/panels/LeftPanel.tsx +48 -0
  36. package/src/panels/changes/ChangeStack.module.scss +3 -0
  37. package/src/panels/changes/ChangeStack.tsx +219 -0
  38. package/src/panels/changes/ChangeStackHeader.tsx +43 -0
  39. package/src/panels/changes/ChangesPanel.module.scss +18 -0
  40. package/src/panels/changes/ChangesPanel.tsx +90 -0
  41. package/src/panels/changes/ControlGroup.module.scss +17 -0
  42. package/src/panels/changes/ControlGroup.tsx +61 -0
  43. package/src/panels/changes/PropertyChange.module.scss +24 -0
  44. package/src/panels/changes/PropertyChange.tsx +159 -0
  45. package/src/panels/changes/UnknownChange.module.scss +46 -0
  46. package/src/panels/changes/UnknownChange.tsx +96 -0
  47. package/src/panels/changes/index.tsx +3 -0
  48. package/src/panels/changes/utils.ts +36 -0
  49. package/src/panels/index.ts +2 -0
  50. package/src/panels/outline/Funnel.tsx +64 -0
  51. package/src/panels/outline/NoControlFound.tsx +45 -0
  52. package/src/panels/outline/OutlinePanel.scss +98 -0
  53. package/src/panels/outline/OutlinePanel.tsx +38 -0
  54. package/src/panels/outline/Tree.tsx +393 -0
  55. package/src/panels/outline/index.ts +1 -0
  56. package/src/panels/outline/utils.ts +154 -0
  57. package/src/panels/properties/Clipboard.tsx +44 -0
  58. package/src/panels/properties/DeviceSelector.tsx +40 -0
  59. package/src/panels/properties/DeviceToggle.tsx +39 -0
  60. package/src/panels/properties/DropdownEditor.tsx +80 -0
  61. package/src/panels/properties/Funnel.tsx +64 -0
  62. package/src/panels/properties/HeaderField.tsx +150 -0
  63. package/src/panels/properties/IconValueHelp.tsx +203 -0
  64. package/src/panels/properties/InputTypeSelector.tsx +20 -0
  65. package/src/panels/properties/InputTypeToggle.module.scss +4 -0
  66. package/src/panels/properties/InputTypeToggle.tsx +79 -0
  67. package/src/panels/properties/InputTypeWrapper.tsx +259 -0
  68. package/src/panels/properties/NoControlSelected.tsx +38 -0
  69. package/src/panels/properties/Properties.scss +102 -0
  70. package/src/panels/properties/PropertiesList.tsx +162 -0
  71. package/src/panels/properties/PropertiesPanel.tsx +30 -0
  72. package/src/panels/properties/PropertyDocumentation.module.scss +81 -0
  73. package/src/panels/properties/PropertyDocumentation.tsx +174 -0
  74. package/src/panels/properties/SapUiIcon.scss +109 -0
  75. package/src/panels/properties/StringEditor.tsx +122 -0
  76. package/src/panels/properties/ViewChanger.module.scss +5 -0
  77. package/src/panels/properties/ViewChanger.tsx +143 -0
  78. package/src/panels/properties/constants.ts +2 -0
  79. package/src/panels/properties/index.tsx +1 -0
  80. package/src/panels/properties/propertyValuesCache.ts +39 -0
  81. package/src/panels/properties/types.ts +49 -0
  82. package/src/slice.ts +216 -0
  83. package/src/store.ts +19 -0
  84. package/src/use-local-storage.ts +40 -0
  85. package/src/use-window-size.ts +39 -0
  86. package/src/variables.scss +2 -0
  87. package/test/unit/App.test.tsx +207 -0
  88. package/test/unit/appIndex.test.ts +23 -0
  89. package/test/unit/components/ChangeIndicator.test.tsx +120 -0
  90. package/test/unit/components/ThemeSelector.test.tsx +41 -0
  91. package/test/unit/middleware.test.ts +116 -0
  92. package/test/unit/panels/changes/ChangesPanel.test.tsx +261 -0
  93. package/test/unit/panels/changes/utils.test.ts +40 -0
  94. package/test/unit/panels/outline/OutlinePanel.test.tsx +353 -0
  95. package/test/unit/panels/outline/__snapshots__/utils.test.ts.snap +36 -0
  96. package/test/unit/panels/outline/utils.test.ts +83 -0
  97. package/test/unit/panels/properties/Clipboard.test.tsx +18 -0
  98. package/test/unit/panels/properties/DropdownEditor.test.tsx +62 -0
  99. package/test/unit/panels/properties/Funnel.test.tsx +34 -0
  100. package/test/unit/panels/properties/HeaderField.test.tsx +36 -0
  101. package/test/unit/panels/properties/IconValueHelp.test.tsx +60 -0
  102. package/test/unit/panels/properties/InputTypeToggle.test.tsx +126 -0
  103. package/test/unit/panels/properties/InputTypeWrapper.test.tsx +430 -0
  104. package/test/unit/panels/properties/PropertyDocumentation.test.tsx +131 -0
  105. package/test/unit/panels/properties/StringEditor.test.tsx +107 -0
  106. package/test/unit/panels/properties/ViewChanger.test.tsx +190 -0
  107. package/test/unit/panels/properties/propertyValuesCache.test.ts +23 -0
  108. package/test/unit/slice.test.ts +268 -0
  109. package/test/unit/utils.tsx +67 -0
  110. package/test/utils/utils.tsx +25 -0
  111. package/tsconfig.eslint.json +4 -0
  112. package/tsconfig.json +39 -0
@@ -0,0 +1,393 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import type { ReactElement } from 'react';
3
+ import { useSelector, useDispatch } from 'react-redux';
4
+ import type { IGroup, IGroupRenderProps, IGroupHeaderProps } from '@fluentui/react';
5
+ import { Icon } from '@fluentui/react';
6
+ import { UIList, UiIcons } from '@sap-ux/ui-components';
7
+
8
+ import { selectControl, reportTelemetry } from '@sap-ux-private/control-property-editor-common';
9
+ import type { Control, OutlineNode } from '@sap-ux-private/control-property-editor-common';
10
+
11
+ import type { RootState } from '../../store';
12
+ import type { ControlChanges, FilterOptions } from '../../slice';
13
+ import { FilterName } from '../../slice';
14
+ import { NoControlFound } from './NoControlFound';
15
+ import { adaptExpandCollapsed, getFilteredModel, isSame } from './utils';
16
+ import { ChangeIndicator } from '../../components/ChangeIndicator';
17
+
18
+ interface OutlineNodeItem extends OutlineNode {
19
+ level: number;
20
+ path: string[];
21
+ }
22
+
23
+ export const Tree = (): ReactElement => {
24
+ const dispatch = useDispatch();
25
+ const [selection, setSelection] = useState<{ group: undefined | IGroup; cell: undefined | OutlineNodeItem }>({
26
+ group: undefined,
27
+ cell: undefined
28
+ });
29
+ const [collapsed, setCollapsed] = useState<IGroup[]>([]);
30
+ const filterQuery = useSelector<RootState, FilterOptions[]>((state) => state.filterQuery);
31
+ const selectedControl = useSelector<RootState, Control | undefined>((state) => state.selectedControl);
32
+ const controlChanges = useSelector<RootState, ControlChanges>((state) => state.changes.controls);
33
+ const model: OutlineNode[] = useSelector<RootState, OutlineNode[]>((state) => state.outline);
34
+ const { groups, items } = useMemo(() => {
35
+ const items: OutlineNodeItem[] = [];
36
+ const filteredModel = getFilteredModel(model, filterQuery);
37
+ return { groups: getGroups(filteredModel, items), items };
38
+ }, [model, filterQuery, selection]);
39
+ const selectedClassName =
40
+ localStorage.getItem('theme') === 'high contrast' ? 'app-panel-hc-selected-bg' : 'app-panel-selected-bg';
41
+
42
+ useEffect(() => {
43
+ if (selection.cell === undefined && selection.group === undefined && selectedControl !== undefined) {
44
+ updateSelectionFromPreview(selectedControl);
45
+ }
46
+
47
+ if (selection.cell !== undefined && selectedControl !== undefined) {
48
+ if (selection.cell.controlId !== selectedControl.id) {
49
+ updateSelectionFromPreview(selectedControl);
50
+ }
51
+ }
52
+
53
+ if (selection.group !== undefined && selectedControl !== undefined) {
54
+ if (selection.group.key !== selectedControl.id) {
55
+ updateSelectionFromPreview(selectedControl);
56
+ }
57
+ }
58
+ }, [selectedControl]);
59
+ useMemo(() => {
60
+ adaptExpandCollapsed(groups, collapsed);
61
+ }, [groups, collapsed, selection]);
62
+
63
+ const scrollRef = useCallback((node: Element) => {
64
+ if (node !== null) {
65
+ setTimeout(() => {
66
+ // make sure that tree is fully rendered
67
+ const rect = node.getBoundingClientRect();
68
+ const outlineContainer = document.getElementsByClassName('section--scrollable')[0];
69
+ if (rect.top <= 20 || rect.bottom >= outlineContainer?.clientHeight) {
70
+ node.scrollIntoView(true);
71
+ }
72
+ }, 0);
73
+ }
74
+ }, []);
75
+
76
+ /**
77
+ * Find in group.
78
+ *
79
+ * @param control Control
80
+ * @param group IGroup
81
+ * @returns IGroup[] | undefined
82
+ */
83
+ function findInGroup(control: Control, group: IGroup): IGroup[] | undefined {
84
+ if (group.key === control.id) {
85
+ return [group];
86
+ }
87
+
88
+ if (group.children !== undefined) {
89
+ const result = findInGroups(control, group.children);
90
+ if (result) {
91
+ return [group, ...result];
92
+ }
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ /**
98
+ * Find in group collection.
99
+ *
100
+ * @param control Control
101
+ * @param groups IGroup[]
102
+ * @returns IGroup[] | undefined
103
+ */
104
+ function findInGroups(control: Control, groups: IGroup[]): IGroup[] | undefined {
105
+ for (const group of groups) {
106
+ const result = findInGroup(control, group);
107
+ if (result !== undefined) {
108
+ return [group, ...result];
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ type CurrentType = IGroup | IGroup[] | undefined;
115
+
116
+ /**
117
+ * Sets current item.
118
+ *
119
+ * @param current CurrentType,
120
+ * @param segment string
121
+ * @returns CurrentType
122
+ */
123
+ function setCurrentItem(current: CurrentType, segment: string): CurrentType {
124
+ if (Array.isArray(current)) {
125
+ current = current[parseInt(segment, 10)];
126
+ } else if (current && segment === 'children') {
127
+ current = current.children;
128
+ } else {
129
+ current = undefined;
130
+ }
131
+ if (!Array.isArray(current) && current) {
132
+ current.isCollapsed = false;
133
+ }
134
+ return current;
135
+ }
136
+
137
+ /**
138
+ * Update selection from preview.
139
+ *
140
+ * @param control Control
141
+ */
142
+ function updateSelectionFromPreview(control: Control): void {
143
+ const item = items.find((item) => item.controlId === control.id);
144
+ const pathToGroup = findInGroups(control, groups);
145
+ if (pathToGroup) {
146
+ for (const group of pathToGroup) {
147
+ group.isCollapsed = false;
148
+ }
149
+ setSelection({
150
+ group: pathToGroup.slice(-1)[0],
151
+ cell: undefined
152
+ });
153
+ } else if (item) {
154
+ let current: CurrentType = groups;
155
+ for (const segment of item.path) {
156
+ current = setCurrentItem(current, segment);
157
+ }
158
+ setSelection({
159
+ group: undefined,
160
+ cell: item
161
+ });
162
+ } else if (selection.cell !== undefined || selection.group !== undefined) {
163
+ setSelection({
164
+ group: undefined,
165
+ cell: undefined
166
+ });
167
+ }
168
+ }
169
+
170
+ if (items.length === 0 && groups.length === 0) {
171
+ return <NoControlFound />;
172
+ }
173
+
174
+ const onSelectCell = (item: OutlineNodeItem): void => {
175
+ setSelection({
176
+ group: undefined,
177
+ cell: item
178
+ });
179
+ const action = selectControl(item.controlId);
180
+ dispatch(action);
181
+ };
182
+ const onSelectHeader = (node: IGroup | undefined): void => {
183
+ if (node) {
184
+ setSelection({
185
+ group: node,
186
+ cell: undefined
187
+ });
188
+ const name = (node.data.controlType as string).toLowerCase().startsWith('sap')
189
+ ? node.data.controlType
190
+ : 'Other Control Types';
191
+ reportTelemetry({ category: 'Outline Selection', controlName: name }).catch((error) => {
192
+ console.error(`Error in reporting telemetry`, error);
193
+ });
194
+ const action = selectControl(node.key);
195
+ dispatch(action);
196
+ }
197
+ };
198
+ const onRenderCell = (nestingDepth?: number, item?: OutlineNodeItem, itemIndex?: number): React.ReactNode => {
199
+ const paddingValue = (item?.level ?? 0) * 10 + 45;
200
+ const classNames: string[] = ['tree-row'];
201
+ const props: {
202
+ ref?: (node: HTMLDivElement) => void;
203
+ } = {};
204
+
205
+ if (selection.cell?.controlId === item?.controlId) {
206
+ props.ref = scrollRef;
207
+ classNames.push(selectedClassName);
208
+ }
209
+
210
+ const focus = filterQuery.find((item) => item.name === FilterName.focusEditable)?.value;
211
+ if (!item?.editable && focus === true) {
212
+ classNames.push('focusEditable');
213
+ }
214
+
215
+ const controlChange = controlChanges[item?.controlId ?? ''];
216
+ const indicator = controlChange ? (
217
+ <ChangeIndicator id={`${item?.controlId}--ChangeIndicator`} {...controlChange} />
218
+ ) : (
219
+ <></>
220
+ );
221
+ return item && typeof itemIndex === 'number' && itemIndex > -1 ? (
222
+ <div className={classNames.join(' ')} onClick={(): void => onSelectCell(item)} id={item.controlId}>
223
+ <div
224
+ {...props}
225
+ className={`tree-cell`}
226
+ style={{
227
+ paddingLeft: paddingValue,
228
+ cursor: 'auto',
229
+ overflow: 'hidden',
230
+ textOverflow: 'ellipsis',
231
+ display: 'block'
232
+ }}>
233
+ {item.name}
234
+ </div>
235
+ <div style={{ marginLeft: '10px', marginRight: '10px' }}>{indicator}</div>
236
+ </div>
237
+ ) : null;
238
+ };
239
+ const onToggleCollapse = (groupHeaderProps?: IGroupHeaderProps): void => {
240
+ if (groupHeaderProps?.onToggleCollapse && groupHeaderProps?.group) {
241
+ groupHeaderProps?.onToggleCollapse(groupHeaderProps?.group);
242
+ }
243
+
244
+ const isCollapsed = groupHeaderProps?.group?.isCollapsed;
245
+ if (groupHeaderProps?.group) {
246
+ if (isCollapsed) {
247
+ // set collapsed row
248
+ setCollapsed([...collapsed, groupHeaderProps.group]);
249
+ } else {
250
+ // filter expanded row
251
+ const filterNodes = collapsed.filter(
252
+ (item) => !isSame(item.data.path, groupHeaderProps.group?.data.path)
253
+ );
254
+ setCollapsed(filterNodes);
255
+ }
256
+ }
257
+ };
258
+ const onRenderHeader = (groupHeaderProps?: IGroupHeaderProps): React.JSX.Element | null => {
259
+ const selectNode = selection.group?.key === groupHeaderProps?.group?.key ? selectedClassName : '';
260
+ let paddingValue = (groupHeaderProps?.group?.level ?? 0) * 10 + 15;
261
+ // add padding to compensate absence of chevron icon
262
+ if (groupHeaderProps?.group?.count === 0) {
263
+ paddingValue += 15;
264
+ }
265
+ const chevronTransform =
266
+ groupHeaderProps?.group?.key && groupHeaderProps.group.isCollapsed
267
+ ? 'right-chevron-icon'
268
+ : 'down-chevron-icon';
269
+ const groupName = `${groupHeaderProps?.group?.name}`;
270
+ const refProps: {
271
+ ref?: (node: HTMLDivElement) => void;
272
+ } = {};
273
+ if (selectNode) {
274
+ refProps.ref = scrollRef;
275
+ }
276
+ const focus = filterQuery.filter((item) => item.name === FilterName.focusEditable)[0].value as boolean;
277
+ const focusEditable = !groupHeaderProps?.group?.data?.editable && focus ? 'focusEditable' : '';
278
+ const controlChange = controlChanges[groupHeaderProps?.group?.key ?? ''];
279
+ const indicator = controlChange ? (
280
+ <ChangeIndicator id={`${groupHeaderProps?.group?.key ?? ''}--ChangeIndicator`} {...controlChange} />
281
+ ) : (
282
+ <></>
283
+ );
284
+ return (
285
+ <div
286
+ {...refProps}
287
+ className={`${selectNode} tree-row ${focusEditable}`}
288
+ onClick={(): void => onSelectHeader(groupHeaderProps?.group)}>
289
+ <span style={{ paddingLeft: paddingValue }} className={`tree-cell`}>
290
+ {groupHeaderProps?.group?.count !== 0 && (
291
+ <Icon
292
+ className={`${chevronTransform}`}
293
+ iconName={UiIcons.Chevron}
294
+ onClick={(event) => {
295
+ onToggleCollapse(groupHeaderProps);
296
+ event.stopPropagation();
297
+ }}
298
+ />
299
+ )}
300
+ <div
301
+ style={{
302
+ cursor: 'pointer',
303
+ overflow: 'hidden',
304
+ textOverflow: 'ellipsis'
305
+ }}>
306
+ {groupName}
307
+ </div>
308
+ </span>
309
+ <div style={{ marginLeft: '10px', marginRight: '10px' }}>{indicator}</div>
310
+ </div>
311
+ );
312
+ };
313
+ const groupRenderProps: IGroupRenderProps = {
314
+ showEmptyGroups: true,
315
+ onRenderHeader
316
+ };
317
+
318
+ // workaround for UIList not exposing GroupedList props
319
+ const listProp: {
320
+ onShouldVirtualize: () => false;
321
+ usePageCache: boolean;
322
+ } = {
323
+ onShouldVirtualize: () => false,
324
+ usePageCache: true
325
+ };
326
+
327
+ return (
328
+ <div id="list-outline" className="app-panel-scroller">
329
+ <UIList
330
+ {...listProp}
331
+ items={items as never[]}
332
+ onRenderCell={onRenderCell}
333
+ groups={groups}
334
+ onSelect={onSelectHeader}
335
+ groupProps={groupRenderProps}></UIList>
336
+ </div>
337
+ );
338
+ };
339
+
340
+ /**
341
+ * Checks a child can be added.
342
+ *
343
+ * @param model OutlineNode[]
344
+ * @returns boolean
345
+ */
346
+ function createGroupChild(model: OutlineNode[]): boolean {
347
+ let result = false;
348
+ for (const data of model) {
349
+ const children = data.children || [];
350
+ if (children.length > 0) {
351
+ result = true;
352
+ break;
353
+ }
354
+ }
355
+ return result;
356
+ }
357
+
358
+ /**
359
+ * Get groups.
360
+ *
361
+ * @param model OutlineNode[]
362
+ * @param items OutlineNodeItem[]
363
+ * @param level number, default to 0
364
+ * @param path string
365
+ * @returns IGroup[]
366
+ */
367
+ function getGroups(model: OutlineNode[], items: OutlineNodeItem[], level = 0, path: string[] = []): IGroup[] {
368
+ const group: IGroup[] = [];
369
+ for (let i = 0; i < model.length; i++) {
370
+ const data = model[i];
371
+ const children = data.children || [];
372
+ const count = children.length; // no of item for each group
373
+ const newPath = [...path, i.toString(), 'children'];
374
+ const newGroup = {
375
+ count,
376
+ key: `${data.controlId}`,
377
+ name: data.name,
378
+ startIndex: items.length,
379
+ level: level,
380
+ children: [] as IGroup[],
381
+ isCollapsed: count === 0,
382
+ data: { ...data, path: newPath }
383
+ };
384
+ const shouldCreate = createGroupChild(children);
385
+ newGroup.children = shouldCreate ? getGroups(children, items, level + 1, newPath) : [];
386
+ group.push(newGroup);
387
+ // add node children to item
388
+ if (!shouldCreate) {
389
+ children.forEach((item, i) => items.push({ ...item, level, path: [...newPath, i.toString()] }));
390
+ }
391
+ }
392
+ return group;
393
+ }
@@ -0,0 +1 @@
1
+ export * from './OutlinePanel';
@@ -0,0 +1,154 @@
1
+ import type { OutlineNode } from '@sap-ux-private/control-property-editor-common';
2
+ import type { FilterOptions } from '../../slice';
3
+ import { FilterName } from '../../slice';
4
+ import type { IGroup } from '@fluentui/react';
5
+
6
+ const commonVisibleControls = [
7
+ 'sap.ui.comp.smarttable.SmartTable',
8
+ 'sap.m.Column',
9
+ 'sap.ui.comp.smartfilterbar.SmartFilterBar',
10
+ 'sap.ui.comp.filterbar.FilterItems',
11
+ 'sap.m.Button',
12
+ 'sap.m.MultiInput',
13
+ 'sap.ui.comp.smartform.SmartForm',
14
+ 'sap.ui.comp.smartform.Group',
15
+ 'sap.ui.comp.smartform.GroupElement',
16
+ 'sap.uxap.ObjectPageSection',
17
+ 'sap.m.Bar',
18
+ 'sap.m.OverflowToolbarButton',
19
+ 'sap.m.MultiComboBox',
20
+ 'sap.m.ComboBox',
21
+ 'sap.m.OverflowToolbar',
22
+ 'sap.m.Table',
23
+ 'sap.m.Dialog',
24
+ 'sap.ui.comp.ValueHelpDialog',
25
+ 'sap.viz.ui5.controls.VizFrame',
26
+ 'sap.ovp.ui.Card'
27
+ ];
28
+
29
+ /**
30
+ * Filter model. If none of filter conditions meet, model without filter is returned.
31
+ *
32
+ * @param model OutlineNode[]
33
+ * @param filterOptions FilterOptions[]
34
+ * @returns OutlineNode[]
35
+ */
36
+ export function getFilteredModel(model: OutlineNode[], filterOptions: FilterOptions[]): OutlineNode[] {
37
+ let filteredModel: OutlineNode[] = [];
38
+ for (const option of filterOptions) {
39
+ if (option.name === FilterName.query) {
40
+ if (filteredModel.length > 0) {
41
+ // filter based on filtered model
42
+ filteredModel = filterByQuery(filteredModel, option);
43
+ } else {
44
+ filteredModel = filterByQuery(model, option);
45
+ }
46
+ } else if (option.name === FilterName.focusCommonlyUsed) {
47
+ if (filteredModel.length > 0) {
48
+ // filter based on filtered model
49
+ filteredModel = filterByCommonlyUsedControls(filteredModel, option);
50
+ } else {
51
+ filteredModel = filterByCommonlyUsedControls(model, option);
52
+ }
53
+ }
54
+ }
55
+
56
+ return filteredModel;
57
+ }
58
+
59
+ /**
60
+ * Filter by options.
61
+ *
62
+ * @param model OutlineNode[]
63
+ * @param filterOption FilterOptions
64
+ * @returns OutlineNode[]
65
+ */
66
+ function filterByQuery(model: OutlineNode[], filterOption: FilterOptions) {
67
+ const filteredModel: OutlineNode[] = [];
68
+ const query = (filterOption.value as string).toLocaleUpperCase();
69
+ if (query.length === 0) {
70
+ return model;
71
+ }
72
+ for (const item of model) {
73
+ let parentMatch = false;
74
+ const name = item.name.toLocaleUpperCase();
75
+ if (name.includes(query)) {
76
+ parentMatch = true;
77
+ // add node without its children
78
+ filteredModel.push({ ...item, children: [] });
79
+ }
80
+ if (item.children.length) {
81
+ const data = filterByQuery(item.children, filterOption);
82
+ if (data.length > 0) {
83
+ // children matched filter query
84
+ if (parentMatch) {
85
+ // parent matched filter query and pushed already to `filterModel`. only replace matched children
86
+ filteredModel[filteredModel.length - 1].children = data;
87
+ } else {
88
+ // add node and its matched children
89
+ const newFilterModel = { ...item, children: data };
90
+ filteredModel.push(newFilterModel);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return filteredModel;
96
+ }
97
+
98
+ /**
99
+ * Filter by commonly used control.
100
+ *
101
+ * @param model OutlineNode[]
102
+ * @param filterOption FilterOptions
103
+ * @returns OutlineNode[]
104
+ */
105
+ function filterByCommonlyUsedControls(model: OutlineNode[], filterOption: FilterOptions) {
106
+ const filteredModel: OutlineNode[] = [];
107
+ const checked = filterOption.value as boolean;
108
+ if (!checked) {
109
+ return model;
110
+ }
111
+ for (const item of model) {
112
+ let parentMatch = false;
113
+ const controlType = item.controlType;
114
+ if (commonVisibleControls.includes(controlType)) {
115
+ parentMatch = true;
116
+ // add node without its children
117
+ filteredModel.push({ ...item, children: [] });
118
+ }
119
+ if (item.children.length) {
120
+ const data = filterByCommonlyUsedControls(item.children, filterOption);
121
+ if (data.length > 0) {
122
+ // children matched filter query
123
+ if (parentMatch) {
124
+ // parent matched filter query and pushed already to `filterModel`. only replace matched children
125
+ filteredModel[filteredModel.length - 1].children = data;
126
+ } else {
127
+ // add node and its matched children
128
+ const newFilterModel = { ...item, children: data };
129
+ filteredModel.push(newFilterModel);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return filteredModel;
135
+ }
136
+
137
+ export const isSame = (a: string[], b: string) => {
138
+ return JSON.stringify(a) === JSON.stringify(b);
139
+ };
140
+
141
+ export const adaptExpandCollapsed = (groups: IGroup[], collapsed: IGroup[]) => {
142
+ if (collapsed.length === 0) {
143
+ return;
144
+ }
145
+ for (const group of groups) {
146
+ const [collapsedResult] = collapsed.filter((data) => isSame(group.data.path, data.data.path));
147
+ if (collapsedResult) {
148
+ group.isCollapsed = true;
149
+ }
150
+ if (group.children) {
151
+ adaptExpandCollapsed(group.children, collapsed);
152
+ }
153
+ }
154
+ };
@@ -0,0 +1,44 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+ import { Icon } from '@fluentui/react';
4
+ import { UICallout, UICalloutContentPadding, UiIcons } from '@sap-ux/ui-components';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { defaultFontSize } from './constants';
7
+
8
+ export interface ClipboardProps {
9
+ label: string;
10
+ }
11
+
12
+ /**
13
+ * React element for Clipboard.
14
+ *
15
+ * @param clipBoardProps ClipboardProps
16
+ * @returns ReactElement
17
+ */
18
+ export function Clipboard(clipBoardProps: ClipboardProps): ReactElement {
19
+ const { label } = clipBoardProps;
20
+ const { t } = useTranslation();
21
+ return (
22
+ <UICallout
23
+ styles={{
24
+ calloutMain: {
25
+ minWidth: 0,
26
+ padding: '5px 10px 5px 10px',
27
+ outline: '1px solid var(--vscode-terminal-ansiGreen) !important',
28
+ fontSize: defaultFontSize
29
+ }
30
+ }}
31
+ target={`#${label.replace(/\s/g, '')}--copy`}
32
+ isBeakVisible={false}
33
+ gapSpace={5}
34
+ directionalHint={9}
35
+ contentPadding={UICalloutContentPadding.None}>
36
+ <span data-testid="copied-to-clipboard-popup" style={{ display: 'flex', alignItems: 'center' }}>
37
+ <Icon iconName={UiIcons.Success} />
38
+ <span data-testid="copied-to-clipboard-message" style={{ marginLeft: '5px' }}>
39
+ {t('COPIED_TO_CLIPBOARD')}
40
+ </span>
41
+ </span>
42
+ </UICallout>
43
+ );
44
+ }
@@ -0,0 +1,40 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { DeviceType } from '../../devices';
6
+
7
+ import type { DeviceToggleProps } from './DeviceToggle';
8
+ import { DeviceToggle } from './DeviceToggle';
9
+
10
+ /**
11
+ * React element for Device Selector.
12
+ *
13
+ * @returns ReactElement
14
+ */
15
+ export function DeviceSelector(): ReactElement {
16
+ const { t } = useTranslation();
17
+
18
+ const deviceProps: DeviceToggleProps[] = [
19
+ {
20
+ deviceType: DeviceType.Desktop,
21
+ tooltip: t('DEVICE_TYPE_DESKTOP')
22
+ },
23
+ {
24
+ deviceType: DeviceType.Tablet,
25
+ tooltip: t('DEVICE_TYPE_TABLET')
26
+ },
27
+ {
28
+ deviceType: DeviceType.Phone,
29
+ tooltip: t('DEVICE_TYPE_PHONE')
30
+ }
31
+ ];
32
+
33
+ return (
34
+ <>
35
+ {deviceProps.map((props: DeviceToggleProps) => (
36
+ <DeviceToggle key={props.deviceType} {...props} />
37
+ ))}
38
+ </>
39
+ );
40
+ }
@@ -0,0 +1,39 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+ import { useDispatch, useSelector } from 'react-redux';
4
+ import type { AnyAction } from 'redux';
5
+ import { UIIconButton } from '@sap-ux/ui-components';
6
+
7
+ import type { DeviceType } from '../../devices';
8
+ import { changeDeviceType } from '../../slice';
9
+ import type { RootState } from '../../store';
10
+
11
+ export interface DeviceToggleProps {
12
+ deviceType: DeviceType;
13
+ tooltip: string;
14
+ }
15
+
16
+ /**
17
+ * React element for device toggle.
18
+ *
19
+ * @param deviceToggleProps DeviceToggleProps
20
+ * @returns ReactElement
21
+ */
22
+ export function DeviceToggle(deviceToggleProps: DeviceToggleProps): ReactElement {
23
+ const { deviceType, tooltip } = deviceToggleProps;
24
+ const dispatch = useDispatch();
25
+ const selectedDeviceType = useSelector<RootState, DeviceType>((state) => state.deviceType);
26
+ const checked = deviceType === selectedDeviceType;
27
+ return (
28
+ <UIIconButton
29
+ id={`device-toggle-${deviceType}`}
30
+ iconProps={{
31
+ iconName: deviceType
32
+ }}
33
+ title={tooltip}
34
+ toggle={true}
35
+ checked={checked}
36
+ onClick={(): AnyAction => dispatch(changeDeviceType(deviceType))}
37
+ />
38
+ );
39
+ }