@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.
Files changed (112) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.js +16 -0
  3. package/CHANGELOG.md +7 -0
  4. package/LICENSE +201 -0
  5. package/README.md +16 -0
  6. package/dist/app.css +2 -0
  7. package/dist/app.css.map +7 -0
  8. package/dist/app.js +347 -0
  9. package/dist/app.js.map +7 -0
  10. package/esbuild.js +25 -0
  11. package/jest.config.js +20 -0
  12. package/package.json +68 -0
  13. package/src/App.scss +57 -0
  14. package/src/App.tsx +136 -0
  15. package/src/Workarounds.scss +79 -0
  16. package/src/actions.ts +3 -0
  17. package/src/components/AppLogo.module.scss +8 -0
  18. package/src/components/AppLogo.tsx +75 -0
  19. package/src/components/ChangeIndicator.tsx +80 -0
  20. package/src/components/Separator.tsx +32 -0
  21. package/src/components/ThemeSelectorCallout.scss +48 -0
  22. package/src/components/ThemeSelectorCallout.tsx +125 -0
  23. package/src/components/ToolBar.scss +39 -0
  24. package/src/components/ToolBar.tsx +26 -0
  25. package/src/components/index.ts +4 -0
  26. package/src/devices.ts +18 -0
  27. package/src/global.d.ts +4 -0
  28. package/src/i18n/i18n.json +68 -0
  29. package/src/i18n.ts +25 -0
  30. package/src/icons.tsx +198 -0
  31. package/src/index.css +1288 -0
  32. package/src/index.tsx +47 -0
  33. package/src/middleware.ts +54 -0
  34. package/src/panels/LeftPanel.scss +17 -0
  35. package/src/panels/LeftPanel.tsx +48 -0
  36. package/src/panels/changes/ChangeStack.module.scss +3 -0
  37. package/src/panels/changes/ChangeStack.tsx +219 -0
  38. package/src/panels/changes/ChangeStackHeader.tsx +43 -0
  39. package/src/panels/changes/ChangesPanel.module.scss +18 -0
  40. package/src/panels/changes/ChangesPanel.tsx +90 -0
  41. package/src/panels/changes/ControlGroup.module.scss +17 -0
  42. package/src/panels/changes/ControlGroup.tsx +61 -0
  43. package/src/panels/changes/PropertyChange.module.scss +24 -0
  44. package/src/panels/changes/PropertyChange.tsx +159 -0
  45. package/src/panels/changes/UnknownChange.module.scss +46 -0
  46. package/src/panels/changes/UnknownChange.tsx +96 -0
  47. package/src/panels/changes/index.tsx +3 -0
  48. package/src/panels/changes/utils.ts +36 -0
  49. package/src/panels/index.ts +2 -0
  50. package/src/panels/outline/Funnel.tsx +64 -0
  51. package/src/panels/outline/NoControlFound.tsx +45 -0
  52. package/src/panels/outline/OutlinePanel.scss +98 -0
  53. package/src/panels/outline/OutlinePanel.tsx +38 -0
  54. package/src/panels/outline/Tree.tsx +393 -0
  55. package/src/panels/outline/index.ts +1 -0
  56. package/src/panels/outline/utils.ts +154 -0
  57. package/src/panels/properties/Clipboard.tsx +44 -0
  58. package/src/panels/properties/DeviceSelector.tsx +40 -0
  59. package/src/panels/properties/DeviceToggle.tsx +39 -0
  60. package/src/panels/properties/DropdownEditor.tsx +80 -0
  61. package/src/panels/properties/Funnel.tsx +64 -0
  62. package/src/panels/properties/HeaderField.tsx +150 -0
  63. package/src/panels/properties/IconValueHelp.tsx +203 -0
  64. package/src/panels/properties/InputTypeSelector.tsx +20 -0
  65. package/src/panels/properties/InputTypeToggle.module.scss +4 -0
  66. package/src/panels/properties/InputTypeToggle.tsx +79 -0
  67. package/src/panels/properties/InputTypeWrapper.tsx +259 -0
  68. package/src/panels/properties/NoControlSelected.tsx +38 -0
  69. package/src/panels/properties/Properties.scss +102 -0
  70. package/src/panels/properties/PropertiesList.tsx +162 -0
  71. package/src/panels/properties/PropertiesPanel.tsx +30 -0
  72. package/src/panels/properties/PropertyDocumentation.module.scss +81 -0
  73. package/src/panels/properties/PropertyDocumentation.tsx +174 -0
  74. package/src/panels/properties/SapUiIcon.scss +109 -0
  75. package/src/panels/properties/StringEditor.tsx +122 -0
  76. package/src/panels/properties/ViewChanger.module.scss +5 -0
  77. package/src/panels/properties/ViewChanger.tsx +143 -0
  78. package/src/panels/properties/constants.ts +2 -0
  79. package/src/panels/properties/index.tsx +1 -0
  80. package/src/panels/properties/propertyValuesCache.ts +39 -0
  81. package/src/panels/properties/types.ts +49 -0
  82. package/src/slice.ts +216 -0
  83. package/src/store.ts +19 -0
  84. package/src/use-local-storage.ts +40 -0
  85. package/src/use-window-size.ts +39 -0
  86. package/src/variables.scss +2 -0
  87. package/test/unit/App.test.tsx +207 -0
  88. package/test/unit/appIndex.test.ts +23 -0
  89. package/test/unit/components/ChangeIndicator.test.tsx +120 -0
  90. package/test/unit/components/ThemeSelector.test.tsx +41 -0
  91. package/test/unit/middleware.test.ts +116 -0
  92. package/test/unit/panels/changes/ChangesPanel.test.tsx +261 -0
  93. package/test/unit/panels/changes/utils.test.ts +40 -0
  94. package/test/unit/panels/outline/OutlinePanel.test.tsx +353 -0
  95. package/test/unit/panels/outline/__snapshots__/utils.test.ts.snap +36 -0
  96. package/test/unit/panels/outline/utils.test.ts +83 -0
  97. package/test/unit/panels/properties/Clipboard.test.tsx +18 -0
  98. package/test/unit/panels/properties/DropdownEditor.test.tsx +62 -0
  99. package/test/unit/panels/properties/Funnel.test.tsx +34 -0
  100. package/test/unit/panels/properties/HeaderField.test.tsx +36 -0
  101. package/test/unit/panels/properties/IconValueHelp.test.tsx +60 -0
  102. package/test/unit/panels/properties/InputTypeToggle.test.tsx +126 -0
  103. package/test/unit/panels/properties/InputTypeWrapper.test.tsx +430 -0
  104. package/test/unit/panels/properties/PropertyDocumentation.test.tsx +131 -0
  105. package/test/unit/panels/properties/StringEditor.test.tsx +107 -0
  106. package/test/unit/panels/properties/ViewChanger.test.tsx +190 -0
  107. package/test/unit/panels/properties/propertyValuesCache.test.ts +23 -0
  108. package/test/unit/slice.test.ts +268 -0
  109. package/test/unit/utils.tsx +67 -0
  110. package/test/utils/utils.tsx +25 -0
  111. package/tsconfig.eslint.json +4 -0
  112. 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,5 @@
1
+ .zoomInput {
2
+ width: 70px;
3
+ margin-left: 6px;
4
+ margin-right: 6px;
5
+ }
@@ -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,2 @@
1
+ export const defaultFontSize = '13px';
2
+ export const sectionHeaderFontSize = '11px';
@@ -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
+ };