@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.
- package/.eslintignore +1 -0
- package/.eslintrc.js +16 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +16 -0
- package/dist/app.css +2 -0
- package/dist/app.css.map +7 -0
- package/dist/app.js +347 -0
- package/dist/app.js.map +7 -0
- package/esbuild.js +25 -0
- package/jest.config.js +20 -0
- package/package.json +68 -0
- package/src/App.scss +57 -0
- package/src/App.tsx +136 -0
- package/src/Workarounds.scss +79 -0
- package/src/actions.ts +3 -0
- package/src/components/AppLogo.module.scss +8 -0
- package/src/components/AppLogo.tsx +75 -0
- package/src/components/ChangeIndicator.tsx +80 -0
- package/src/components/Separator.tsx +32 -0
- package/src/components/ThemeSelectorCallout.scss +48 -0
- package/src/components/ThemeSelectorCallout.tsx +125 -0
- package/src/components/ToolBar.scss +39 -0
- package/src/components/ToolBar.tsx +26 -0
- package/src/components/index.ts +4 -0
- package/src/devices.ts +18 -0
- package/src/global.d.ts +4 -0
- package/src/i18n/i18n.json +68 -0
- package/src/i18n.ts +25 -0
- package/src/icons.tsx +198 -0
- package/src/index.css +1288 -0
- package/src/index.tsx +47 -0
- package/src/middleware.ts +54 -0
- package/src/panels/LeftPanel.scss +17 -0
- package/src/panels/LeftPanel.tsx +48 -0
- package/src/panels/changes/ChangeStack.module.scss +3 -0
- package/src/panels/changes/ChangeStack.tsx +219 -0
- package/src/panels/changes/ChangeStackHeader.tsx +43 -0
- package/src/panels/changes/ChangesPanel.module.scss +18 -0
- package/src/panels/changes/ChangesPanel.tsx +90 -0
- package/src/panels/changes/ControlGroup.module.scss +17 -0
- package/src/panels/changes/ControlGroup.tsx +61 -0
- package/src/panels/changes/PropertyChange.module.scss +24 -0
- package/src/panels/changes/PropertyChange.tsx +159 -0
- package/src/panels/changes/UnknownChange.module.scss +46 -0
- package/src/panels/changes/UnknownChange.tsx +96 -0
- package/src/panels/changes/index.tsx +3 -0
- package/src/panels/changes/utils.ts +36 -0
- package/src/panels/index.ts +2 -0
- package/src/panels/outline/Funnel.tsx +64 -0
- package/src/panels/outline/NoControlFound.tsx +45 -0
- package/src/panels/outline/OutlinePanel.scss +98 -0
- package/src/panels/outline/OutlinePanel.tsx +38 -0
- package/src/panels/outline/Tree.tsx +393 -0
- package/src/panels/outline/index.ts +1 -0
- package/src/panels/outline/utils.ts +154 -0
- package/src/panels/properties/Clipboard.tsx +44 -0
- package/src/panels/properties/DeviceSelector.tsx +40 -0
- package/src/panels/properties/DeviceToggle.tsx +39 -0
- package/src/panels/properties/DropdownEditor.tsx +80 -0
- package/src/panels/properties/Funnel.tsx +64 -0
- package/src/panels/properties/HeaderField.tsx +150 -0
- package/src/panels/properties/IconValueHelp.tsx +203 -0
- package/src/panels/properties/InputTypeSelector.tsx +20 -0
- package/src/panels/properties/InputTypeToggle.module.scss +4 -0
- package/src/panels/properties/InputTypeToggle.tsx +79 -0
- package/src/panels/properties/InputTypeWrapper.tsx +259 -0
- package/src/panels/properties/NoControlSelected.tsx +38 -0
- package/src/panels/properties/Properties.scss +102 -0
- package/src/panels/properties/PropertiesList.tsx +162 -0
- package/src/panels/properties/PropertiesPanel.tsx +30 -0
- package/src/panels/properties/PropertyDocumentation.module.scss +81 -0
- package/src/panels/properties/PropertyDocumentation.tsx +174 -0
- package/src/panels/properties/SapUiIcon.scss +109 -0
- package/src/panels/properties/StringEditor.tsx +122 -0
- package/src/panels/properties/ViewChanger.module.scss +5 -0
- package/src/panels/properties/ViewChanger.tsx +143 -0
- package/src/panels/properties/constants.ts +2 -0
- package/src/panels/properties/index.tsx +1 -0
- package/src/panels/properties/propertyValuesCache.ts +39 -0
- package/src/panels/properties/types.ts +49 -0
- package/src/slice.ts +216 -0
- package/src/store.ts +19 -0
- package/src/use-local-storage.ts +40 -0
- package/src/use-window-size.ts +39 -0
- package/src/variables.scss +2 -0
- package/test/unit/App.test.tsx +207 -0
- package/test/unit/appIndex.test.ts +23 -0
- package/test/unit/components/ChangeIndicator.test.tsx +120 -0
- package/test/unit/components/ThemeSelector.test.tsx +41 -0
- package/test/unit/middleware.test.ts +116 -0
- package/test/unit/panels/changes/ChangesPanel.test.tsx +261 -0
- package/test/unit/panels/changes/utils.test.ts +40 -0
- package/test/unit/panels/outline/OutlinePanel.test.tsx +353 -0
- package/test/unit/panels/outline/__snapshots__/utils.test.ts.snap +36 -0
- package/test/unit/panels/outline/utils.test.ts +83 -0
- package/test/unit/panels/properties/Clipboard.test.tsx +18 -0
- package/test/unit/panels/properties/DropdownEditor.test.tsx +62 -0
- package/test/unit/panels/properties/Funnel.test.tsx +34 -0
- package/test/unit/panels/properties/HeaderField.test.tsx +36 -0
- package/test/unit/panels/properties/IconValueHelp.test.tsx +60 -0
- package/test/unit/panels/properties/InputTypeToggle.test.tsx +126 -0
- package/test/unit/panels/properties/InputTypeWrapper.test.tsx +430 -0
- package/test/unit/panels/properties/PropertyDocumentation.test.tsx +131 -0
- package/test/unit/panels/properties/StringEditor.test.tsx +107 -0
- package/test/unit/panels/properties/ViewChanger.test.tsx +190 -0
- package/test/unit/panels/properties/propertyValuesCache.test.ts +23 -0
- package/test/unit/slice.test.ts +268 -0
- package/test/unit/utils.tsx +67 -0
- package/test/utils/utils.tsx +25 -0
- package/tsconfig.eslint.json +4 -0
- 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
|
+
}
|