@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,174 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useSelector } from 'react-redux';
|
|
5
|
+
import { Text, Stack } from '@fluentui/react';
|
|
6
|
+
|
|
7
|
+
import { UIIcon, UIIconButton, UiIcons } from '@sap-ux/ui-components';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
Control,
|
|
11
|
+
SavedPropertyChange,
|
|
12
|
+
PendingPropertyChange
|
|
13
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
14
|
+
import { Separator } from '../../components';
|
|
15
|
+
import type { RootState } from '../../store';
|
|
16
|
+
|
|
17
|
+
import styles from './PropertyDocumentation.module.scss';
|
|
18
|
+
|
|
19
|
+
export interface PropertyDocumentationProps {
|
|
20
|
+
defaultValue: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
description: string;
|
|
23
|
+
propertyName: string;
|
|
24
|
+
propertyType: string | undefined;
|
|
25
|
+
onDelete?(controlId: string, propertyName: string): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* React element PropertyDocumentation.
|
|
29
|
+
*
|
|
30
|
+
* @param propDocProps PropertyDocumentationProps
|
|
31
|
+
* @returns ReactElement
|
|
32
|
+
*/
|
|
33
|
+
export function PropertyDocumentation(propDocProps: PropertyDocumentationProps): ReactElement {
|
|
34
|
+
const { propertyName, title, defaultValue, description, propertyType, onDelete } = propDocProps;
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
|
|
37
|
+
const control = useSelector<RootState, Control | undefined>((state) => state.selectedControl);
|
|
38
|
+
|
|
39
|
+
const propertyChanges = useSelector<
|
|
40
|
+
RootState,
|
|
41
|
+
| {
|
|
42
|
+
pending: number;
|
|
43
|
+
saved: number;
|
|
44
|
+
lastSavedChange?: SavedPropertyChange;
|
|
45
|
+
lastChange?: PendingPropertyChange;
|
|
46
|
+
}
|
|
47
|
+
| undefined
|
|
48
|
+
>((state) => state.changes.controls[control?.id ?? '']?.properties[propertyName]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Stack
|
|
53
|
+
id={`${propertyName}--PropertyTooltip-Header`}
|
|
54
|
+
horizontal
|
|
55
|
+
horizontalAlign="space-between"
|
|
56
|
+
className={styles.header}>
|
|
57
|
+
<Stack.Item>
|
|
58
|
+
<Text className={styles.title}>{title}</Text>
|
|
59
|
+
</Stack.Item>
|
|
60
|
+
{propertyChanges && (
|
|
61
|
+
<Stack.Item>
|
|
62
|
+
<Modified {...propertyChanges} />
|
|
63
|
+
</Stack.Item>
|
|
64
|
+
)}
|
|
65
|
+
</Stack>
|
|
66
|
+
<Stack
|
|
67
|
+
className={styles.container}
|
|
68
|
+
tokens={{
|
|
69
|
+
childrenGap: '10px'
|
|
70
|
+
}}>
|
|
71
|
+
<Stack.Item>
|
|
72
|
+
<section id={`${propertyName}--PropertyTooltip-Content`} className={styles.grid}>
|
|
73
|
+
<DocumentationRow label={t('PROPERTY_NAME')} value={propertyName} />
|
|
74
|
+
<DocumentationRow label={t('PROPERTY_TYPE')} value={propertyType} />
|
|
75
|
+
<>
|
|
76
|
+
<Text className={styles.propertyName}>{t('DEFAULT_VALUE')}</Text>
|
|
77
|
+
<Text title={defaultValue?.toString()} className={styles.bold}>
|
|
78
|
+
{defaultValue?.toString()}
|
|
79
|
+
</Text>
|
|
80
|
+
<UIIcon
|
|
81
|
+
className={styles.infoIcon}
|
|
82
|
+
iconName={UiIcons.Info}
|
|
83
|
+
title={t('DEFAULT_VALUE_TOOLTIP')}
|
|
84
|
+
/>
|
|
85
|
+
</>
|
|
86
|
+
{propertyChanges?.lastChange && (
|
|
87
|
+
<>
|
|
88
|
+
<Text className={styles.propertyName}>{t('CURRENT_VALUE')}</Text>
|
|
89
|
+
<Text
|
|
90
|
+
title={propertyChanges.lastChange.value.toString()}
|
|
91
|
+
className={[styles.bold, styles.propertyWithNoActions].join(' ')}>
|
|
92
|
+
{propertyChanges.lastChange.value.toString()}
|
|
93
|
+
</Text>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
{propertyChanges?.lastSavedChange && (
|
|
97
|
+
<>
|
|
98
|
+
<Text className={styles.propertyName}>{t('SAVED_VALUE')}</Text>
|
|
99
|
+
<Text title={propertyChanges.lastSavedChange.value.toString()} className={styles.bold}>
|
|
100
|
+
{propertyChanges.lastSavedChange.value.toString()}
|
|
101
|
+
</Text>
|
|
102
|
+
<UIIconButton
|
|
103
|
+
iconProps={{ iconName: UiIcons.TrashCan }}
|
|
104
|
+
title={t('DELETE_ALL_PROPERTY_CHANGES_TOOLTIP')}
|
|
105
|
+
onClick={(): void => {
|
|
106
|
+
if (control?.id) {
|
|
107
|
+
onDelete(control.id, propertyName);
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
</section>
|
|
114
|
+
</Stack.Item>
|
|
115
|
+
<Stack.Item>
|
|
116
|
+
<Separator />
|
|
117
|
+
</Stack.Item>
|
|
118
|
+
<Stack.Item>
|
|
119
|
+
<Text id={`${propertyName}--PropertyTooltip-Footer`} className={styles.description}>
|
|
120
|
+
{description}
|
|
121
|
+
</Text>
|
|
122
|
+
</Stack.Item>
|
|
123
|
+
</Stack>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
interface ModifiedProps {
|
|
128
|
+
saved: number;
|
|
129
|
+
pending: number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* React element Modified.
|
|
133
|
+
*
|
|
134
|
+
* @param modifiedProps ModifiedProps
|
|
135
|
+
* @returns ReactElement
|
|
136
|
+
*/
|
|
137
|
+
function Modified(modifiedProps: ModifiedProps): ReactElement {
|
|
138
|
+
const { pending, saved } = modifiedProps;
|
|
139
|
+
const { t } = useTranslation();
|
|
140
|
+
if (saved > 0 && pending === 0) {
|
|
141
|
+
return <Text className={styles.savedChanges}>{t('SAVED_CHANGES')}</Text>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (pending > 0 && saved === 0) {
|
|
145
|
+
return <Text className={styles.unsavedChanges}>{t('UNSAVED_CHANGES')}</Text>;
|
|
146
|
+
}
|
|
147
|
+
return <Text className={styles.unsavedChanges}>{t('SAVED_AND_UNSAVED_CHANGES')}</Text>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface DocumentationRowProps {
|
|
151
|
+
label: string;
|
|
152
|
+
value?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* React element DocumentationRow.
|
|
157
|
+
*
|
|
158
|
+
* @param documentationRowProps DocumentationRowProps
|
|
159
|
+
* @returns ReactElement
|
|
160
|
+
*/
|
|
161
|
+
function DocumentationRow(documentationRowProps: DocumentationRowProps): ReactElement {
|
|
162
|
+
const { label, value } = documentationRowProps;
|
|
163
|
+
if (!value) {
|
|
164
|
+
return <></>;
|
|
165
|
+
}
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<Text className={styles.propertyName}>{label}</Text>
|
|
169
|
+
<Text title={value} className={[styles.value, styles.propertyWithNoActions].join(' ')}>
|
|
170
|
+
{value}
|
|
171
|
+
</Text>
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
.sapUiIcon:before {
|
|
2
|
+
content: attr(data-sap-ui-icon-content);
|
|
3
|
+
speak: none;
|
|
4
|
+
font-weight: normal;
|
|
5
|
+
-webkit-font-smoothing: antialiased;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.icon-dialog {
|
|
9
|
+
.ms-Dialog-main {
|
|
10
|
+
height: 100%;
|
|
11
|
+
max-height: calc(100% - 23rem);
|
|
12
|
+
.ms-Modal-scrollableContent {
|
|
13
|
+
height: 100%;
|
|
14
|
+
> div {
|
|
15
|
+
height: 100%;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
.ms-Dialog-inner {
|
|
19
|
+
height: 100%;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
.ms-Dialog-content {
|
|
23
|
+
height: 100%;
|
|
24
|
+
bottom: 10px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
$medium: 800px;
|
|
33
|
+
$small: 600px;
|
|
34
|
+
$mini: 400px;
|
|
35
|
+
|
|
36
|
+
@media screen and (max-height: $medium) {
|
|
37
|
+
.icon-dialog .ms-Dialog-main {
|
|
38
|
+
max-height: calc(100% - 8rem);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media screen and (max-height: $small) {
|
|
43
|
+
.icon-dialog .ms-Dialog-main {
|
|
44
|
+
max-height: calc(100% - 4rem);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@media screen and (max-height: $mini) {
|
|
49
|
+
.icon-dialog .ms-Dialog-main {
|
|
50
|
+
max-height: calc(100% - 50px);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.icon-table {
|
|
55
|
+
margin-bottom: 46px;
|
|
56
|
+
overflow-x: hidden;
|
|
57
|
+
border-bottom: 1px solid var(--vscode-scm-providerBorder);
|
|
58
|
+
.ms-DetailsHeader-cell .not-editable-icon {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ms-DetailsRow-fields:hover {
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
background-color: var(--vscode-dropdown-background);
|
|
65
|
+
color: var(--vscode-dropdown-foreground);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.is-selected .ms-DetailsRow-fields {
|
|
69
|
+
color: var(--vscode-list-activeSelectionForeground) !important;
|
|
70
|
+
background-color: var(--vscode-list-activeSelectionBackground) !important;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.root-226:focus .ms-DetailsRow-cell {
|
|
74
|
+
color: var(--vscode-background) !important;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ms-ScrollablePane .ms-FocusZone .ms-DetailsHeader-cell {
|
|
78
|
+
box-shadow: inset 0 1px 0 var(--vscode-scm-providerBorder), inset 0 -1px 0 var(--vscode-scm-providerBorder),
|
|
79
|
+
inset -1px 0 0 var(--vscode-scm-providerBorder), inset 1px 0 0 var(--vscode-scm-providerBorder);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ms-ScrollablePane .ms-FocusZone .ms-DetailsRow-fields .ms-DetailsRow-cell:last-child {
|
|
83
|
+
box-shadow: none;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.icon-textField {
|
|
88
|
+
.ms-TextField-wrapper {
|
|
89
|
+
.ms-TextField-fieldGroup {
|
|
90
|
+
.ms-TextField-suffix {
|
|
91
|
+
color: var(--vscode-input-foreground);
|
|
92
|
+
background-color: var(--vscode-input-background);
|
|
93
|
+
padding: 1px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.tablediv {
|
|
100
|
+
.ms-ScrollablePane .ms-FocusZone {
|
|
101
|
+
background: var(--vscode-tab-inactiveBackground);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.filter-icon-div {
|
|
106
|
+
.filter-icons {
|
|
107
|
+
margin-top: 20px;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
4
|
+
|
|
5
|
+
import type { UITextInputProps } from '@sap-ux/ui-components';
|
|
6
|
+
import { UITextInput } from '@sap-ux/ui-components';
|
|
7
|
+
|
|
8
|
+
import { changeProperty } from '../../slice';
|
|
9
|
+
|
|
10
|
+
import type { PropertyInputProps } from './types';
|
|
11
|
+
import { isExpression, InputType } from './types';
|
|
12
|
+
import { setCachedValue } from './propertyValuesCache';
|
|
13
|
+
|
|
14
|
+
import './Properties.scss';
|
|
15
|
+
import {
|
|
16
|
+
reportTelemetry,
|
|
17
|
+
debounce,
|
|
18
|
+
FLOAT_VALUE_TYPE,
|
|
19
|
+
INTEGER_VALUE_TYPE,
|
|
20
|
+
BOOLEAN_VALUE_TYPE
|
|
21
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
22
|
+
import './SapUiIcon.scss';
|
|
23
|
+
import { IconValueHelp } from './IconValueHelp';
|
|
24
|
+
import type { IconDetails } from '@sap-ux-private/control-property-editor-common';
|
|
25
|
+
import type { RootState } from '../../store';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* React element for string editor in property panel.
|
|
29
|
+
*
|
|
30
|
+
* @param propertyInputProps PropertyInputProps
|
|
31
|
+
* @returns ReactElement
|
|
32
|
+
*/
|
|
33
|
+
export function StringEditor(propertyInputProps: PropertyInputProps): ReactElement {
|
|
34
|
+
const {
|
|
35
|
+
property: { name, value, isEnabled, isIcon, type, errorMessage },
|
|
36
|
+
controlId,
|
|
37
|
+
controlName
|
|
38
|
+
} = propertyInputProps;
|
|
39
|
+
const [val, setValue] = useState(value);
|
|
40
|
+
const icons = useSelector<RootState, IconDetails[]>((state) => state.icons);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setValue(value);
|
|
44
|
+
}, [value]);
|
|
45
|
+
|
|
46
|
+
const getValueHelpButton = (): React.ReactElement => {
|
|
47
|
+
return (
|
|
48
|
+
<IconValueHelp
|
|
49
|
+
disabled={!isEnabled}
|
|
50
|
+
icons={icons ?? []}
|
|
51
|
+
isIcon={isIcon}
|
|
52
|
+
value={value as string}
|
|
53
|
+
controlId={controlId}
|
|
54
|
+
propertyName={name}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
const dispatch = useDispatch();
|
|
59
|
+
const dispatchWithDelay = useRef(debounce(dispatch, 500));
|
|
60
|
+
|
|
61
|
+
const inputProps: UITextInputProps = {};
|
|
62
|
+
|
|
63
|
+
inputProps.onBlur = (e) => {
|
|
64
|
+
reportTelemetry({ category: 'Property Change', propertyName: name }).catch((error) => {
|
|
65
|
+
console.error(`Error in reporting telemetry`, error);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (type === FLOAT_VALUE_TYPE && !isExpression(val)) {
|
|
69
|
+
let newValue: string | number = String(e.target.value);
|
|
70
|
+
if (type === FLOAT_VALUE_TYPE && !isExpression(newValue)) {
|
|
71
|
+
newValue = parseFloat(String(newValue?.trim()));
|
|
72
|
+
}
|
|
73
|
+
setCachedValue(controlId, name, InputType.number, newValue);
|
|
74
|
+
const action = changeProperty({ controlId, propertyName: name, value: newValue, controlName });
|
|
75
|
+
dispatch(action);
|
|
76
|
+
setValue(newValue);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (isIcon && !isExpression(val)) {
|
|
81
|
+
inputProps.onRenderSuffix = getValueHelpButton;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<UITextInput
|
|
87
|
+
className={`stringEditor icon-textField`}
|
|
88
|
+
key={name}
|
|
89
|
+
data-testid={`${name}--StringEditor`}
|
|
90
|
+
disabled={!isEnabled}
|
|
91
|
+
errorMessage={errorMessage}
|
|
92
|
+
value={val as string}
|
|
93
|
+
onChange={(
|
|
94
|
+
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
95
|
+
newValue: string | undefined
|
|
96
|
+
): void => {
|
|
97
|
+
let value: string | number = String(newValue ?? '');
|
|
98
|
+
if (type === FLOAT_VALUE_TYPE && !isExpression(value)) {
|
|
99
|
+
const index = value.search(/\./) + 1;
|
|
100
|
+
const result = value.substring(0, index) + value.slice(index).replace(/\./g, '');
|
|
101
|
+
value = result.trim().replace(/(^-)|[^0-9.]+/g, '$1');
|
|
102
|
+
} else {
|
|
103
|
+
if (type === INTEGER_VALUE_TYPE && !isExpression(value)) {
|
|
104
|
+
value = value.trim().replace(/(^-)|(\D+)/g, '$1');
|
|
105
|
+
value = parseInt(String(value), 10);
|
|
106
|
+
}
|
|
107
|
+
const inputType = type === INTEGER_VALUE_TYPE ? InputType.number : InputType.string;
|
|
108
|
+
setCachedValue(controlId, name, inputType, value);
|
|
109
|
+
const action = changeProperty({ controlId, propertyName: name, value: value, controlName });
|
|
110
|
+
// starting from ui5 version 1.106, empty string "" is not accepted as change for boolean type properties
|
|
111
|
+
if (value || type !== BOOLEAN_VALUE_TYPE) {
|
|
112
|
+
// allow empty string "" when we have string type property
|
|
113
|
+
dispatchWithDelay.current(action);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
setValue(value);
|
|
117
|
+
}}
|
|
118
|
+
{...inputProps}
|
|
119
|
+
/>
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
5
|
+
|
|
6
|
+
import type { UIComboBoxOption, UIComboBoxRef } from '@sap-ux/ui-components';
|
|
7
|
+
import { UIComboBox, UIIconButton, UiIcons } from '@sap-ux/ui-components';
|
|
8
|
+
|
|
9
|
+
import type { RootState } from '../../store';
|
|
10
|
+
import { changePreviewScale, changePreviewScaleMode } from '../../slice';
|
|
11
|
+
|
|
12
|
+
import styles from './ViewChanger.module.scss';
|
|
13
|
+
|
|
14
|
+
const ZOOM_STEP = 0.1;
|
|
15
|
+
const SCALE_INPUT_PATTERN = /(\d{1,20})%/;
|
|
16
|
+
const MAX_SCALE = 1;
|
|
17
|
+
const MIN_SCALE = 0.1;
|
|
18
|
+
const FIT_PREVIEW_KEY = 'fit';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* React element to view changer.
|
|
22
|
+
*
|
|
23
|
+
* @returns ReactElement
|
|
24
|
+
*/
|
|
25
|
+
export function ViewChanger(): ReactElement {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const dispatch = useDispatch();
|
|
28
|
+
const scale = useSelector<RootState, number>((state) => state.scale);
|
|
29
|
+
const fitPreview = useSelector<RootState, boolean>((state) => state.fitPreview ?? false);
|
|
30
|
+
const options = [
|
|
31
|
+
{
|
|
32
|
+
key: 0.25,
|
|
33
|
+
text: '25%'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 0.5,
|
|
37
|
+
text: '50%'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 0.75,
|
|
41
|
+
text: '75%'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 1,
|
|
45
|
+
text: '100%'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: FIT_PREVIEW_KEY,
|
|
49
|
+
text: t('FIT_PREVIEW')
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
const key = fitPreview ? FIT_PREVIEW_KEY : scale;
|
|
53
|
+
const selectedOption = options.find((enumValue) => enumValue.key === key);
|
|
54
|
+
const text = !selectedOption && scale ? scaleInPercent(scale) : undefined;
|
|
55
|
+
|
|
56
|
+
function zoomIn(): void {
|
|
57
|
+
const newScale = Math.min(scale + ZOOM_STEP, MAX_SCALE);
|
|
58
|
+
dispatch(changePreviewScale(newScale));
|
|
59
|
+
dispatch(changePreviewScaleMode('fixed'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function zoomOut(): void {
|
|
63
|
+
const newScale = Math.max(scale - ZOOM_STEP, MIN_SCALE);
|
|
64
|
+
dispatch(changePreviewScale(newScale));
|
|
65
|
+
dispatch(changePreviewScaleMode('fixed'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
*
|
|
70
|
+
* @param event React.FormEvent<UIComboBoxRef>
|
|
71
|
+
* @param option UIComboBoxOption
|
|
72
|
+
* @param index number
|
|
73
|
+
* @param value string
|
|
74
|
+
*/
|
|
75
|
+
function onChange(
|
|
76
|
+
event: React.FormEvent<UIComboBoxRef>,
|
|
77
|
+
option?: UIComboBoxOption,
|
|
78
|
+
index?: number,
|
|
79
|
+
value?: string
|
|
80
|
+
): void {
|
|
81
|
+
if (option?.key) {
|
|
82
|
+
if (typeof option?.key === 'number') {
|
|
83
|
+
dispatch(changePreviewScale(option?.key));
|
|
84
|
+
dispatch(changePreviewScaleMode('fixed'));
|
|
85
|
+
} else {
|
|
86
|
+
dispatch(changePreviewScaleMode('fit'));
|
|
87
|
+
}
|
|
88
|
+
} else if (value) {
|
|
89
|
+
const match = SCALE_INPUT_PATTERN.exec(value);
|
|
90
|
+
if (match) {
|
|
91
|
+
const percent = parseInt(match[1], 10);
|
|
92
|
+
const newScale = percent / 100;
|
|
93
|
+
if (newScale >= MIN_SCALE && newScale <= MAX_SCALE) {
|
|
94
|
+
dispatch(changePreviewScale(newScale));
|
|
95
|
+
dispatch(changePreviewScaleMode('fixed'));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<UIIconButton
|
|
104
|
+
title={t('ZOOM_OUT')}
|
|
105
|
+
iconProps={{
|
|
106
|
+
iconName: UiIcons.ZoomOut
|
|
107
|
+
}}
|
|
108
|
+
disabled={scale <= MIN_SCALE}
|
|
109
|
+
onClick={zoomOut}
|
|
110
|
+
/>
|
|
111
|
+
<UIComboBox
|
|
112
|
+
id="view-changer-combobox"
|
|
113
|
+
data-testid={`testId-view-changer-combobox`}
|
|
114
|
+
className={styles.zoomInput}
|
|
115
|
+
autoComplete="off"
|
|
116
|
+
selectedKey={key}
|
|
117
|
+
text={text}
|
|
118
|
+
allowFreeform={true}
|
|
119
|
+
useComboBoxAsMenuWidth={true}
|
|
120
|
+
options={options}
|
|
121
|
+
onChange={onChange}
|
|
122
|
+
/>
|
|
123
|
+
<UIIconButton
|
|
124
|
+
title={t('ZOOM_IN')}
|
|
125
|
+
iconProps={{
|
|
126
|
+
iconName: UiIcons.ZoomIn
|
|
127
|
+
}}
|
|
128
|
+
disabled={scale >= MAX_SCALE}
|
|
129
|
+
onClick={zoomIn}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scale in percent.
|
|
137
|
+
*
|
|
138
|
+
* @param scale number
|
|
139
|
+
* @returns scaled value - string
|
|
140
|
+
*/
|
|
141
|
+
function scaleInPercent(scale: number): string {
|
|
142
|
+
return `${Math.floor(scale * 100)}%`;
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PropertiesPanel } from './PropertiesPanel';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { InputType, isExpression } from './types';
|
|
2
|
+
|
|
3
|
+
export type CacheValue = string | boolean | number;
|
|
4
|
+
|
|
5
|
+
const propertyValueCache: Record<string, Record<string, Record<string, CacheValue>>> = {};
|
|
6
|
+
|
|
7
|
+
export const setCachedValue = (
|
|
8
|
+
controlId: string,
|
|
9
|
+
propertyName: string,
|
|
10
|
+
defaultInputType: string,
|
|
11
|
+
value: CacheValue
|
|
12
|
+
): void => {
|
|
13
|
+
if (!propertyValueCache[controlId]) {
|
|
14
|
+
propertyValueCache[controlId] = {};
|
|
15
|
+
}
|
|
16
|
+
const propertyMap = propertyValueCache[controlId];
|
|
17
|
+
if (propertyMap) {
|
|
18
|
+
if (!propertyMap[propertyName]) {
|
|
19
|
+
propertyMap[propertyName] = {};
|
|
20
|
+
}
|
|
21
|
+
const inputTypeMap = propertyMap[propertyName];
|
|
22
|
+
if (inputTypeMap) {
|
|
23
|
+
const inputType = isExpression(value) ? InputType.expression : defaultInputType;
|
|
24
|
+
inputTypeMap[inputType] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getCachedValue = (controlId: string, propertyId: string, inputType: string): CacheValue | null => {
|
|
30
|
+
const propertyMap = propertyValueCache[controlId];
|
|
31
|
+
if (!propertyMap) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const inputTypeMap = propertyMap[propertyId];
|
|
35
|
+
if (!inputTypeMap) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return inputTypeMap[inputType] || null;
|
|
39
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ControlProperty,
|
|
5
|
+
CHECKBOX_EDITOR_TYPE,
|
|
6
|
+
DROPDOWN_EDITOR_TYPE,
|
|
7
|
+
INPUT_EDITOR_TYPE
|
|
8
|
+
} from '@sap-ux-private/control-property-editor-common';
|
|
9
|
+
|
|
10
|
+
import type { PropertyChangeStats } from '../../slice';
|
|
11
|
+
|
|
12
|
+
export interface PropertyInputProps<T extends ControlProperty = ControlProperty> {
|
|
13
|
+
controlId: string;
|
|
14
|
+
controlName: string;
|
|
15
|
+
property: T;
|
|
16
|
+
changes?: PropertyChangeStats;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const enum InputType {
|
|
20
|
+
booleanTrue = 'booleanTrue',
|
|
21
|
+
booleanFalse = 'booleanFalse',
|
|
22
|
+
enumMember = 'enumMember',
|
|
23
|
+
string = 'string',
|
|
24
|
+
number = 'number',
|
|
25
|
+
expression = 'expression'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InputTypeToggleOptionProps {
|
|
29
|
+
inputType: InputType;
|
|
30
|
+
tooltip: string;
|
|
31
|
+
iconName: string;
|
|
32
|
+
selected?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type InputTypeWrapperProps = PropertyInputProps & {
|
|
36
|
+
toggleOptions: InputTypeToggleOptionProps[];
|
|
37
|
+
children?: ReactElement;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type InputTypeToggleProps = InputTypeWrapperProps & {
|
|
41
|
+
key: string;
|
|
42
|
+
inputTypeProps: InputTypeToggleOptionProps;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type Editor = typeof INPUT_EDITOR_TYPE | typeof DROPDOWN_EDITOR_TYPE | typeof CHECKBOX_EDITOR_TYPE;
|
|
46
|
+
|
|
47
|
+
export const isExpression = (value: string | boolean | number): boolean => {
|
|
48
|
+
return typeof value === 'string' && value.includes('{') && value.includes('}');
|
|
49
|
+
};
|