@sap-ux/control-property-editor 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc.js +16 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +16 -0
- package/dist/app.css +2 -0
- package/dist/app.css.map +7 -0
- package/dist/app.js +347 -0
- package/dist/app.js.map +7 -0
- package/esbuild.js +25 -0
- package/jest.config.js +20 -0
- package/package.json +68 -0
- package/src/App.scss +57 -0
- package/src/App.tsx +136 -0
- package/src/Workarounds.scss +79 -0
- package/src/actions.ts +3 -0
- package/src/components/AppLogo.module.scss +8 -0
- package/src/components/AppLogo.tsx +75 -0
- package/src/components/ChangeIndicator.tsx +80 -0
- package/src/components/Separator.tsx +32 -0
- package/src/components/ThemeSelectorCallout.scss +48 -0
- package/src/components/ThemeSelectorCallout.tsx +125 -0
- package/src/components/ToolBar.scss +39 -0
- package/src/components/ToolBar.tsx +26 -0
- package/src/components/index.ts +4 -0
- package/src/devices.ts +18 -0
- package/src/global.d.ts +4 -0
- package/src/i18n/i18n.json +68 -0
- package/src/i18n.ts +25 -0
- package/src/icons.tsx +198 -0
- package/src/index.css +1288 -0
- package/src/index.tsx +47 -0
- package/src/middleware.ts +54 -0
- package/src/panels/LeftPanel.scss +17 -0
- package/src/panels/LeftPanel.tsx +48 -0
- package/src/panels/changes/ChangeStack.module.scss +3 -0
- package/src/panels/changes/ChangeStack.tsx +219 -0
- package/src/panels/changes/ChangeStackHeader.tsx +43 -0
- package/src/panels/changes/ChangesPanel.module.scss +18 -0
- package/src/panels/changes/ChangesPanel.tsx +90 -0
- package/src/panels/changes/ControlGroup.module.scss +17 -0
- package/src/panels/changes/ControlGroup.tsx +61 -0
- package/src/panels/changes/PropertyChange.module.scss +24 -0
- package/src/panels/changes/PropertyChange.tsx +159 -0
- package/src/panels/changes/UnknownChange.module.scss +46 -0
- package/src/panels/changes/UnknownChange.tsx +96 -0
- package/src/panels/changes/index.tsx +3 -0
- package/src/panels/changes/utils.ts +36 -0
- package/src/panels/index.ts +2 -0
- package/src/panels/outline/Funnel.tsx +64 -0
- package/src/panels/outline/NoControlFound.tsx +45 -0
- package/src/panels/outline/OutlinePanel.scss +98 -0
- package/src/panels/outline/OutlinePanel.tsx +38 -0
- package/src/panels/outline/Tree.tsx +393 -0
- package/src/panels/outline/index.ts +1 -0
- package/src/panels/outline/utils.ts +154 -0
- package/src/panels/properties/Clipboard.tsx +44 -0
- package/src/panels/properties/DeviceSelector.tsx +40 -0
- package/src/panels/properties/DeviceToggle.tsx +39 -0
- package/src/panels/properties/DropdownEditor.tsx +80 -0
- package/src/panels/properties/Funnel.tsx +64 -0
- package/src/panels/properties/HeaderField.tsx +150 -0
- package/src/panels/properties/IconValueHelp.tsx +203 -0
- package/src/panels/properties/InputTypeSelector.tsx +20 -0
- package/src/panels/properties/InputTypeToggle.module.scss +4 -0
- package/src/panels/properties/InputTypeToggle.tsx +79 -0
- package/src/panels/properties/InputTypeWrapper.tsx +259 -0
- package/src/panels/properties/NoControlSelected.tsx +38 -0
- package/src/panels/properties/Properties.scss +102 -0
- package/src/panels/properties/PropertiesList.tsx +162 -0
- package/src/panels/properties/PropertiesPanel.tsx +30 -0
- package/src/panels/properties/PropertyDocumentation.module.scss +81 -0
- package/src/panels/properties/PropertyDocumentation.tsx +174 -0
- package/src/panels/properties/SapUiIcon.scss +109 -0
- package/src/panels/properties/StringEditor.tsx +122 -0
- package/src/panels/properties/ViewChanger.module.scss +5 -0
- package/src/panels/properties/ViewChanger.tsx +143 -0
- package/src/panels/properties/constants.ts +2 -0
- package/src/panels/properties/index.tsx +1 -0
- package/src/panels/properties/propertyValuesCache.ts +39 -0
- package/src/panels/properties/types.ts +49 -0
- package/src/slice.ts +216 -0
- package/src/store.ts +19 -0
- package/src/use-local-storage.ts +40 -0
- package/src/use-window-size.ts +39 -0
- package/src/variables.scss +2 -0
- package/test/unit/App.test.tsx +207 -0
- package/test/unit/appIndex.test.ts +23 -0
- package/test/unit/components/ChangeIndicator.test.tsx +120 -0
- package/test/unit/components/ThemeSelector.test.tsx +41 -0
- package/test/unit/middleware.test.ts +116 -0
- package/test/unit/panels/changes/ChangesPanel.test.tsx +261 -0
- package/test/unit/panels/changes/utils.test.ts +40 -0
- package/test/unit/panels/outline/OutlinePanel.test.tsx +353 -0
- package/test/unit/panels/outline/__snapshots__/utils.test.ts.snap +36 -0
- package/test/unit/panels/outline/utils.test.ts +83 -0
- package/test/unit/panels/properties/Clipboard.test.tsx +18 -0
- package/test/unit/panels/properties/DropdownEditor.test.tsx +62 -0
- package/test/unit/panels/properties/Funnel.test.tsx +34 -0
- package/test/unit/panels/properties/HeaderField.test.tsx +36 -0
- package/test/unit/panels/properties/IconValueHelp.test.tsx +60 -0
- package/test/unit/panels/properties/InputTypeToggle.test.tsx +126 -0
- package/test/unit/panels/properties/InputTypeWrapper.test.tsx +430 -0
- package/test/unit/panels/properties/PropertyDocumentation.test.tsx +131 -0
- package/test/unit/panels/properties/StringEditor.test.tsx +107 -0
- package/test/unit/panels/properties/ViewChanger.test.tsx +190 -0
- package/test/unit/panels/properties/propertyValuesCache.test.ts +23 -0
- package/test/unit/slice.test.ts +268 -0
- package/test/unit/utils.tsx +67 -0
- package/test/utils/utils.tsx +25 -0
- package/tsconfig.eslint.json +4 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useDispatch } from 'react-redux';
|
|
4
|
+
import type { TFunction } from 'i18next';
|
|
5
|
+
import { Label, Stack } from '@fluentui/react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
UIFocusZone,
|
|
9
|
+
UITooltip,
|
|
10
|
+
UITooltipUtils,
|
|
11
|
+
UIDirectionalHint,
|
|
12
|
+
getCalloutStyle,
|
|
13
|
+
UIDialog
|
|
14
|
+
} from '@sap-ux/ui-components';
|
|
15
|
+
|
|
16
|
+
import type { ControlProperty, PropertyChangeDeletionDetails } from '@sap-ux-private/control-property-editor-common';
|
|
17
|
+
import {
|
|
18
|
+
CHECKBOX_EDITOR_TYPE,
|
|
19
|
+
deletePropertyChanges,
|
|
20
|
+
DROPDOWN_EDITOR_TYPE,
|
|
21
|
+
INPUT_EDITOR_TYPE,
|
|
22
|
+
INTEGER_VALUE_TYPE,
|
|
23
|
+
STRING_VALUE_TYPE
|
|
24
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
25
|
+
|
|
26
|
+
import { IconName } from '../../icons';
|
|
27
|
+
import { ChangeIndicator } from '../../components/ChangeIndicator';
|
|
28
|
+
|
|
29
|
+
import type { CacheValue } from './propertyValuesCache';
|
|
30
|
+
import type { InputTypeToggleOptionProps, InputTypeWrapperProps } from './types';
|
|
31
|
+
import { InputType, isExpression } from './types';
|
|
32
|
+
import { PropertyDocumentation } from './PropertyDocumentation';
|
|
33
|
+
import { defaultFontSize } from './constants';
|
|
34
|
+
import { InputTypeSelector } from './InputTypeSelector';
|
|
35
|
+
import { useTranslation } from 'react-i18next';
|
|
36
|
+
|
|
37
|
+
export const getDefaultInputType = (editor: string, type: string, value: CacheValue): InputType => {
|
|
38
|
+
let defaultInputType: InputType = InputType.expression;
|
|
39
|
+
switch (editor) {
|
|
40
|
+
case DROPDOWN_EDITOR_TYPE:
|
|
41
|
+
defaultInputType = InputType.enumMember;
|
|
42
|
+
break;
|
|
43
|
+
case INPUT_EDITOR_TYPE:
|
|
44
|
+
defaultInputType = type === STRING_VALUE_TYPE ? InputType.string : InputType.number;
|
|
45
|
+
break;
|
|
46
|
+
case CHECKBOX_EDITOR_TYPE:
|
|
47
|
+
if (typeof value === 'boolean') {
|
|
48
|
+
defaultInputType = value ? InputType.booleanTrue : InputType.booleanFalse;
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
}
|
|
53
|
+
return defaultInputType;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets translated tooltip, if translation function exists.
|
|
58
|
+
*
|
|
59
|
+
* @param value string
|
|
60
|
+
* @param t TFunction
|
|
61
|
+
* @returns string
|
|
62
|
+
*/
|
|
63
|
+
function getToolTip(value: string, t?: TFunction): string {
|
|
64
|
+
if (t) {
|
|
65
|
+
return t(value);
|
|
66
|
+
} else {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const getInputTypeToggleOptions = (property: ControlProperty, t?: TFunction): InputTypeToggleOptionProps[] => {
|
|
72
|
+
const { value, editor, type } = property;
|
|
73
|
+
const inputTypeToggleOptions: InputTypeToggleOptionProps[] = [];
|
|
74
|
+
switch (editor) {
|
|
75
|
+
case CHECKBOX_EDITOR_TYPE:
|
|
76
|
+
inputTypeToggleOptions.push({
|
|
77
|
+
inputType: InputType.booleanTrue,
|
|
78
|
+
tooltip: getToolTip('BOOLEAN_TYPE_TRUE', t),
|
|
79
|
+
iconName: IconName.boolTrue,
|
|
80
|
+
selected: typeof value === 'boolean' && value === true
|
|
81
|
+
});
|
|
82
|
+
inputTypeToggleOptions.push({
|
|
83
|
+
inputType: InputType.booleanFalse,
|
|
84
|
+
tooltip: getToolTip('BOOLEAN_TYPE_FALSE', t),
|
|
85
|
+
iconName: IconName.boolFalse,
|
|
86
|
+
selected: typeof value === 'boolean' && value === false
|
|
87
|
+
});
|
|
88
|
+
break;
|
|
89
|
+
case DROPDOWN_EDITOR_TYPE:
|
|
90
|
+
inputTypeToggleOptions.push({
|
|
91
|
+
inputType: InputType.enumMember,
|
|
92
|
+
tooltip: getToolTip('ENUM_TYPE', t),
|
|
93
|
+
iconName: IconName.dropdown,
|
|
94
|
+
selected: !!property.options.find((option) => option.key === value)
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
case INPUT_EDITOR_TYPE:
|
|
98
|
+
if (type === STRING_VALUE_TYPE) {
|
|
99
|
+
inputTypeToggleOptions.push({
|
|
100
|
+
inputType: InputType.string,
|
|
101
|
+
tooltip: getToolTip('STRING_TYPE', t),
|
|
102
|
+
iconName: IconName.string,
|
|
103
|
+
selected: typeof value !== 'string' || !isExpression(value)
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
const textKey = type === INTEGER_VALUE_TYPE ? 'INTEGER_TYPE' : 'FLOAT_TYPE';
|
|
107
|
+
inputTypeToggleOptions.push({
|
|
108
|
+
inputType: InputType.number,
|
|
109
|
+
tooltip: getToolTip(textKey, t),
|
|
110
|
+
iconName: IconName.number,
|
|
111
|
+
selected: typeof value !== 'string' || !isExpression(value)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
inputTypeToggleOptions.push({
|
|
119
|
+
inputType: InputType.expression,
|
|
120
|
+
tooltip: getToolTip('EXPRESSION_TYPE', t),
|
|
121
|
+
iconName: IconName.expression,
|
|
122
|
+
selected: typeof value === 'string' && isExpression(value)
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return inputTypeToggleOptions;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* React element for input type wrapper.
|
|
130
|
+
*
|
|
131
|
+
* @param props InputTypeWrapperProps
|
|
132
|
+
* @returns ReactElement
|
|
133
|
+
*/
|
|
134
|
+
export function InputTypeWrapper(props: InputTypeWrapperProps): ReactElement {
|
|
135
|
+
const { property, changes } = props;
|
|
136
|
+
const { name, isEnabled, documentation } = property;
|
|
137
|
+
|
|
138
|
+
const { t } = useTranslation();
|
|
139
|
+
const dispatch = useDispatch();
|
|
140
|
+
|
|
141
|
+
const [dialogState, setDialogState] = useState<PropertyChangeDeletionDetails | undefined>(undefined);
|
|
142
|
+
const documentationContent = documentation && (
|
|
143
|
+
<PropertyDocumentation
|
|
144
|
+
title={property.readableName}
|
|
145
|
+
defaultValue={documentation?.defaultValue}
|
|
146
|
+
description={documentation?.description}
|
|
147
|
+
propertyName={documentation?.propertyName}
|
|
148
|
+
propertyType={documentation?.propertyType}
|
|
149
|
+
onDelete={showDeleteConfirmation}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
*
|
|
155
|
+
* @param controlId string
|
|
156
|
+
* @param propertyName string
|
|
157
|
+
*/
|
|
158
|
+
function showDeleteConfirmation(controlId: string, propertyName: string): void {
|
|
159
|
+
setDialogState({
|
|
160
|
+
controlId,
|
|
161
|
+
propertyName
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onConfirmDelete(): void {
|
|
166
|
+
if (dialogState) {
|
|
167
|
+
dispatch(deletePropertyChanges(dialogState));
|
|
168
|
+
setDialogState(undefined);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onCancelDelete(): void {
|
|
173
|
+
setDialogState(undefined);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const indicator = changes ? (
|
|
177
|
+
<Stack.Item>
|
|
178
|
+
<ChangeIndicator id={`${name}--ChangeIndicator`} {...changes} />
|
|
179
|
+
</Stack.Item>
|
|
180
|
+
) : (
|
|
181
|
+
<></>
|
|
182
|
+
);
|
|
183
|
+
return (
|
|
184
|
+
<UIFocusZone
|
|
185
|
+
isCircularNavigation={true}
|
|
186
|
+
data-testid={`${name}--InputTypeWrapper`}
|
|
187
|
+
style={{
|
|
188
|
+
marginBottom: 10,
|
|
189
|
+
marginTop: 10
|
|
190
|
+
}}>
|
|
191
|
+
<Stack horizontal horizontalAlign="space-between">
|
|
192
|
+
<UITooltip
|
|
193
|
+
calloutProps={{
|
|
194
|
+
gapSpace: 5,
|
|
195
|
+
styles: getCalloutStyle({
|
|
196
|
+
styles: {
|
|
197
|
+
root: {
|
|
198
|
+
padding: 'none'
|
|
199
|
+
},
|
|
200
|
+
calloutMain: {
|
|
201
|
+
padding: 'none'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
}}
|
|
206
|
+
delay={2}
|
|
207
|
+
maxWidth={400}
|
|
208
|
+
directionalHint={UIDirectionalHint.leftCenter}
|
|
209
|
+
id={`${name}--PropertyTooltip`}
|
|
210
|
+
tooltipProps={UITooltipUtils.renderContent(documentationContent ?? '')}>
|
|
211
|
+
<Stack
|
|
212
|
+
horizontal
|
|
213
|
+
horizontalAlign="space-between"
|
|
214
|
+
tokens={{
|
|
215
|
+
childrenGap: '5px'
|
|
216
|
+
}}>
|
|
217
|
+
{indicator}
|
|
218
|
+
<Stack.Item>
|
|
219
|
+
<Label
|
|
220
|
+
data-aria-label={name}
|
|
221
|
+
data-testid={`${name}--Label`}
|
|
222
|
+
disabled={!isEnabled}
|
|
223
|
+
style={{
|
|
224
|
+
color: 'var(--vscode-foreground)',
|
|
225
|
+
opacity: isEnabled ? 1 : 0.4,
|
|
226
|
+
fontSize: defaultFontSize,
|
|
227
|
+
fontWeight: 'bold',
|
|
228
|
+
padding: 0,
|
|
229
|
+
width: '190px',
|
|
230
|
+
textOverflow: 'ellipsis',
|
|
231
|
+
whiteSpace: 'nowrap',
|
|
232
|
+
overflowX: 'hidden',
|
|
233
|
+
marginTop: 2
|
|
234
|
+
}}>
|
|
235
|
+
{property.readableName}
|
|
236
|
+
</Label>
|
|
237
|
+
</Stack.Item>
|
|
238
|
+
</Stack>
|
|
239
|
+
</UITooltip>
|
|
240
|
+
<Stack horizontal horizontalAlign="end">
|
|
241
|
+
<InputTypeSelector {...props} />
|
|
242
|
+
</Stack>
|
|
243
|
+
</Stack>
|
|
244
|
+
{props.children}
|
|
245
|
+
{dialogState && (
|
|
246
|
+
<UIDialog
|
|
247
|
+
hidden={dialogState === undefined}
|
|
248
|
+
onAccept={onConfirmDelete}
|
|
249
|
+
acceptButtonText={t('CONFIRM_DELETE')}
|
|
250
|
+
cancelButtonText={t('CANCEL_DELETE')}
|
|
251
|
+
onCancel={onCancelDelete}
|
|
252
|
+
dialogContentProps={{
|
|
253
|
+
title: t('CONFIRM_DELETE_TITLE'),
|
|
254
|
+
subText: t('CONFIRM_DELETE_SUBTEXT')
|
|
255
|
+
}}></UIDialog>
|
|
256
|
+
)}
|
|
257
|
+
</UIFocusZone>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from '@fluentui/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
const NoControlSelected = () => {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Text className="properties-no-control-selected-text">{t('NO_CONTROL_SELECTED')}</Text>
|
|
10
|
+
<Text className="properties-modify-text">{t('PROPERTIES_MODIFY_TEXT')}</Text>
|
|
11
|
+
<div className="properties-control-select-icon">
|
|
12
|
+
{
|
|
13
|
+
<svg
|
|
14
|
+
data-testid="Control-Property-Editor-No-Control-Selected"
|
|
15
|
+
width="80"
|
|
16
|
+
height="37"
|
|
17
|
+
viewBox="0 0 80 37"
|
|
18
|
+
fill="none"
|
|
19
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
20
|
+
<path
|
|
21
|
+
fillRule="evenodd"
|
|
22
|
+
clipRule="evenodd"
|
|
23
|
+
d="M0 2.75V0H3.16666V2H2V2.75H0ZM15.8333 0H9.5V2H15.8333V0ZM22.1667 0V2H28.5V0H22.1667ZM34.8333 0V2H41.1667V0H34.8333ZM47.5 0V2H53.8333V0H47.5ZM60.1667 0V2H66.5V0H60.1667ZM72.8333 0V2H74V2.75H76V0H72.8333ZM76 8.25H74V13.75H76V8.25ZM76 19.25H74V24.75H76V19.25ZM76 30.25H74V31H72.8333V33H76V30.25ZM66.5 33V31H60.1667V33H66.5ZM53.8333 33V31H47.5V33H53.8333ZM41.1667 33V31H34.8333V33H41.1667ZM28.5 33V31H22.1667V33H28.5ZM15.8333 33V31H9.5V33H15.8333ZM3.16667 33V31H2V30.25H0V33H3.16667ZM0 24.75H2V19.25H0V24.75ZM0 13.75H2V8.25H0V13.75Z"
|
|
24
|
+
fill="var(--vscode-focusBorder)"
|
|
25
|
+
/>
|
|
26
|
+
<path d="M66 12H69V20H66V12Z" fill="var(--vscode-icon-foreground)" />
|
|
27
|
+
<path d="M72 23H80V26H72V23Z" fill="var(--vscode-icon-foreground)" />
|
|
28
|
+
<path d="M63 23H55V26H63V23Z" fill="var(--vscode-icon-foreground)" />
|
|
29
|
+
<path d="M66 29H69V37H66V29Z" fill="var(--vscode-icon-foreground)" />
|
|
30
|
+
<path d="M69 23H66V26H69V23Z" fill="var(--vscode-icon-foreground)" />
|
|
31
|
+
</svg>
|
|
32
|
+
}
|
|
33
|
+
</div>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { NoControlSelected };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
@import './../../variables.scss';
|
|
2
|
+
|
|
3
|
+
.property-content {
|
|
4
|
+
padding: 20px 15px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.space {
|
|
8
|
+
margin-bottom: 10px;
|
|
9
|
+
&:last-child {
|
|
10
|
+
margin-bottom: 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.space-toggle {
|
|
15
|
+
label {
|
|
16
|
+
font-size: $defaultFontSize;
|
|
17
|
+
font-weight: bold;
|
|
18
|
+
text-overflow: ellipsis;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
}
|
|
22
|
+
margin-bottom: 12px;
|
|
23
|
+
}
|
|
24
|
+
.space-toggle-disabled {
|
|
25
|
+
label {
|
|
26
|
+
opacity: 0.55;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.dropdownEditor {
|
|
31
|
+
label {
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
padding-bottom: 2px;
|
|
34
|
+
height: auto;
|
|
35
|
+
}
|
|
36
|
+
color: var(--vscode-foreground);
|
|
37
|
+
font-size: $defaultFontSize;
|
|
38
|
+
margin-bottom: 12px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.stringEditor {
|
|
42
|
+
font-weight: bold;
|
|
43
|
+
font-size: $defaultFontSize;
|
|
44
|
+
div {
|
|
45
|
+
label {
|
|
46
|
+
font-weight: bold !important;
|
|
47
|
+
margin-bottom: 0px !important;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
margin-bottom: 12px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.stringEditorDisabled {
|
|
54
|
+
div {
|
|
55
|
+
div {
|
|
56
|
+
opacity: 0.4;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.properties-no-control-selected-text {
|
|
62
|
+
font-style: normal;
|
|
63
|
+
font-weight: bold;
|
|
64
|
+
font-size: 24px;
|
|
65
|
+
line-height: 29px;
|
|
66
|
+
text-align: center;
|
|
67
|
+
padding-top: 30px;
|
|
68
|
+
color: var(--vscode-foreground);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.properties-modify-text {
|
|
72
|
+
font-style: normal;
|
|
73
|
+
font-weight: normal;
|
|
74
|
+
font-size: 13px;
|
|
75
|
+
line-height: 18px;
|
|
76
|
+
text-align: center;
|
|
77
|
+
margin: 10%;
|
|
78
|
+
color: var(--vscode-foreground);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.properties-control-select-icon {
|
|
82
|
+
text-align: center;
|
|
83
|
+
}
|
|
84
|
+
.filter-properties {
|
|
85
|
+
display: flex;
|
|
86
|
+
margin: 0px 15px 10px 15px;
|
|
87
|
+
flex-direction: row;
|
|
88
|
+
align-items: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.funnel-properties-icon {
|
|
92
|
+
margin-left: 16px;
|
|
93
|
+
i {
|
|
94
|
+
line-height: 20px;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
label[for='editable-properties-checkbox'] {
|
|
99
|
+
.ms-Checkbox-text {
|
|
100
|
+
font-size: 13px !important;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { Control } from '@sap-ux-private/control-property-editor-common';
|
|
5
|
+
import {
|
|
6
|
+
CHECKBOX_EDITOR_TYPE,
|
|
7
|
+
DROPDOWN_EDITOR_TYPE,
|
|
8
|
+
INPUT_EDITOR_TYPE
|
|
9
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
10
|
+
import { Separator } from '../../components/Separator';
|
|
11
|
+
import type { RootState } from '../../store';
|
|
12
|
+
import { DropdownEditor } from './DropdownEditor';
|
|
13
|
+
import { HeaderField } from './HeaderField';
|
|
14
|
+
import { getDefaultInputType, getInputTypeToggleOptions, InputTypeWrapper } from './InputTypeWrapper';
|
|
15
|
+
import { setCachedValue } from './propertyValuesCache';
|
|
16
|
+
import { StringEditor } from './StringEditor';
|
|
17
|
+
import type { InputTypeWrapperProps } from './types';
|
|
18
|
+
import { isExpression } from './types';
|
|
19
|
+
import { sectionHeaderFontSize } from './constants';
|
|
20
|
+
import { useSelector } from 'react-redux';
|
|
21
|
+
import { UISearchBox } from '@sap-ux/ui-components';
|
|
22
|
+
import { NoControlSelected } from './NoControlSelected';
|
|
23
|
+
import { Label } from '@fluentui/react';
|
|
24
|
+
import './Properties.scss';
|
|
25
|
+
import { Funnel } from './Funnel';
|
|
26
|
+
import type { ControlChangeStats, FilterOptions } from '../../slice';
|
|
27
|
+
import { FilterName } from '../../slice';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* React element for all properties including id & type and property editors.
|
|
31
|
+
*
|
|
32
|
+
* @returns ReactElement
|
|
33
|
+
*/
|
|
34
|
+
export function PropertiesList(): ReactElement {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const control = useSelector<RootState, Control | undefined>((state) => state.selectedControl);
|
|
37
|
+
const controlChanges = useSelector<RootState, ControlChangeStats | undefined>(
|
|
38
|
+
(state) => state.changes.controls[control?.id ?? '']
|
|
39
|
+
);
|
|
40
|
+
const isEditableOnly = useSelector<RootState, FilterOptions[]>((state) => state.filterQuery).filter(
|
|
41
|
+
(item) => item.name === FilterName.showEditableProperties
|
|
42
|
+
)[0].value as boolean;
|
|
43
|
+
const [filterValue, setFilterValue] = useState('');
|
|
44
|
+
const doc = {
|
|
45
|
+
defaultValue: '-',
|
|
46
|
+
description: 'The unique identifier within a page, either configured or automatically generated.',
|
|
47
|
+
propertyName: 'id',
|
|
48
|
+
type: 'sap.ui.core.ID',
|
|
49
|
+
propertyType: 'ID'
|
|
50
|
+
};
|
|
51
|
+
if (!control) {
|
|
52
|
+
// Nothing selected, show message
|
|
53
|
+
return <NoControlSelected />;
|
|
54
|
+
}
|
|
55
|
+
const { id, type, properties, name } = control;
|
|
56
|
+
const onFilterChange = (
|
|
57
|
+
event?: React.ChangeEvent<HTMLInputElement> | undefined,
|
|
58
|
+
filterValue?: string | undefined
|
|
59
|
+
): void => {
|
|
60
|
+
setFilterValue(filterValue?.toLowerCase() ?? '');
|
|
61
|
+
};
|
|
62
|
+
const editors = (
|
|
63
|
+
<>
|
|
64
|
+
{properties
|
|
65
|
+
.filter(
|
|
66
|
+
(property) =>
|
|
67
|
+
!filterValue ||
|
|
68
|
+
property.name.toLowerCase().includes(filterValue) ||
|
|
69
|
+
property.readableName.toLowerCase().includes(filterValue)
|
|
70
|
+
)
|
|
71
|
+
.filter((property) => (isEditableOnly && property.isEnabled) || !isEditableOnly)
|
|
72
|
+
.map((property) => {
|
|
73
|
+
const props: InputTypeWrapperProps = {
|
|
74
|
+
controlId: id,
|
|
75
|
+
controlName: name,
|
|
76
|
+
property,
|
|
77
|
+
toggleOptions: getInputTypeToggleOptions(property, t),
|
|
78
|
+
changes: controlChanges?.properties[property.name]
|
|
79
|
+
};
|
|
80
|
+
const defaultInputType = getDefaultInputType(property.editor, property.type, property.value);
|
|
81
|
+
setCachedValue(id, property.name, defaultInputType, property.value);
|
|
82
|
+
let result: ReactElement | undefined;
|
|
83
|
+
switch (property.editor) {
|
|
84
|
+
case CHECKBOX_EDITOR_TYPE:
|
|
85
|
+
if (typeof property.value !== 'boolean') {
|
|
86
|
+
result = <StringEditor {...props} />;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case DROPDOWN_EDITOR_TYPE:
|
|
90
|
+
if (isExpression(property.value)) {
|
|
91
|
+
result = <StringEditor {...props} />;
|
|
92
|
+
} else {
|
|
93
|
+
result = (
|
|
94
|
+
<DropdownEditor
|
|
95
|
+
{...{
|
|
96
|
+
key: property.name,
|
|
97
|
+
controlId: id,
|
|
98
|
+
property,
|
|
99
|
+
controlName: name
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case INPUT_EDITOR_TYPE:
|
|
106
|
+
result = <StringEditor {...props} />;
|
|
107
|
+
break;
|
|
108
|
+
default: {
|
|
109
|
+
console.warn(
|
|
110
|
+
`No property editor for '${JSON.stringify(property)}' found. Fallback to string editor`
|
|
111
|
+
);
|
|
112
|
+
result = <StringEditor {...props} />;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return (
|
|
117
|
+
<InputTypeWrapper key={property.name} {...props}>
|
|
118
|
+
{result}
|
|
119
|
+
</InputTypeWrapper>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<div className="property-content">
|
|
127
|
+
<HeaderField label={t('CONTROL_ID_LABEL')} value={id} documentation={doc} hidden={false} />
|
|
128
|
+
<HeaderField label={t('CONTROL_TYPE_LABEL')} value={type} />
|
|
129
|
+
</div>
|
|
130
|
+
<Separator
|
|
131
|
+
style={{
|
|
132
|
+
marginTop: '0px',
|
|
133
|
+
marginBottom: '20px'
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
<div className="filter-properties">
|
|
137
|
+
<UISearchBox
|
|
138
|
+
id="SearchProperties"
|
|
139
|
+
autoFocus={false}
|
|
140
|
+
disableAnimation={false}
|
|
141
|
+
placeholder={t('SEARCH_PROPERTIES')}
|
|
142
|
+
onChange={onFilterChange}
|
|
143
|
+
/>
|
|
144
|
+
<Funnel />
|
|
145
|
+
</div>
|
|
146
|
+
<div className={`property-content app-panel-scroller`}>
|
|
147
|
+
<Label
|
|
148
|
+
data-aria-label={t('PROPERTIES')}
|
|
149
|
+
style={{
|
|
150
|
+
color: 'var(--vscode-foreground)',
|
|
151
|
+
fontSize: sectionHeaderFontSize,
|
|
152
|
+
fontWeight: 'bold',
|
|
153
|
+
padding: 0,
|
|
154
|
+
marginBottom: '10px'
|
|
155
|
+
}}>
|
|
156
|
+
{t('PROPERTIES')}
|
|
157
|
+
</Label>
|
|
158
|
+
{editors}
|
|
159
|
+
</div>
|
|
160
|
+
</>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { PropertiesList } from './PropertiesList';
|
|
5
|
+
import { Separator, ThemeSelectorCallout, Toolbar } from '../../components';
|
|
6
|
+
import { ViewChanger } from './ViewChanger';
|
|
7
|
+
import { DeviceSelector } from './DeviceSelector';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main function to render the properties panel.
|
|
11
|
+
*
|
|
12
|
+
* @returns Properties panel as ReactElement
|
|
13
|
+
*/
|
|
14
|
+
export function PropertiesPanel(): ReactElement {
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<Toolbar
|
|
18
|
+
left={
|
|
19
|
+
<>
|
|
20
|
+
<ThemeSelectorCallout />
|
|
21
|
+
<Separator direction="vertical" style={{ marginLeft: '10px', marginRight: '10px' }} />
|
|
22
|
+
<ViewChanger />
|
|
23
|
+
</>
|
|
24
|
+
}
|
|
25
|
+
right={<DeviceSelector />}
|
|
26
|
+
/>
|
|
27
|
+
<PropertiesList />
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
padding: 10px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
height: 46px;
|
|
7
|
+
padding: 13px 10px 13px 10px;
|
|
8
|
+
border-bottom: 1px solid var(--vscode-contrastBorder, var(--vscode-editorIndentGuide-background));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.savedChanges {
|
|
12
|
+
color: var(--vscode-terminal-ansiGreen);
|
|
13
|
+
font-size: 11px;
|
|
14
|
+
line-height: 15px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.unsavedChanges {
|
|
18
|
+
color: var(--vscode-foreground);
|
|
19
|
+
font-size: 11px;
|
|
20
|
+
line-height: 15px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.title {
|
|
24
|
+
color: var(--vscode-foreground);
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
font-weight: bold;
|
|
27
|
+
line-height: 16px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.savedChangeRow {
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.bold,
|
|
35
|
+
.propertyName {
|
|
36
|
+
color: var(--vscode-foreground);
|
|
37
|
+
font-weight: bold;
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
line-height: 18px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.propertyName {
|
|
43
|
+
grid-column-start: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.value {
|
|
47
|
+
color: var(--vscode-foreground);
|
|
48
|
+
font-size: 13px;
|
|
49
|
+
line-height: 18px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.infoIcon {
|
|
53
|
+
padding: 3px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.description {
|
|
57
|
+
color: var(--vscode-foreground);
|
|
58
|
+
font-size: 11px;
|
|
59
|
+
line-height: 15px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.grid {
|
|
63
|
+
display: grid;
|
|
64
|
+
grid-template-columns: 125px 1fr 22px;
|
|
65
|
+
grid-template-rows: repeat(auto-fill, 22px);
|
|
66
|
+
min-height: 82px;
|
|
67
|
+
row-gap: 8px;
|
|
68
|
+
column-gap: 10px;
|
|
69
|
+
justify-items: start;
|
|
70
|
+
}
|
|
71
|
+
.grid span {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
max-width: 100%;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.propertyWithNoActions {
|
|
79
|
+
grid-column-start: 2;
|
|
80
|
+
grid-column-end: span 2;
|
|
81
|
+
}
|