@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
package/src/index.tsx ADDED
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { Provider } from 'react-redux';
4
+
5
+ import { initIcons } from '@sap-ux/ui-components';
6
+ import { enableTelemetry } from '@sap-ux-private/control-property-editor-common';
7
+ import { initI18n } from './i18n';
8
+
9
+ import './index.css';
10
+ import App from './App';
11
+ import { store } from './store';
12
+ import type { ThemeName } from './components';
13
+ import { setThemeOnDocument } from './components';
14
+ import { registerAppIcons } from './icons';
15
+
16
+ export interface StartOptions {
17
+ previewUrl: string;
18
+ rootElementId: string;
19
+ telemetry?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Start Control Property Editor with options.
24
+ *
25
+ * @param options StartOptions
26
+ */
27
+ export function start(options: StartOptions): void {
28
+ const { previewUrl, rootElementId, telemetry = false } = options;
29
+ if (telemetry) {
30
+ enableTelemetry();
31
+ }
32
+ initI18n();
33
+ registerAppIcons();
34
+ initIcons();
35
+
36
+ const theme = localStorage.getItem('theme') ?? 'dark';
37
+ setThemeOnDocument(theme as ThemeName);
38
+
39
+ ReactDOM.render(
40
+ <React.StrictMode>
41
+ <Provider store={store}>
42
+ <App previewUrl={previewUrl} />
43
+ </Provider>
44
+ </React.StrictMode>,
45
+ document.getElementById(rootElementId)
46
+ );
47
+ }
@@ -0,0 +1,54 @@
1
+ import type { Dispatch } from 'redux';
2
+ import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
3
+
4
+ import type { ExternalAction } from '@sap-ux-private/control-property-editor-common';
5
+ import {
6
+ startPostMessageCommunication,
7
+ changeProperty as externalChangeProperty,
8
+ selectControl,
9
+ deletePropertyChanges
10
+ } from '@sap-ux-private/control-property-editor-common';
11
+
12
+ import type { Action } from './actions';
13
+ import { changeProperty } from './slice';
14
+
15
+ /**
16
+ * Communication between preview iframe and main application is realized through the communication middleware.
17
+ *
18
+ * @param store - redux store
19
+ * @returns Function
20
+ */
21
+ export const communicationMiddleware: Middleware<Dispatch<Action>> = (store: MiddlewareAPI) => {
22
+ const { sendAction } = startPostMessageCommunication<ExternalAction>(
23
+ function getTarget(): Window | undefined {
24
+ let result;
25
+ const target = (document.getElementById('preview') as HTMLIFrameElement).contentWindow;
26
+ if (target) {
27
+ result = target;
28
+ }
29
+ return result;
30
+ },
31
+ function onAction(action) {
32
+ store.dispatch(action);
33
+ return Promise.resolve();
34
+ }
35
+ );
36
+ return (next: Dispatch<Action>) =>
37
+ (action: Action): Action => {
38
+ action = next(action);
39
+
40
+ switch (action.type) {
41
+ case changeProperty.type: {
42
+ sendAction(externalChangeProperty(action.payload));
43
+ break;
44
+ }
45
+ case deletePropertyChanges.type:
46
+ case selectControl.type: {
47
+ sendAction(action);
48
+ break;
49
+ }
50
+ default:
51
+ }
52
+ return action;
53
+ };
54
+ };
@@ -0,0 +1,17 @@
1
+ .editor__outline {
2
+ .section__body {
3
+ margin-bottom: 0px;
4
+ margin-top: 0px;
5
+ margin-left: 0px;
6
+ margin-right: 0px;
7
+ padding-left: 0px !important;
8
+ padding-right: 0px;
9
+ }
10
+ .tree-no-control-found {
11
+ display: block;
12
+ }
13
+ .tree-modify-search-input {
14
+ display: block;
15
+ margin-bottom: 20px;
16
+ }
17
+ }
@@ -0,0 +1,48 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+
4
+ import { UISectionLayout, UISections, UISplitterLayoutType, UISplitterType } from '@sap-ux/ui-components';
5
+ import { AppLogo, Toolbar } from '../components';
6
+
7
+ import { ChangesPanel } from './changes';
8
+ import { OutlinePanel } from './outline';
9
+
10
+ import './LeftPanel.scss';
11
+
12
+ /**
13
+ * React element for left panel containing outline and change stack.
14
+ *
15
+ * @returns ReactElement
16
+ */
17
+ export function LeftPanel(): ReactElement {
18
+ return (
19
+ <>
20
+ <Toolbar left={<AppLogo />} />
21
+ <UISections
22
+ vertical={true}
23
+ splitter={true}
24
+ height="100%"
25
+ splitterType={UISplitterType.Resize}
26
+ splitterLayoutType={UISplitterLayoutType.Compact}
27
+ minSectionSize={[300, 190]}
28
+ sizes={[60, 40]}
29
+ sizesAsPercents={true}
30
+ animation={true}>
31
+ <UISections.Section
32
+ scrollable={true}
33
+ layout={UISectionLayout.Standard}
34
+ className="editor__outline"
35
+ height="100%">
36
+ <OutlinePanel />
37
+ </UISections.Section>
38
+ <UISections.Section
39
+ layout={UISectionLayout.Standard}
40
+ className="editor__outline"
41
+ height="100%"
42
+ cleanPadding={true}>
43
+ <ChangesPanel />
44
+ </UISections.Section>
45
+ </UISections>
46
+ </>
47
+ );
48
+ }
@@ -0,0 +1,3 @@
1
+ .item {
2
+ margin: 0px 15px;
3
+ }
@@ -0,0 +1,219 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+
4
+ import { Stack } from '@fluentui/react';
5
+
6
+ import type { Change, ValidChange } from '@sap-ux-private/control-property-editor-common';
7
+
8
+ import { Separator } from '../../components';
9
+ import type { ControlGroupProps, ControlPropertyChange } from './ControlGroup';
10
+ import { ControlGroup } from './ControlGroup';
11
+ import type { UnknownChangeProps } from './UnknownChange';
12
+ import { UnknownChange } from './UnknownChange';
13
+
14
+ import styles from './ChangeStack.module.scss';
15
+ import { useSelector } from 'react-redux';
16
+ import type { FilterOptions } from '../../slice';
17
+ import { FilterName } from '../../slice';
18
+ import type { RootState } from '../../store';
19
+ import { convertCamelCaseToPascalCase } from '@sap-ux-private/control-property-editor-common';
20
+ import { getFormattedDateAndTime } from './utils';
21
+
22
+ export interface ChangeStackProps {
23
+ changes: Change[];
24
+ }
25
+
26
+ /**
27
+ * React element for Change stack.
28
+ *
29
+ * @param changeStackProps ChangeStackProps
30
+ * @returns ReactElement
31
+ */
32
+ export function ChangeStack(changeStackProps: ChangeStackProps): ReactElement {
33
+ const { changes } = changeStackProps;
34
+ let groups = convertChanges(changes);
35
+ const filterQuery = useSelector<RootState, FilterOptions[]>((state) => state.filterQuery)
36
+ .filter((item) => item.name === FilterName.changeSummaryFilterQuery)[0]
37
+ .value.toString()
38
+ .toLowerCase();
39
+ groups = filterGroup(groups, filterQuery);
40
+ const stackName = changes[0].type === 'pending' ? 'unsaved-changes-stack' : 'saved-changes-stack';
41
+ return (
42
+ <Stack data-testid={stackName} tokens={{ childrenGap: 5, padding: '5px 0px 5px 0px' }}>
43
+ {groups.map((item, i) => [
44
+ isKnownChange(item) ? (
45
+ <Stack.Item
46
+ data-testid={`${stackName}-${item.controlId}-${item.changeIndex}`}
47
+ key={`${item.controlId}-${item.changeIndex}`}>
48
+ <ControlGroup {...item} />
49
+ </Stack.Item>
50
+ ) : (
51
+ <Stack.Item key={`${item.fileName}`}>
52
+ <UnknownChange {...item} />
53
+ </Stack.Item>
54
+ ),
55
+
56
+ i + 1 < groups.length ? (
57
+ <Stack.Item key={getKey(i)}>
58
+ <Separator className={styles.item} />
59
+ </Stack.Item>
60
+ ) : (
61
+ <></>
62
+ )
63
+ ])}
64
+ </Stack>
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Generate react attribute key.
70
+ *
71
+ * @param i number
72
+ * @returns string
73
+ */
74
+ function getKey(i: number): string {
75
+ return `${i}-separator`;
76
+ }
77
+
78
+ type Item = ControlGroupProps | UnknownChangeProps;
79
+
80
+ /**
81
+ * Method to convert changes to unknown or control group.
82
+ *
83
+ * @param changes Change[]
84
+ * @returns Item[]
85
+ */
86
+ function convertChanges(changes: Change[]): Item[] {
87
+ const items: Item[] = [];
88
+ let i = 0;
89
+ while (i < changes.length) {
90
+ const change = changes[i];
91
+ if (change.type === 'saved' && change.kind === 'unknown') {
92
+ items.push({
93
+ fileName: change.fileName,
94
+ timestamp: change.timestamp
95
+ });
96
+ i++;
97
+ } else {
98
+ const group: ControlGroupProps = {
99
+ controlId: change.controlId,
100
+ text: convertCamelCaseToPascalCase(change.controlName),
101
+ changeIndex: i,
102
+ changes: [toPropertyChangeProps(change, i)]
103
+ };
104
+ items.push(group);
105
+ i++;
106
+ while (i < changes.length) {
107
+ // We don't need to add header again if the next control is the same
108
+ const nextChange = changes[i];
109
+ if (
110
+ (nextChange.type === 'saved' && nextChange.kind === 'unknown') ||
111
+ change.controlId !== nextChange.controlId
112
+ ) {
113
+ break;
114
+ }
115
+ group.changes.push(toPropertyChangeProps(nextChange, i));
116
+ i++;
117
+ }
118
+ }
119
+ }
120
+ return items;
121
+ }
122
+
123
+ /**
124
+ * Converts a change to ControlPropertyChange.
125
+ *
126
+ * @param change ValidChange
127
+ * @param changeIndex number
128
+ * @returns ControlPropertyChange
129
+ */
130
+ function toPropertyChangeProps(change: ValidChange, changeIndex: number): ControlPropertyChange {
131
+ const { controlId, propertyName, value, controlName } = change;
132
+ const base = {
133
+ controlId,
134
+ controlName,
135
+ propertyName,
136
+ value,
137
+ changeIndex
138
+ };
139
+ if (change.type === 'pending') {
140
+ const { isActive } = change;
141
+ return {
142
+ ...base,
143
+ isActive
144
+ };
145
+ } else {
146
+ const { fileName, timestamp } = change;
147
+ return {
148
+ ...base,
149
+ isActive: true,
150
+ fileName,
151
+ timestamp
152
+ };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Returns true, if controlId is defined.
158
+ *
159
+ * @param change ControlGroupProps | UnknownChangeProps
160
+ * @returns boolean
161
+ */
162
+ export function isKnownChange(change: ControlGroupProps | UnknownChangeProps): change is ControlGroupProps {
163
+ return (change as ControlGroupProps).controlId !== undefined;
164
+ }
165
+
166
+ const filterPropertyChanges = (changes: ControlPropertyChange[], query: string): ControlPropertyChange[] => {
167
+ return changes.filter((item) => {
168
+ return (
169
+ !query ||
170
+ item.propertyName.trim().toLowerCase().includes(query) ||
171
+ convertCamelCaseToPascalCase(item.propertyName.toString()).trim().toLowerCase().includes(query) ||
172
+ item.value.toString().trim().toLowerCase().includes(query) ||
173
+ convertCamelCaseToPascalCase(item.value.toString()).trim().toLowerCase().includes(query) ||
174
+ (item.timestamp && getFormattedDateAndTime(item.timestamp).trim().toLowerCase().includes(query))
175
+ );
176
+ });
177
+ };
178
+
179
+ /**
180
+ * Filter group in change stack.
181
+ *
182
+ * @param model Item[]
183
+ * @param query string
184
+ * @returns Item[]
185
+ */
186
+ function filterGroup(model: Item[], query: string): Item[] {
187
+ const filteredModel: Item[] = [];
188
+ if (query.length === 0) {
189
+ return model;
190
+ }
191
+ for (const item of model) {
192
+ let parentMatch = false;
193
+ if (!isKnownChange(item)) {
194
+ continue;
195
+ }
196
+ const name = item.text.trim().toLowerCase();
197
+ if (name.includes(query)) {
198
+ parentMatch = true;
199
+ // add node without its children
200
+ filteredModel.push({ ...item, changes: [] });
201
+ }
202
+ const controlPropModel = item;
203
+ if (controlPropModel.changes.length <= 0) {
204
+ continue;
205
+ }
206
+ const data = filterPropertyChanges(controlPropModel.changes, query);
207
+
208
+ if (parentMatch) {
209
+ // parent matched filter query and pushed already to `filterModel`. only replace matched children
210
+ (filteredModel[filteredModel.length - 1] as ControlGroupProps).changes = controlPropModel.changes;
211
+ // add node and its matched children
212
+ } else if (data.length > 0) {
213
+ const newFilterModel = { ...item, changes: data };
214
+ filteredModel.push(newFilterModel);
215
+ }
216
+ }
217
+
218
+ return filteredModel;
219
+ }
@@ -0,0 +1,43 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+
4
+ import { Text } from '@fluentui/react';
5
+
6
+ import { sectionHeaderFontSize } from '../properties/constants';
7
+
8
+ export interface ChangeStackHeaderProps {
9
+ backgroundColor: string;
10
+ color: string;
11
+ text: string;
12
+ }
13
+
14
+ /**
15
+ * React element of header of change stack.
16
+ *
17
+ * @param changeStackHeaderProps ChangeStackHeaderProps
18
+ * @returns ReactElement
19
+ */
20
+ export function ChangeStackHeader(changeStackHeaderProps: ChangeStackHeaderProps): ReactElement {
21
+ const { backgroundColor, color, text } = changeStackHeaderProps;
22
+ return (
23
+ <div
24
+ style={{
25
+ backgroundColor: backgroundColor,
26
+ padding: '6px 15px'
27
+ }}>
28
+ <Text
29
+ style={{
30
+ color: color,
31
+ fontSize: sectionHeaderFontSize,
32
+ fontWeight: 'bold',
33
+ textOverflow: 'ellipsis',
34
+ whiteSpace: 'nowrap',
35
+ overflowX: 'hidden',
36
+ lineHeight: '18px',
37
+ display: 'block'
38
+ }}>
39
+ {text}
40
+ </Text>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,18 @@
1
+ .filter {
2
+ display: flex;
3
+ margin: 17px 15px 10px 15px;
4
+ flex-direction: row;
5
+ align-items: center;
6
+ overflow-y: auto;
7
+ }
8
+
9
+ .noData {
10
+ font-style: normal;
11
+ font-weight: bolder;
12
+ font-size: 15px;
13
+ line-height: 21px;
14
+ text-align: center;
15
+ padding-top: 20px;
16
+ margin-left: 35px;
17
+ color: var(--vscode-foreground);
18
+ }
@@ -0,0 +1,90 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useDispatch, useSelector } from 'react-redux';
6
+
7
+ import { Text } from '@fluentui/react';
8
+ import { UISearchBox } from '@sap-ux/ui-components';
9
+
10
+ import type { ChangesSlice } from '../../slice';
11
+ import { FilterName, filterNodes } from '../../slice';
12
+ import type { RootState } from '../../store';
13
+
14
+ import { Separator } from '../../components';
15
+ import { ChangeStack } from './ChangeStack';
16
+ import { ChangeStackHeader } from './ChangeStackHeader';
17
+
18
+ import styles from './ChangesPanel.module.scss';
19
+
20
+ /**
21
+ * React element for ChangePanel.
22
+ *
23
+ * @returns ReactElement
24
+ */
25
+ export function ChangesPanel(): ReactElement {
26
+ const { t } = useTranslation();
27
+ const dispatch = useDispatch();
28
+ const { pending, saved } = useSelector<RootState, ChangesSlice>((state) => state.changes);
29
+ const onFilterChange = (
30
+ event?: React.ChangeEvent<HTMLInputElement> | undefined,
31
+ filterValue?: string | undefined
32
+ ): void => {
33
+ const action = filterNodes([{ name: FilterName.changeSummaryFilterQuery, value: filterValue ?? '' }]);
34
+ dispatch(action);
35
+ };
36
+
37
+ /**
38
+ * Method renders the ReactElement for ChangePanel.
39
+ *
40
+ * @returns ReactElement
41
+ */
42
+ function renderChanges(): ReactElement {
43
+ if (pending.length === 0 && saved.length === 0) {
44
+ return <Text className={styles.noData}>{t('NO_CONTROL_CHANGES_FOUND')}</Text>;
45
+ }
46
+ return (
47
+ <>
48
+ {pending.length > 0 && (
49
+ <>
50
+ <Separator />
51
+ <ChangeStackHeader
52
+ backgroundColor="var(--vscode-sideBar-background);"
53
+ color="var(--vscode-editor-foreground)"
54
+ text={t('CHANGE_SUMMARY_UNSAVED_CHANGES')}
55
+ />
56
+ <Separator />
57
+ <ChangeStack key="pending-changes" changes={pending} />
58
+ </>
59
+ )}
60
+ {saved.length > 0 && (
61
+ <>
62
+ <Separator />
63
+ <ChangeStackHeader
64
+ backgroundColor="var(--vscode-sideBar-background);"
65
+ color="var(--vscode-terminal-ansiGreen)"
66
+ text={t('CHANGE_SUMMARY_SAVED_CHANGES')}
67
+ />
68
+ <Separator />
69
+ <ChangeStack key="saved-changes" changes={saved} />
70
+ </>
71
+ )}
72
+ <Separator />
73
+ </>
74
+ );
75
+ }
76
+
77
+ return (
78
+ <>
79
+ <div className={styles.filter}>
80
+ <UISearchBox
81
+ autoFocus={false}
82
+ disableAnimation={false}
83
+ placeholder={t('FILTER')}
84
+ onChange={onFilterChange}
85
+ />
86
+ </div>
87
+ {renderChanges()}
88
+ </>
89
+ );
90
+ }
@@ -0,0 +1,17 @@
1
+ .header,
2
+ .item {
3
+ padding: 5px 15px 5px 15px;
4
+ }
5
+
6
+ .item:hover {
7
+ background-color: var(--vscode-dropdown-background);
8
+ color: var(--vscode-dropdown-foreground);
9
+ outline: 1px dashed var(--vscode-contrastActiveBorder);
10
+ .actions {
11
+ visibility: visible;
12
+ }
13
+ }
14
+
15
+ .actions {
16
+ visibility: hidden;
17
+ }
@@ -0,0 +1,61 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react';
3
+ import { Link, Stack } from '@fluentui/react';
4
+
5
+ import { useAppDispatch } from '../../store';
6
+ import { selectControl } from '@sap-ux-private/control-property-editor-common';
7
+
8
+ import type { PropertyChangeProps } from './PropertyChange';
9
+ import { PropertyChange } from './PropertyChange';
10
+
11
+ import styles from './ControlGroup.module.scss';
12
+
13
+ export interface ControlGroupProps {
14
+ text: string;
15
+ controlId: string;
16
+ changeIndex: number;
17
+ changes: ControlPropertyChange[];
18
+ }
19
+ export type ControlPropertyChange = Omit<PropertyChangeProps, 'actionClassName'>;
20
+
21
+ /**
22
+ * React Element for control groups.
23
+ *
24
+ * @param controlGroupProps ControlGroupProps
25
+ * @returns ReactElement
26
+ */
27
+ export function ControlGroup(controlGroupProps: ControlGroupProps): ReactElement {
28
+ const { text, controlId, changes } = controlGroupProps;
29
+ const dispatch = useAppDispatch();
30
+ const stackName = changes[0].timestamp ? `saved-changes-stack` : `unsaved-changes-stack`;
31
+ return (
32
+ <Stack>
33
+ <Stack.Item className={styles.header}>
34
+ <Link
35
+ onClick={(): void => {
36
+ const action = selectControl(controlId);
37
+ dispatch(action);
38
+ }}
39
+ style={{
40
+ color: 'var(--vscode-textLink-foreground)',
41
+ fontSize: '13px',
42
+ fontWeight: 'bold',
43
+ textOverflow: 'ellipsis',
44
+ whiteSpace: 'nowrap',
45
+ overflowX: 'hidden',
46
+ lineHeight: '18px'
47
+ }}>
48
+ {text}
49
+ </Link>
50
+ </Stack.Item>
51
+ {changes.map((change) => (
52
+ <Stack.Item
53
+ data-testid={`${stackName}-${controlId}-${change.propertyName}-${change.changeIndex}`}
54
+ key={`${change.changeIndex}`}
55
+ className={styles.item}>
56
+ <PropertyChange {...change} actionClassName={styles.actions} />
57
+ </Stack.Item>
58
+ ))}
59
+ </Stack>
60
+ );
61
+ }
@@ -0,0 +1,24 @@
1
+ .text,
2
+ .timestamp {
3
+ color: var(--vscode-editor-foreground);
4
+ }
5
+
6
+ .text {
7
+ margin-right: 5px;
8
+ line-height: 18px;
9
+ display: inline-block;
10
+ }
11
+
12
+ .timestamp {
13
+ font-size: 11px;
14
+ line-height: 15px;
15
+ opacity: 0.5;
16
+ }
17
+
18
+ .valueIcon svg path {
19
+ fill: #ffffff !important;
20
+ }
21
+
22
+ .property {
23
+ overflow-wrap: anywhere;
24
+ }