@sap-ux/control-property-editor 0.5.21 → 0.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Control Property Editor",
4
4
  "description": "Control Property Editor",
5
5
  "license": "Apache-2.0",
6
- "version": "0.5.21",
6
+ "version": "0.5.22",
7
7
  "main": "dist/app.js",
8
8
  "repository": {
9
9
  "type": "git",
@@ -49,8 +49,8 @@
49
49
  "autoprefixer": "10.4.7",
50
50
  "postcss": "8.4.31",
51
51
  "yargs-parser": "21.1.1",
52
- "@sap-ux/ui-components": "1.19.0",
53
- "@sap-ux-private/control-property-editor-common": "0.5.4"
52
+ "@sap-ux/ui-components": "1.20.1",
53
+ "@sap-ux-private/control-property-editor-common": "0.5.5"
54
54
  },
55
55
  "scripts": {
56
56
  "clean": "rimraf --glob dist coverage *.tsbuildinfo",
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { Stack } from '@fluentui/react';
4
4
  import type { Change } from '@sap-ux-private/control-property-editor-common';
5
5
  import {
6
+ CONTROL_CHANGE_KIND,
6
7
  convertCamelCaseToPascalCase,
7
8
  PENDING_CHANGE_TYPE,
8
9
  PROPERTY_CHANGE_KIND,
@@ -22,6 +23,8 @@ import type { FilterOptions } from '../../slice';
22
23
  import { FilterName } from '../../slice';
23
24
  import type { RootState } from '../../store';
24
25
  import { getFormattedDateAndTime } from './utils';
26
+ import type { ControlItemProps } from './ControlChange';
27
+ import { ControlChange } from './ControlChange';
25
28
 
26
29
  export interface ChangeStackProps {
27
30
  changes: Change[];
@@ -44,31 +47,57 @@ export function ChangeStack(changeStackProps: ChangeStackProps): ReactElement {
44
47
  const stackName = changes[0].type === PENDING_CHANGE_TYPE ? 'unsaved-changes-stack' : 'saved-changes-stack';
45
48
  return (
46
49
  <Stack data-testid={stackName} tokens={{ childrenGap: 5, padding: '5px 0px 5px 0px' }}>
47
- {groups.map((item, i) => [
48
- isControlGroup(item) ? (
49
- <Stack.Item
50
- data-testid={`${stackName}-${item.controlId}-${item.index}`}
51
- key={`${item.controlId}-${item.index}`}>
52
- <ControlGroup {...item} />
53
- </Stack.Item>
54
- ) : (
55
- <Stack.Item key={`${item.fileName}`}>
56
- <UnknownChange {...item} />
57
- </Stack.Item>
58
- ),
59
-
60
- i + 1 < groups.length ? (
61
- <Stack.Item key={getKey(i)}>
62
- <Separator className={styles.item} />
63
- </Stack.Item>
64
- ) : (
65
- <></>
66
- )
67
- ])}
50
+ {groups.map((item, i) => [renderChangeItem(item, stackName), renderSeparator(i, groups)])}
68
51
  </Stack>
69
52
  );
70
53
  }
71
54
 
55
+ /**
56
+ * Renders the appropriate change item component based on the type of the item.
57
+ *
58
+ * @param item - The current item from the group to be rendered.
59
+ * @param stackName - The name of the stack used for test IDs.
60
+ * @returns The rendered change item (`ControlGroup`, `ControlChange`, or `UnknownChange`).
61
+ */
62
+ function renderChangeItem(item: Item, stackName: string): ReactElement {
63
+ if (isPropertyGroup(item)) {
64
+ return (
65
+ <Stack.Item
66
+ data-testid={`${stackName}-${item.controlId}-${item.index}`}
67
+ key={`${item.controlId}-${item.index}`}>
68
+ <ControlGroup {...item} />
69
+ </Stack.Item>
70
+ );
71
+ } else if (isControlItem(item)) {
72
+ return (
73
+ <Stack.Item key={`${item.fileName}`}>
74
+ <ControlChange {...item} />
75
+ </Stack.Item>
76
+ );
77
+ } else {
78
+ return (
79
+ <Stack.Item key={`${item.fileName}`}>
80
+ <UnknownChange {...item} />
81
+ </Stack.Item>
82
+ );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Renders a separator between items, except for the last one.
88
+ *
89
+ * @param i - The index of the current item in the group.
90
+ * @param groups - The array of all groups to check if it's the last item.
91
+ * @returns {ReactElement | null} The rendered separator or `null` if it's the last item.
92
+ */
93
+ function renderSeparator(i: number, groups: Item[]): ReactElement | null {
94
+ return i + 1 < groups.length ? (
95
+ <Stack.Item key={getKey(i)}>
96
+ <Separator className={styles.item} />
97
+ </Stack.Item>
98
+ ) : null;
99
+ }
100
+
72
101
  /**
73
102
  * Generate react attribute key.
74
103
  *
@@ -79,13 +108,13 @@ function getKey(i: number): string {
79
108
  return `${i}-separator`;
80
109
  }
81
110
 
82
- type Item = ControlGroupProps | UnknownChangeProps;
111
+ type Item = ControlGroupProps | UnknownChangeProps | ControlItemProps;
83
112
 
84
113
  /**
85
- * Method to convert changes to unknown or control group.
114
+ * Converts an array of changes into an array of items, grouping changes by controlId and handling different kinds of changes.
86
115
  *
87
- * @param changes Change[]
88
- * @returns Item[]
116
+ * @param {Change[]} changes - An array of changes to be converted.
117
+ * @returns {Item[]} An array of items, some of which may be control groups.
89
118
  */
90
119
  function convertChanges(changes: Change[]): Item[] {
91
120
  const items: Item[] = [];
@@ -93,28 +122,24 @@ function convertChanges(changes: Change[]): Item[] {
93
122
  while (i < changes.length) {
94
123
  const change: Change = changes[i];
95
124
  let group: ControlGroupProps;
96
- if (change.kind === UNKNOWN_CHANGE_KIND && change.type === SAVED_CHANGE_TYPE) {
97
- items.push({
98
- fileName: change.fileName,
99
- timestamp: change.timestamp,
100
- header: true,
101
- controlId: change.controlId ?? ''
102
- });
125
+ if (change.kind === UNKNOWN_CHANGE_KIND) {
126
+ items.push(handleUnknownChange(change));
127
+ i++;
128
+ } else if (change.kind === CONTROL_CHANGE_KIND) {
129
+ items.push(handleControlChange(change));
103
130
  i++;
104
131
  } else {
105
- group = {
106
- controlId: change.controlId,
107
- controlName: change.controlName,
108
- text: convertCamelCaseToPascalCase(change.controlName),
109
- index: i,
110
- changes: [change]
111
- };
132
+ group = handleGroupedChange(change, i);
112
133
  items.push(group);
113
134
  i++;
114
135
  while (i < changes.length) {
115
136
  // We don't need to add header again if the next control is the same
116
137
  const nextChange = changes[i];
117
- if (nextChange.kind === UNKNOWN_CHANGE_KIND || change.controlId !== nextChange.controlId) {
138
+ if (
139
+ nextChange.kind === UNKNOWN_CHANGE_KIND ||
140
+ nextChange.kind === CONTROL_CHANGE_KIND ||
141
+ change.controlId !== nextChange.controlId
142
+ ) {
118
143
  break;
119
144
  }
120
145
  group.changes.push(nextChange);
@@ -125,16 +150,76 @@ function convertChanges(changes: Change[]): Item[] {
125
150
  return items;
126
151
  }
127
152
 
153
+ /**
154
+ * Handles changes of kind `unknown` and creates an item with a header.
155
+ *
156
+ * @param {Change} change - The change object of kind `unknown`.
157
+ * @returns {Item} An item object containing the filename and header information.
158
+ */
159
+ function handleUnknownChange(change: Change): Item {
160
+ return {
161
+ fileName: change.fileName,
162
+ header: true,
163
+ timestamp: change.type === SAVED_CHANGE_TYPE ? change.timestamp : undefined
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Handles changes of kind `control` and creates an item with controlId and type.
169
+ *
170
+ * @param {Change} change - The change object of kind `control`.
171
+ * @returns {Item} An item object containing the filename, controlId, type, and optional timestamp.
172
+ */
173
+ function handleControlChange(change: Change & { controlId: string }): Item {
174
+ return {
175
+ fileName: change.fileName,
176
+ controlId: change.controlId,
177
+ timestamp: change.type === SAVED_CHANGE_TYPE ? change.timestamp : undefined,
178
+ type: change.type
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Handles grouped changes by initializing a control group with a list of changes that share the same controlId.
184
+ *
185
+ * @param {Change} change - The initial change object to start the group.
186
+ * @param {number} i - The index of the initial change in the list.
187
+ * @returns {ControlGroupProps} A control group object containing grouped changes.
188
+ */
189
+ function handleGroupedChange(
190
+ change: Change & { controlId: string; controlName: string },
191
+ i: number
192
+ ): ControlGroupProps {
193
+ return {
194
+ controlId: change.controlId,
195
+ controlName: change.controlName,
196
+ text: convertCamelCaseToPascalCase(change.controlName),
197
+ index: i,
198
+ changes: [change],
199
+ timestamp: change.type === SAVED_CHANGE_TYPE ? change.timestamp : undefined
200
+ };
201
+ }
202
+
128
203
  /**
129
204
  * Checks if item is of type {@link ControlGroupProps}.
130
205
  *
131
- * @param item ControlGroupProps | UnknownChangeProps
206
+ * @param item ControlGroupProps | UnknownChangeProps | ControlItemProps
132
207
  * @returns boolean
133
208
  */
134
- function isControlGroup(item: ControlGroupProps | UnknownChangeProps): item is ControlGroupProps {
209
+ function isPropertyGroup(item: ControlGroupProps | UnknownChangeProps | ControlItemProps): item is ControlGroupProps {
135
210
  return (item as ControlGroupProps).controlName !== undefined;
136
211
  }
137
212
 
213
+ /**
214
+ * Checks if item is of type {@link ControlItemProps}.
215
+ *
216
+ * @param item UnknownChangeProps | ControlItemProps
217
+ * @returns boolean
218
+ */
219
+ function isControlItem(item: UnknownChangeProps | ControlItemProps): item is ControlItemProps {
220
+ return item?.controlId !== undefined;
221
+ }
222
+
138
223
  const filterPropertyChanges = (changes: Change[], query: string): Change[] => {
139
224
  return changes.filter((item): boolean => {
140
225
  if (item.kind === PROPERTY_CHANGE_KIND) {
@@ -184,7 +269,7 @@ function filterGroup(model: Item[], query: string): Item[] {
184
269
  }
185
270
  for (const item of model) {
186
271
  let parentMatch = false;
187
- if (!isControlGroup(item)) {
272
+ if (!isPropertyGroup(item)) {
188
273
  if (isQueryMatchesChange(item, query)) {
189
274
  filteredModel.push({ ...item, changes: [] });
190
275
  }
@@ -0,0 +1,139 @@
1
+ import type { ReactElement } from 'react';
2
+ import React, { useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Text, Stack, Link } from '@fluentui/react';
5
+ import { useDispatch } from 'react-redux';
6
+ import styles from './UnknownChange.module.scss';
7
+ import { UIIconButton, UiIcons, UIDialog } from '@sap-ux/ui-components';
8
+ import type {
9
+ PropertyChangeDeletionDetails,
10
+ PENDING_CHANGE_TYPE
11
+ } from '@sap-ux-private/control-property-editor-common';
12
+ import {
13
+ SAVED_CHANGE_TYPE,
14
+ convertCamelCaseToPascalCase,
15
+ deletePropertyChanges,
16
+ selectControl
17
+ } from '@sap-ux-private/control-property-editor-common';
18
+ import { getFormattedDateAndTime } from './utils';
19
+
20
+ export interface ControlItemProps {
21
+ fileName: string;
22
+ timestamp?: number;
23
+ controlId: string;
24
+ type: typeof SAVED_CHANGE_TYPE | typeof PENDING_CHANGE_TYPE;
25
+ }
26
+
27
+ /**
28
+ * React element for control change in the change stack.
29
+ *
30
+ * @param {Readonly<ControlItemProps>} props - The props object.
31
+ * @param {string} props.controlId - The ID of the control being changed.
32
+ * @param {string} props.fileName - The name of the file associated with the change.
33
+ * @param {number} [props.timestamp] - The timestamp of the change, optional.
34
+ * @param {string} props.type - The type of the change (e.g., 'saved' | 'pending').
35
+ * @returns {ReactElement} A React element that renders the control change UI.
36
+ */
37
+ export function ControlChange({ controlId, fileName, timestamp, type }: Readonly<ControlItemProps>): ReactElement {
38
+ const { t } = useTranslation();
39
+ const dispatch = useDispatch();
40
+ const [dialogState, setDialogState] = useState<PropertyChangeDeletionDetails | undefined>(undefined);
41
+
42
+ const name = useMemo(() => {
43
+ const parts = fileName.split('_');
44
+ const changeName = parts[parts.length - 1];
45
+ return convertCamelCaseToPascalCase(changeName);
46
+ }, [fileName]);
47
+
48
+ const onConfirmDelete = (): void => {
49
+ if (dialogState) {
50
+ dispatch(deletePropertyChanges(dialogState));
51
+ setDialogState(undefined);
52
+ }
53
+ };
54
+
55
+ const onCancelDelete = (): void => {
56
+ setDialogState(undefined);
57
+ };
58
+
59
+ return (
60
+ <>
61
+ <Stack className={styles.item}>
62
+ <Stack.Item className={styles.property}>
63
+ <Stack horizontal>
64
+ <Stack.Item>
65
+ <Link
66
+ className={styles.textHeader}
67
+ onClick={(): void => {
68
+ const action = selectControl(controlId);
69
+ dispatch(action);
70
+ }}
71
+ style={{
72
+ color: 'var(--vscode-textLink-foreground)',
73
+ fontSize: '13px',
74
+ fontWeight: 'bold',
75
+ textOverflow: 'ellipsis',
76
+ whiteSpace: 'nowrap',
77
+ overflowX: 'hidden',
78
+ lineHeight: '18px'
79
+ }}>
80
+ {name} {t('CHANGE')}
81
+ </Link>
82
+
83
+ <Stack horizontal>
84
+ <Stack.Item className={styles.fileLabel}>{t('FILE')}</Stack.Item>
85
+ <Stack.Item className={styles.fileText} title={fileName}>
86
+ {fileName}
87
+ </Stack.Item>
88
+ </Stack>
89
+ {controlId && (
90
+ <Stack horizontal>
91
+ <Stack.Item className={styles.controlLabel}>{t('CONTROL')}</Stack.Item>
92
+ <Stack.Item className={styles.controlText} title={controlId}>
93
+ {controlId}
94
+ </Stack.Item>
95
+ </Stack>
96
+ )}
97
+ </Stack.Item>
98
+
99
+ {type === SAVED_CHANGE_TYPE && (
100
+ <Stack.Item className={styles.actions}>
101
+ <UIIconButton
102
+ iconProps={{ iconName: UiIcons.TrashCan }}
103
+ onClick={(): void => {
104
+ setDialogState({
105
+ controlId: '',
106
+ propertyName: '',
107
+ fileName
108
+ });
109
+ }}
110
+ />
111
+ </Stack.Item>
112
+ )}
113
+ </Stack>
114
+ </Stack.Item>
115
+ {timestamp && (
116
+ <Stack.Item>
117
+ <Stack horizontal horizontalAlign="space-between">
118
+ <Text className={styles.timestamp}>{getFormattedDateAndTime(timestamp ?? 0)}</Text>
119
+ </Stack>
120
+ </Stack.Item>
121
+ )}
122
+ </Stack>
123
+
124
+ {dialogState && (
125
+ <UIDialog
126
+ hidden={dialogState === undefined}
127
+ onAccept={onConfirmDelete}
128
+ acceptButtonText={t('CONFIRM_DELETE')}
129
+ cancelButtonText={t('CANCEL_DELETE')}
130
+ onCancel={onCancelDelete}
131
+ dialogContentProps={{
132
+ title: t('CONFIRM_OTHER_CHANGE_DELETE_TITLE'),
133
+ subText: t('CONFIRM_OTHER_CHANGE_DELETE_SUBTEXT', { name })
134
+ }}
135
+ />
136
+ )}
137
+ </>
138
+ );
139
+ }
@@ -1,13 +1,12 @@
1
- import type { ReactElement } from 'react';
2
1
  import React from 'react';
2
+ import type { ReactElement } from 'react';
3
3
  import { Link, Stack } from '@fluentui/react';
4
4
 
5
- import { useAppDispatch } from '../../store';
6
- import type { Change } from '@sap-ux-private/control-property-editor-common';
7
5
  import { PROPERTY_CHANGE_KIND, SAVED_CHANGE_TYPE, selectControl } from '@sap-ux-private/control-property-editor-common';
6
+ import type { Change } from '@sap-ux-private/control-property-editor-common';
8
7
 
9
8
  import { PropertyChange } from './PropertyChange';
10
- import { OtherChange } from './OtherChange';
9
+ import { useAppDispatch } from '../../store';
11
10
 
12
11
  import styles from './ControlGroup.module.scss';
13
12
 
@@ -17,6 +16,7 @@ export interface ControlGroupProps {
17
16
  controlName: string;
18
17
  index: number;
19
18
  changes: Change[];
19
+ timestamp?: number;
20
20
  }
21
21
 
22
22
  /**
@@ -59,9 +59,7 @@ export function ControlGroup(controlGroupProps: ControlGroupProps): ReactElement
59
59
  className={styles.item}>
60
60
  {change.kind === PROPERTY_CHANGE_KIND ? (
61
61
  <PropertyChange change={change} actionClassName={styles.actions} />
62
- ) : (
63
- <OtherChange change={change} actionClassName={styles.actions} />
64
- )}
62
+ ) : null}
65
63
  </Stack.Item>
66
64
  );
67
65
  })}
package/src/slice.ts CHANGED
@@ -35,7 +35,8 @@ import {
35
35
  applicationModeChanged,
36
36
  UNKNOWN_CHANGE_KIND,
37
37
  SAVED_CHANGE_TYPE,
38
- PENDING_CHANGE_TYPE
38
+ PENDING_CHANGE_TYPE,
39
+ PROPERTY_CHANGE_KIND
39
40
  } from '@sap-ux-private/control-property-editor-common';
40
41
  import { DeviceType } from './devices';
41
42
 
@@ -158,6 +159,53 @@ export const initialState: SliceState = {
158
159
  quickActions: []
159
160
  };
160
161
 
162
+ /**
163
+ * Process a control and update the control stats.
164
+ *
165
+ * @param control The control to update
166
+ * @param changeType The type of change
167
+ */
168
+ const processControl = (control: ControlChangeStats, changeType: string): void => {
169
+ if (changeType === PENDING_CHANGE_TYPE) {
170
+ control.pending++;
171
+ } else if (changeType === SAVED_CHANGE_TYPE) {
172
+ control.saved++;
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Process a property change and update the property stats.
178
+ *
179
+ * @param control The control to update
180
+ * @param change The change to process
181
+ */
182
+ const processPropertyChange = (
183
+ control: ControlChangeStats,
184
+ change: PendingPropertyChange | SavedPropertyChange
185
+ ): void => {
186
+ const { propertyName } = change;
187
+
188
+ const property = control.properties[propertyName]
189
+ ? {
190
+ pending: control.properties[propertyName].pending,
191
+ saved: control.properties[propertyName].saved,
192
+ lastSavedChange: control.properties[propertyName].lastSavedChange,
193
+ lastChange: control.properties[propertyName].lastChange
194
+ }
195
+ : {
196
+ pending: 0,
197
+ saved: 0
198
+ };
199
+ if (change.type === PENDING_CHANGE_TYPE) {
200
+ property.pending++;
201
+ property.lastChange = change;
202
+ } else if (change.type === SAVED_CHANGE_TYPE) {
203
+ property.lastSavedChange = change;
204
+ property.saved++;
205
+ }
206
+ control.properties[propertyName] = property;
207
+ };
208
+
161
209
  const slice = createSlice<SliceState, SliceCaseReducers<SliceState>, string>({
162
210
  name: 'app',
163
211
  initialState,
@@ -238,7 +286,7 @@ const slice = createSlice<SliceState, SliceCaseReducers<SliceState>, string>({
238
286
  if (change.kind === UNKNOWN_CHANGE_KIND) {
239
287
  continue;
240
288
  }
241
- const { controlId, propertyName, type, controlName } = change;
289
+ const { controlId, type } = change;
242
290
  const key = `${controlId}`;
243
291
  const control = state.changes.controls[key]
244
292
  ? {
@@ -250,33 +298,14 @@ const slice = createSlice<SliceState, SliceCaseReducers<SliceState>, string>({
250
298
  : {
251
299
  pending: 0,
252
300
  saved: 0,
253
- controlName: controlName ?? '',
301
+ controlName: change.kind === PROPERTY_CHANGE_KIND ? change.controlName : '',
254
302
  properties: {}
255
303
  };
256
- if (type === PENDING_CHANGE_TYPE) {
257
- control.pending++;
258
- } else if (type === SAVED_CHANGE_TYPE) {
259
- control.saved++;
260
- }
261
- const property = control.properties[propertyName]
262
- ? {
263
- pending: control.properties[propertyName].pending,
264
- saved: control.properties[propertyName].saved,
265
- lastSavedChange: control.properties[propertyName].lastSavedChange,
266
- lastChange: control.properties[propertyName].lastChange
267
- }
268
- : {
269
- pending: 0,
270
- saved: 0
271
- };
272
- if (change.type === PENDING_CHANGE_TYPE) {
273
- property.pending++;
274
- property.lastChange = change;
275
- } else if (change.type === SAVED_CHANGE_TYPE) {
276
- property.lastSavedChange = change;
277
- property.saved++;
304
+ processControl(control, type);
305
+ if (change.kind === PROPERTY_CHANGE_KIND) {
306
+ processPropertyChange(control, change);
278
307
  }
279
- control.properties[propertyName] = property;
308
+
280
309
  state.changes.controls[key] = control;
281
310
  }
282
311
  })