@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
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,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
|
+
}
|