@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/slice.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { SliceCaseReducers } from '@reduxjs/toolkit';
|
|
2
|
+
import { createSlice, createAction } from '@reduxjs/toolkit';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Control,
|
|
6
|
+
IconDetails,
|
|
7
|
+
OutlineNode,
|
|
8
|
+
PendingPropertyChange,
|
|
9
|
+
PropertyChange,
|
|
10
|
+
SavedPropertyChange
|
|
11
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
12
|
+
import {
|
|
13
|
+
changeStackModified,
|
|
14
|
+
controlSelected,
|
|
15
|
+
iconsLoaded,
|
|
16
|
+
outlineChanged,
|
|
17
|
+
propertyChanged,
|
|
18
|
+
propertyChangeFailed
|
|
19
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
20
|
+
import { DeviceType } from './devices';
|
|
21
|
+
|
|
22
|
+
interface SliceState {
|
|
23
|
+
deviceType: DeviceType;
|
|
24
|
+
scale: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* If set to true on resize the preview will be scaled to fit available space
|
|
28
|
+
*/
|
|
29
|
+
fitPreview?: boolean;
|
|
30
|
+
selectedControl: Control | undefined;
|
|
31
|
+
outline: OutlineNode[];
|
|
32
|
+
filterQuery: FilterOptions[];
|
|
33
|
+
icons: IconDetails[];
|
|
34
|
+
changes: ChangesSlice;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ChangesSlice {
|
|
38
|
+
controls: ControlChanges;
|
|
39
|
+
pending: PendingPropertyChange[];
|
|
40
|
+
saved: SavedPropertyChange[];
|
|
41
|
+
}
|
|
42
|
+
export interface ControlChanges {
|
|
43
|
+
[id: string]: ControlChangeStats;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ControlChangeStats {
|
|
47
|
+
controlName: string;
|
|
48
|
+
pending: number;
|
|
49
|
+
saved: number;
|
|
50
|
+
properties: PropertyChanges;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PropertyChanges {
|
|
54
|
+
[id: string]: PropertyChangeStats;
|
|
55
|
+
}
|
|
56
|
+
export interface PropertyChangeStats {
|
|
57
|
+
pending: number;
|
|
58
|
+
saved: number;
|
|
59
|
+
lastSavedChange?: SavedPropertyChange;
|
|
60
|
+
lastChange?: PendingPropertyChange;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const enum FilterName {
|
|
64
|
+
focusEditable = 'focus-editable-controls',
|
|
65
|
+
focusCommonlyUsed = 'focus-commonly-used-controls',
|
|
66
|
+
query = 'query',
|
|
67
|
+
changeSummaryFilterQuery = 'change-summary-filter-query',
|
|
68
|
+
showEditableProperties = 'show-editable-properties'
|
|
69
|
+
}
|
|
70
|
+
export interface FilterOptions {
|
|
71
|
+
name: FilterName;
|
|
72
|
+
value: string | boolean;
|
|
73
|
+
}
|
|
74
|
+
const filterInitOptions: FilterOptions[] = [
|
|
75
|
+
{ name: FilterName.focusEditable, value: true },
|
|
76
|
+
{ name: FilterName.focusCommonlyUsed, value: true },
|
|
77
|
+
{ name: FilterName.query, value: '' },
|
|
78
|
+
{ name: FilterName.changeSummaryFilterQuery, value: '' },
|
|
79
|
+
{ name: FilterName.showEditableProperties, value: true }
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export const changeProperty = createAction<PropertyChange>('app/change-property');
|
|
83
|
+
export const changePreviewScale = createAction<number>('app/change-preview-scale');
|
|
84
|
+
export const changePreviewScaleMode = createAction<'fit' | 'fixed'>('app/change-preview-scale-mode');
|
|
85
|
+
export const changeDeviceType = createAction<DeviceType>('app/change-device-type');
|
|
86
|
+
export const filterNodes = createAction<FilterOptions[]>('app/filter-nodes');
|
|
87
|
+
|
|
88
|
+
export const initialState = {
|
|
89
|
+
deviceType: DeviceType.Desktop,
|
|
90
|
+
scale: 1.0,
|
|
91
|
+
selectedControl: undefined,
|
|
92
|
+
outline: [],
|
|
93
|
+
filterQuery: filterInitOptions,
|
|
94
|
+
icons: [],
|
|
95
|
+
changes: {
|
|
96
|
+
controls: {},
|
|
97
|
+
pending: [],
|
|
98
|
+
saved: []
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const slice = createSlice<SliceState, SliceCaseReducers<SliceState>, string>({
|
|
102
|
+
name: 'app',
|
|
103
|
+
initialState,
|
|
104
|
+
reducers: {},
|
|
105
|
+
extraReducers: (builder) =>
|
|
106
|
+
builder
|
|
107
|
+
.addMatcher(outlineChanged.match, (state, action: ReturnType<typeof outlineChanged>): void => {
|
|
108
|
+
state.outline = action.payload;
|
|
109
|
+
})
|
|
110
|
+
.addMatcher(controlSelected.match, (state, action: ReturnType<typeof controlSelected>): void => {
|
|
111
|
+
state.selectedControl = action.payload;
|
|
112
|
+
})
|
|
113
|
+
.addMatcher(changeDeviceType.match, (state, action: ReturnType<typeof changeDeviceType>): void => {
|
|
114
|
+
state.deviceType = action.payload;
|
|
115
|
+
})
|
|
116
|
+
.addMatcher(changePreviewScale.match, (state, action: ReturnType<typeof changePreviewScale>): void => {
|
|
117
|
+
state.scale = action.payload;
|
|
118
|
+
})
|
|
119
|
+
.addMatcher(
|
|
120
|
+
changePreviewScaleMode.match,
|
|
121
|
+
(state, action: ReturnType<typeof changePreviewScaleMode>): void => {
|
|
122
|
+
state.fitPreview = action.payload === 'fit';
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
.addMatcher(iconsLoaded.match, (state, action: ReturnType<typeof iconsLoaded>): void => {
|
|
126
|
+
state.icons = action.payload;
|
|
127
|
+
})
|
|
128
|
+
.addMatcher(changeProperty.match, (state, action: ReturnType<typeof changeProperty>): void => {
|
|
129
|
+
if (state.selectedControl?.id === action.payload.controlId) {
|
|
130
|
+
const index = state.selectedControl?.properties.findIndex(
|
|
131
|
+
(property) => property.name === action.payload.propertyName
|
|
132
|
+
);
|
|
133
|
+
if (index !== -1) {
|
|
134
|
+
state.selectedControl.properties[index].value = action.payload.value;
|
|
135
|
+
state.selectedControl.properties[index].errorMessage = '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.addMatcher(propertyChanged.match, (state, action: ReturnType<typeof propertyChanged>): void => {
|
|
140
|
+
if (state.selectedControl?.id === action.payload.controlId) {
|
|
141
|
+
const index = state.selectedControl?.properties.findIndex(
|
|
142
|
+
(property) => property.name === action.payload.propertyName
|
|
143
|
+
);
|
|
144
|
+
if (index !== -1) {
|
|
145
|
+
state.selectedControl.properties[index].value = action.payload.newValue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
.addMatcher(filterNodes.match, (state, action: ReturnType<typeof filterNodes>): void => {
|
|
150
|
+
action.payload.forEach((item) => {
|
|
151
|
+
const stateItem = state.filterQuery.find((filterItem) => filterItem.name === item.name);
|
|
152
|
+
if (stateItem) {
|
|
153
|
+
stateItem.value = item.value;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
})
|
|
157
|
+
.addMatcher(propertyChangeFailed.match, (state, action: ReturnType<typeof propertyChangeFailed>): void => {
|
|
158
|
+
if (state.selectedControl?.id === action.payload.controlId) {
|
|
159
|
+
const index = state.selectedControl?.properties.findIndex(
|
|
160
|
+
(property) => property.name === action.payload.propertyName
|
|
161
|
+
);
|
|
162
|
+
if (index !== -1) {
|
|
163
|
+
state.selectedControl.properties[index].errorMessage = action.payload.errorMessage;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.addMatcher(changeStackModified.match, (state, action: ReturnType<typeof changeStackModified>): void => {
|
|
168
|
+
state.changes.saved = action.payload.saved;
|
|
169
|
+
state.changes.pending = action.payload.pending;
|
|
170
|
+
state.changes.controls = {};
|
|
171
|
+
for (const change of [...action.payload.pending, ...action.payload.saved].reverse()) {
|
|
172
|
+
const { controlId, propertyName, type, controlName } = change;
|
|
173
|
+
const key = `${controlId}`;
|
|
174
|
+
const control = state.changes.controls[key]
|
|
175
|
+
? {
|
|
176
|
+
pending: state.changes.controls[key].pending,
|
|
177
|
+
saved: state.changes.controls[key].saved,
|
|
178
|
+
controlName: state.changes.controls[key].controlName,
|
|
179
|
+
properties: state.changes.controls[key].properties
|
|
180
|
+
}
|
|
181
|
+
: {
|
|
182
|
+
pending: 0,
|
|
183
|
+
saved: 0,
|
|
184
|
+
controlName: controlName ?? '',
|
|
185
|
+
properties: {}
|
|
186
|
+
};
|
|
187
|
+
if (type === 'pending') {
|
|
188
|
+
control.pending++;
|
|
189
|
+
} else if (type === 'saved') {
|
|
190
|
+
control.saved++;
|
|
191
|
+
}
|
|
192
|
+
const property = control.properties[propertyName]
|
|
193
|
+
? {
|
|
194
|
+
pending: control.properties[propertyName].pending,
|
|
195
|
+
saved: control.properties[propertyName].saved,
|
|
196
|
+
lastSavedChange: control.properties[propertyName].lastSavedChange,
|
|
197
|
+
lastChange: control.properties[propertyName].lastChange
|
|
198
|
+
}
|
|
199
|
+
: {
|
|
200
|
+
pending: 0,
|
|
201
|
+
saved: 0
|
|
202
|
+
};
|
|
203
|
+
if (change.type === 'pending') {
|
|
204
|
+
property.pending++;
|
|
205
|
+
property.lastChange = change;
|
|
206
|
+
} else if (change.type === 'saved') {
|
|
207
|
+
property.lastSavedChange = change;
|
|
208
|
+
property.saved++;
|
|
209
|
+
}
|
|
210
|
+
control.properties[propertyName] = property;
|
|
211
|
+
state.changes.controls[key] = control;
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export default slice.reducer;
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { configureStore, compose, applyMiddleware } from '@reduxjs/toolkit';
|
|
2
|
+
import { useDispatch } from 'react-redux';
|
|
3
|
+
import { communicationMiddleware } from './middleware';
|
|
4
|
+
import reducer from './slice';
|
|
5
|
+
|
|
6
|
+
declare let window: Window & { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: <R>(data: R) => R };
|
|
7
|
+
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?? compose;
|
|
8
|
+
|
|
9
|
+
export const store = configureStore({
|
|
10
|
+
reducer,
|
|
11
|
+
devTools: false,
|
|
12
|
+
middleware: [],
|
|
13
|
+
enhancers: [composeEnhancers(applyMiddleware(communicationMiddleware))]
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type AppDispatch = typeof store.dispatch;
|
|
17
|
+
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
|
|
18
|
+
|
|
19
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get full control key.
|
|
6
|
+
*
|
|
7
|
+
* @param key string
|
|
8
|
+
* @returns string - key value
|
|
9
|
+
*/
|
|
10
|
+
function fullKey(key: string): string {
|
|
11
|
+
return `com.sap.ux.control-property-editor.${key}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Use local storage.
|
|
15
|
+
*
|
|
16
|
+
* @param key string
|
|
17
|
+
* @param defaultValue T
|
|
18
|
+
* @returns [value, setValue] [T, React.Dispatch<T>]
|
|
19
|
+
*/
|
|
20
|
+
export function useLocalStorage<T>(key: string, defaultValue: T): [T, React.Dispatch<T>] {
|
|
21
|
+
const [value, setValue] = useState(() => {
|
|
22
|
+
const savedValue = localStorage.getItem(fullKey(key));
|
|
23
|
+
|
|
24
|
+
if (!savedValue) {
|
|
25
|
+
return defaultValue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(savedValue);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return defaultValue;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
localStorage.setItem(fullKey(key), JSON.stringify(value));
|
|
37
|
+
}, [key, value]);
|
|
38
|
+
|
|
39
|
+
return [value, setValue];
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { debounce } from '@sap-ux-private/control-property-editor-common';
|
|
4
|
+
|
|
5
|
+
export interface WindowSize {
|
|
6
|
+
width: number | undefined;
|
|
7
|
+
height: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gets state of window size.
|
|
12
|
+
*
|
|
13
|
+
* @returns WindowSize
|
|
14
|
+
*/
|
|
15
|
+
export function useWindowSize(): WindowSize {
|
|
16
|
+
const [windowSize, setWindowSize] = useState<WindowSize>({
|
|
17
|
+
width: undefined,
|
|
18
|
+
height: undefined
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const handleResize = debounce(() => {
|
|
23
|
+
setWindowSize({
|
|
24
|
+
width: window.innerWidth,
|
|
25
|
+
height: window.innerHeight
|
|
26
|
+
});
|
|
27
|
+
}, 500);
|
|
28
|
+
|
|
29
|
+
window.addEventListener('resize', handleResize);
|
|
30
|
+
|
|
31
|
+
setWindowSize({
|
|
32
|
+
width: window.innerWidth,
|
|
33
|
+
height: window.innerHeight
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (): void => window.removeEventListener('resize', handleResize);
|
|
37
|
+
}, []);
|
|
38
|
+
return windowSize;
|
|
39
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, screen } from '@testing-library/react';
|
|
3
|
+
import { initIcons } from '@sap-ux/ui-components';
|
|
4
|
+
|
|
5
|
+
import { render, mockDomEventListener } from './utils';
|
|
6
|
+
import { initI18n } from '../../src/i18n';
|
|
7
|
+
|
|
8
|
+
import App from '../../src/App';
|
|
9
|
+
import { controlSelected } from '@sap-ux-private/control-property-editor-common';
|
|
10
|
+
import { mockResizeObserver } from '../utils/utils';
|
|
11
|
+
import { InputType } from '../../src/panels/properties/types';
|
|
12
|
+
import { registerAppIcons } from '../../src/icons';
|
|
13
|
+
import { DeviceType } from '../../src/devices';
|
|
14
|
+
import { changePreviewScale, initialState } from '../../src/slice';
|
|
15
|
+
|
|
16
|
+
jest.useFakeTimers({ advanceTimers: true });
|
|
17
|
+
const windowEventListenerMock = mockDomEventListener(window);
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
mockResizeObserver();
|
|
20
|
+
initI18n();
|
|
21
|
+
registerAppIcons();
|
|
22
|
+
initIcons();
|
|
23
|
+
// JSDom does not implement this and an error was being
|
|
24
|
+
// thrown from jest-axe because of it.
|
|
25
|
+
(window as any).getComputedStyle = jest.fn();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('renders empty properties panel', () => {
|
|
29
|
+
render(<App previewUrl="" />);
|
|
30
|
+
const noControlSelected = screen.getByText(/No control selected/i);
|
|
31
|
+
expect(noControlSelected).toBeInTheDocument();
|
|
32
|
+
|
|
33
|
+
const selectControlText = screen.getByText(/Select a control on the canvas to see and modify its properties/i);
|
|
34
|
+
expect(selectControlText).toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
const controlSelectIcon = screen.getByTestId('Control-Property-Editor-No-Control-Selected');
|
|
37
|
+
expect(controlSelectIcon).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('renders properties', () => {
|
|
41
|
+
const { store } = render(<App previewUrl="" />);
|
|
42
|
+
const propNameString = 'activeIcon';
|
|
43
|
+
const propNameDropDown = 'ariaHasPopup';
|
|
44
|
+
const propNameCheckbox = 'visible';
|
|
45
|
+
const propNameCheckboxExpression = 'random';
|
|
46
|
+
const propNameDropDownExpression = 'sync';
|
|
47
|
+
store.dispatch(
|
|
48
|
+
controlSelected({
|
|
49
|
+
id: 'v2flex::sap.suite.ui.generic.template.ListReport.view.ListReport::SEPMRA_C_PD_Product--addEntry',
|
|
50
|
+
type: 'sap.m.Button',
|
|
51
|
+
properties: [
|
|
52
|
+
{
|
|
53
|
+
type: 'string',
|
|
54
|
+
editor: 'input',
|
|
55
|
+
name: propNameString,
|
|
56
|
+
value: '',
|
|
57
|
+
isEnabled: true,
|
|
58
|
+
readableName: ''
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'string',
|
|
62
|
+
editor: 'dropdown',
|
|
63
|
+
name: propNameDropDown,
|
|
64
|
+
value: 'None',
|
|
65
|
+
isEnabled: true,
|
|
66
|
+
readableName: '',
|
|
67
|
+
options: [
|
|
68
|
+
{
|
|
69
|
+
key: 'None',
|
|
70
|
+
text: 'None'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'Menu',
|
|
74
|
+
text: 'Menu'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'ListBox',
|
|
78
|
+
text: 'ListBox'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'Tree',
|
|
82
|
+
text: 'Tree'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'Grid',
|
|
86
|
+
text: 'Grid'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'Dialog',
|
|
90
|
+
text: 'Dialog'
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
editor: 'checkbox',
|
|
97
|
+
name: propNameCheckbox,
|
|
98
|
+
value: true,
|
|
99
|
+
isEnabled: true,
|
|
100
|
+
readableName: 'test check'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'string',
|
|
104
|
+
editor: 'dropdown',
|
|
105
|
+
options: [],
|
|
106
|
+
name: propNameDropDownExpression,
|
|
107
|
+
value: '{ dropDownDynamicExpression }',
|
|
108
|
+
isEnabled: true,
|
|
109
|
+
readableName: ''
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'boolean',
|
|
113
|
+
editor: 'checkbox',
|
|
114
|
+
name: propNameCheckboxExpression,
|
|
115
|
+
isEnabled: true,
|
|
116
|
+
value: '{ checkBoxDynamicExpression }',
|
|
117
|
+
readableName: 'test check dynamic expression'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'string',
|
|
121
|
+
editor: 'unknown',
|
|
122
|
+
name: 'unknownprop',
|
|
123
|
+
isEnabled: true,
|
|
124
|
+
value: 'checkBoxDynamicExpression',
|
|
125
|
+
readableName: 'test check dynamic expression'
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
const visiblePropertyLabel = screen.getByTestId(`${propNameCheckbox}--Label`);
|
|
131
|
+
expect(visiblePropertyLabel).toBeInTheDocument();
|
|
132
|
+
|
|
133
|
+
const buttonTrue = screen.getByTestId(`${propNameCheckbox}--InputTypeToggle--${InputType.booleanTrue}`);
|
|
134
|
+
expect(buttonTrue.getAttribute('aria-pressed')).toBe('true');
|
|
135
|
+
expect(buttonTrue).toBeInTheDocument();
|
|
136
|
+
|
|
137
|
+
const buttonFalse = screen.getByTestId(`${propNameCheckbox}--InputTypeToggle--${InputType.booleanFalse}`);
|
|
138
|
+
expect(buttonFalse.getAttribute('aria-pressed')).toBe('false');
|
|
139
|
+
expect(buttonFalse).toBeInTheDocument();
|
|
140
|
+
|
|
141
|
+
const buttonExpression = screen.getByTestId(`${propNameCheckbox}--InputTypeToggle--${InputType.expression}`);
|
|
142
|
+
expect(buttonExpression.getAttribute('aria-pressed')).toBe('false');
|
|
143
|
+
expect(buttonExpression).toBeInTheDocument();
|
|
144
|
+
|
|
145
|
+
const dropdownButtonExp2 = screen.getByTestId(
|
|
146
|
+
`${propNameDropDownExpression}--InputTypeToggle--${InputType.expression}`
|
|
147
|
+
);
|
|
148
|
+
expect(dropdownButtonExp2.getAttribute('aria-pressed')).toBe('true');
|
|
149
|
+
expect(dropdownButtonExp2).toBeInTheDocument();
|
|
150
|
+
|
|
151
|
+
const checkButtonExp2 = screen.getByTestId(
|
|
152
|
+
`${propNameCheckboxExpression}--InputTypeToggle--${InputType.expression}`
|
|
153
|
+
);
|
|
154
|
+
expect(checkButtonExp2.getAttribute('aria-pressed')).toBe('true');
|
|
155
|
+
expect(checkButtonExp2).toBeInTheDocument();
|
|
156
|
+
|
|
157
|
+
let notFoundException = null;
|
|
158
|
+
try {
|
|
159
|
+
screen.getByTestId(`${propNameCheckbox}--StringEditor`);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
notFoundException = e;
|
|
162
|
+
}
|
|
163
|
+
expect(notFoundException).toBeTruthy();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('renders warning dialog', async () => {
|
|
167
|
+
render(<App previewUrl="" />);
|
|
168
|
+
const dialogContent = screen.getByText(
|
|
169
|
+
/The Control Property Editor enables you to change control properties and behavior directly. These changes may not have the desired effect with Fiori elements applications. Please consult documentation to learn which changes are supported./i
|
|
170
|
+
);
|
|
171
|
+
expect(dialogContent).toBeInTheDocument();
|
|
172
|
+
const okButton = screen.getByText(/ok/i);
|
|
173
|
+
expect(okButton).toBeInTheDocument();
|
|
174
|
+
fireEvent.click(okButton);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const testCases = [
|
|
178
|
+
{
|
|
179
|
+
deviceType: DeviceType.Desktop,
|
|
180
|
+
expectedScale: 460 / 1200
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
deviceType: DeviceType.Tablet,
|
|
184
|
+
expectedScale: 460 / 720
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const testCase of testCases) {
|
|
189
|
+
test(`Test resize - fitPreview=true, device=${testCase.deviceType}`, async () => {
|
|
190
|
+
const stateTemp = JSON.parse(JSON.stringify(initialState));
|
|
191
|
+
stateTemp.fitPreview = true;
|
|
192
|
+
stateTemp.deviceType = testCase.deviceType;
|
|
193
|
+
|
|
194
|
+
const { dispatch } = render(<App previewUrl="" />, {
|
|
195
|
+
initialState: stateTemp
|
|
196
|
+
});
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
198
|
+
global.window.innerWidth = 111;
|
|
199
|
+
global.window.innerHeight = 2222;
|
|
200
|
+
dispatch.mockReset();
|
|
201
|
+
jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 500);
|
|
202
|
+
windowEventListenerMock.simulateEvent('resize', {});
|
|
203
|
+
// Debounce timeout within resize + within use effect
|
|
204
|
+
jest.advanceTimersByTime(3000);
|
|
205
|
+
expect(dispatch).toBeCalledWith(changePreviewScale(testCase.expectedScale));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { start } from '../../src/index';
|
|
2
|
+
import * as i18n from '../../src/i18n';
|
|
3
|
+
import * as icons from '../../src/icons';
|
|
4
|
+
import * as initIcon from '@sap-ux/ui-components/dist/components/Icons'; // bug in TS 4.6 https://github.com/microsoft/TypeScript/issues/43081
|
|
5
|
+
import ReactDOM from 'react-dom';
|
|
6
|
+
|
|
7
|
+
describe('index', () => {
|
|
8
|
+
const i18nSpy = jest.spyOn(i18n, 'initI18n');
|
|
9
|
+
const iconsSpy = jest.spyOn(icons, 'registerAppIcons');
|
|
10
|
+
// ts-ignore
|
|
11
|
+
const initIconSpy = jest.spyOn(initIcon, 'initIcons');
|
|
12
|
+
const reactSpy = jest.spyOn(ReactDOM, 'render').mockReturnValue();
|
|
13
|
+
|
|
14
|
+
test('start', () => {
|
|
15
|
+
const previewUrl = 'URL';
|
|
16
|
+
const rootElementId = 'root';
|
|
17
|
+
start({ previewUrl, rootElementId });
|
|
18
|
+
expect(i18nSpy).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(iconsSpy).toHaveBeenCalledTimes(1);
|
|
20
|
+
expect(initIconSpy).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(reactSpy).toHaveBeenCalledTimes(1);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { render } from '../utils';
|
|
4
|
+
import { initI18n } from '../../../src/i18n';
|
|
5
|
+
|
|
6
|
+
import { ChangeIndicator } from '../../../src/components/ChangeIndicator';
|
|
7
|
+
import { mockResizeObserver } from '../../utils/utils';
|
|
8
|
+
import { initIcons } from '@sap-ux/ui-components';
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
mockResizeObserver();
|
|
12
|
+
initI18n();
|
|
13
|
+
initIcons();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('ChangeIndicator', () => {
|
|
17
|
+
test('saved changes', () => {
|
|
18
|
+
const { container } = render(<ChangeIndicator id={'change-indicator'} saved={1} pending={0} />);
|
|
19
|
+
expect(container.querySelector('svg')).toMatchInlineSnapshot(`
|
|
20
|
+
<svg
|
|
21
|
+
fill="none"
|
|
22
|
+
height="8"
|
|
23
|
+
id="change-indicator"
|
|
24
|
+
role="img"
|
|
25
|
+
viewBox="0 0 8 8"
|
|
26
|
+
width="8"
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
>
|
|
29
|
+
<title>
|
|
30
|
+
Modified & Saved
|
|
31
|
+
</title>
|
|
32
|
+
<circle
|
|
33
|
+
cx="4"
|
|
34
|
+
cy="4"
|
|
35
|
+
fill="var(--vscode-terminal-ansiGreen)"
|
|
36
|
+
r="4"
|
|
37
|
+
/>
|
|
38
|
+
</svg>
|
|
39
|
+
`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('pending changes', () => {
|
|
43
|
+
const { container } = render(<ChangeIndicator saved={0} pending={2} />);
|
|
44
|
+
expect(container.querySelector('svg')).toMatchInlineSnapshot(`
|
|
45
|
+
<svg
|
|
46
|
+
fill="none"
|
|
47
|
+
height="8"
|
|
48
|
+
role="img"
|
|
49
|
+
viewBox="0 0 8 8"
|
|
50
|
+
width="8"
|
|
51
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
52
|
+
>
|
|
53
|
+
<title>
|
|
54
|
+
Modified & Unsaved
|
|
55
|
+
</title>
|
|
56
|
+
<circle
|
|
57
|
+
cx="4"
|
|
58
|
+
cy="4"
|
|
59
|
+
r="3.5"
|
|
60
|
+
stroke="var(--vscode-terminal-ansiGreen)"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('pending and saved changes', () => {
|
|
67
|
+
const { container } = render(<ChangeIndicator saved={3} pending={2} />);
|
|
68
|
+
expect(container.querySelector('svg')).toMatchInlineSnapshot(`
|
|
69
|
+
<svg
|
|
70
|
+
fill="none"
|
|
71
|
+
height="8"
|
|
72
|
+
role="img"
|
|
73
|
+
viewBox="0 0 8 8"
|
|
74
|
+
width="8"
|
|
75
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
76
|
+
>
|
|
77
|
+
<title>
|
|
78
|
+
Saved & Unsaved
|
|
79
|
+
</title>
|
|
80
|
+
<circle
|
|
81
|
+
cx="4"
|
|
82
|
+
cy="4"
|
|
83
|
+
r="3.5"
|
|
84
|
+
stroke="var(--vscode-terminal-ansiGreen)"
|
|
85
|
+
/>
|
|
86
|
+
<path
|
|
87
|
+
d="M4 8a4 4 0 1 0 0-8v8Z"
|
|
88
|
+
fill="var(--vscode-terminal-ansiGreen)"
|
|
89
|
+
/>
|
|
90
|
+
</svg>
|
|
91
|
+
`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('do not add unknown properties', () => {
|
|
95
|
+
const { container } = render(
|
|
96
|
+
<ChangeIndicator id={'change-indicator'} saved={1} pending={0} {...{ xyz: 'abc ' }} />
|
|
97
|
+
);
|
|
98
|
+
expect(container.querySelector('svg')).toMatchInlineSnapshot(`
|
|
99
|
+
<svg
|
|
100
|
+
fill="none"
|
|
101
|
+
height="8"
|
|
102
|
+
id="change-indicator"
|
|
103
|
+
role="img"
|
|
104
|
+
viewBox="0 0 8 8"
|
|
105
|
+
width="8"
|
|
106
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
107
|
+
>
|
|
108
|
+
<title>
|
|
109
|
+
Modified & Saved
|
|
110
|
+
</title>
|
|
111
|
+
<circle
|
|
112
|
+
cx="4"
|
|
113
|
+
cy="4"
|
|
114
|
+
fill="var(--vscode-terminal-ansiGreen)"
|
|
115
|
+
r="4"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
118
|
+
`);
|
|
119
|
+
});
|
|
120
|
+
});
|