@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,131 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import { render } from '../../utils';
4
+ import type { PropertyDocumentationProps } from '../../../../src/panels/properties/PropertyDocumentation';
5
+ import { PropertyDocumentation } from '../../../../src/panels/properties/PropertyDocumentation';
6
+ import { initI18n } from '../../../../src/i18n';
7
+
8
+ import { mockResizeObserver } from '../../../utils/utils';
9
+
10
+ describe('PropertyDoc', () => {
11
+ beforeAll(() => {
12
+ mockResizeObserver();
13
+ initI18n();
14
+ });
15
+
16
+ test('no changes', () => {
17
+ const props: PropertyDocumentationProps = {
18
+ description: 'testDoc',
19
+ title: 'Test Property',
20
+ defaultValue: 'defaultValue',
21
+ propertyName: 'testProperty',
22
+ propertyType: 'testType',
23
+ onDelete: jest.fn()
24
+ };
25
+ render(<PropertyDocumentation {...props} />);
26
+
27
+ expect(screen.getByText(/property name/i)).toBeInTheDocument();
28
+ expect(screen.getByText(/testProperty/i)).toBeInTheDocument();
29
+ expect(screen.getByText(/property type/i)).toBeInTheDocument();
30
+ expect(screen.getByText(/testType/i)).toBeInTheDocument();
31
+ expect(screen.getByText(/default value/i)).toBeInTheDocument();
32
+ expect(screen.getByText(/defaultValue/i)).toBeInTheDocument();
33
+ expect(screen.getByText(/testDoc/i)).toBeInTheDocument();
34
+ });
35
+
36
+ test('pending changes', () => {
37
+ const props: PropertyDocumentationProps = {
38
+ description: 'testDoc',
39
+ title: 'Test Property',
40
+ defaultValue: 'defaultValue',
41
+ propertyName: 'testProperty',
42
+ propertyType: 'testType',
43
+ onDelete: jest.fn()
44
+ };
45
+ render(<PropertyDocumentation {...props} />, {
46
+ initialState: {
47
+ selectedControl: {
48
+ id: 'control1'
49
+ } as any,
50
+ changes: {
51
+ stack: [],
52
+ controls: {
53
+ control1: {
54
+ pending: 0,
55
+ saved: 1,
56
+ properties: {
57
+ testProperty: {
58
+ saved: 0,
59
+ pending: 1,
60
+ lastChange: {
61
+ propertyName: 'testProperty',
62
+ value: 'c value',
63
+ type: 'pending',
64
+ isActive: true,
65
+ controlId: 'control1'
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ });
74
+
75
+ expect(screen.getByText(/Modified & Unsaved/i)).toBeInTheDocument();
76
+
77
+ expect(screen.getByText(/current value/i)).toBeInTheDocument();
78
+ expect(screen.getByText(/c value/i)).toBeInTheDocument();
79
+ });
80
+
81
+ test('saved changes', () => {
82
+ const props: PropertyDocumentationProps = {
83
+ description: 'testDoc',
84
+ title: 'Test Property',
85
+ defaultValue: 'defaultValue',
86
+ propertyName: 'testProperty',
87
+ propertyType: 'testType',
88
+ onDelete: jest.fn()
89
+ };
90
+ render(<PropertyDocumentation {...props} />, {
91
+ initialState: {
92
+ selectedControl: {
93
+ id: 'control1'
94
+ } as any,
95
+ changes: {
96
+ stack: [],
97
+ controls: {
98
+ control1: {
99
+ pending: 0,
100
+ saved: 1,
101
+ properties: {
102
+ testProperty: {
103
+ saved: 1,
104
+ pending: 0,
105
+ lastSavedChange: {
106
+ propertyName: 'testProperty',
107
+ value: 'old value',
108
+ type: 'saved',
109
+ fileName: 'file',
110
+ timestamp: 123,
111
+ controlId: 'control1'
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ } as any
119
+ });
120
+
121
+ expect(screen.getByText(/Modified & Saved/i)).toBeInTheDocument();
122
+
123
+ expect(screen.getByText(/Saved value/i)).toBeInTheDocument();
124
+ expect(screen.getByText(/old value/i)).toBeInTheDocument();
125
+ const deleteButton = screen.getByRole('button');
126
+
127
+ deleteButton.click();
128
+
129
+ expect(props.onDelete).toHaveBeenCalledWith('control1', 'testProperty');
130
+ });
131
+ });
@@ -0,0 +1,107 @@
1
+ import { screen, fireEvent } from '@testing-library/react';
2
+ import { render } from '../../utils';
3
+ import React from 'react';
4
+ import { StringEditor } from '../../../../src/panels/properties/StringEditor';
5
+
6
+ describe('StringEditor', () => {
7
+ const controlId = 'testControlId';
8
+
9
+ test('initial load', () => {
10
+ const value = 'testValue12345';
11
+ const props: any = {
12
+ icons: [],
13
+ isIcon: false,
14
+ isEnabled: false,
15
+ name: 'testProperty',
16
+ value
17
+ };
18
+ render(<StringEditor property={...props} controlId={controlId} />);
19
+
20
+ const textBox = screen.getByDisplayValue(value);
21
+ expect(textBox).toBeInTheDocument();
22
+ });
23
+ test('render suffix', () => {
24
+ const value = 'testValue12345';
25
+ const props: any = {
26
+ icons: [
27
+ {
28
+ content: 'testData2',
29
+ fontFamily: 'SAP-fontFamily',
30
+ name: 'testName2'
31
+ },
32
+ {
33
+ content: 'testData3',
34
+ fontFamily: 'SAP-fontFamily',
35
+ name: 'testName3'
36
+ }
37
+ ],
38
+ isEnabled: false,
39
+ isIcon: true,
40
+ name: 'testProperty',
41
+ value
42
+ };
43
+ render(<StringEditor property={...props} controlId={controlId} />);
44
+
45
+ const textBox = screen.getByDisplayValue(value);
46
+ expect(textBox).toBeInTheDocument();
47
+ });
48
+
49
+ test('integer value', () => {
50
+ const value = '12345';
51
+ const props: any = {
52
+ icons: [],
53
+ isEnabled: false,
54
+ isIcon: false,
55
+ name: 'testProperty',
56
+ value,
57
+ type: 'integer'
58
+ };
59
+
60
+ render(<StringEditor property={...props} controlId={controlId} />);
61
+
62
+ const textBox = screen.getByDisplayValue(value);
63
+ expect(textBox).toBeInTheDocument();
64
+ expect((textBox as any).value).toBe(value);
65
+ fireEvent.change(textBox, { target: { value: 'testName1' } });
66
+ expect((textBox as any).value).toBe('1');
67
+ });
68
+ test('float value', () => {
69
+ const value = '0.12345';
70
+ const props: any = {
71
+ icons: [],
72
+ isEnabled: false,
73
+ isIcon: false,
74
+ name: 'testProperty',
75
+ value,
76
+ type: 'float'
77
+ };
78
+
79
+ render(<StringEditor property={...props} controlId={controlId} />);
80
+
81
+ const textBox = screen.getByDisplayValue(value);
82
+ expect(textBox).toBeInTheDocument();
83
+ expect((textBox as any).value).toBe(value);
84
+ fireEvent.change(textBox, { target: { value: '5.3f04f.23' } });
85
+ expect((textBox as any).value).toBe('5.30423');
86
+ });
87
+
88
+ test('float value onBlur', () => {
89
+ const value = '5.300';
90
+ const props: any = {
91
+ icons: [],
92
+ isEnabled: false,
93
+ isIcon: false,
94
+ name: 'testProperty',
95
+ value,
96
+ type: 'float'
97
+ };
98
+
99
+ render(<StringEditor property={...props} controlId={controlId} />);
100
+
101
+ const textBox = screen.getByDisplayValue(value);
102
+ expect(textBox).toBeInTheDocument();
103
+ expect((textBox as any).value).toBe('5.300');
104
+ fireEvent.blur(textBox);
105
+ expect((textBox as any).value).toBe('5.3');
106
+ });
107
+ });
@@ -0,0 +1,190 @@
1
+ import React from 'react';
2
+ import { fireEvent, screen } from '@testing-library/react';
3
+
4
+ import { ViewChanger } from '../../../../src/panels/properties/ViewChanger';
5
+ import { initI18n } from '../../../../src/i18n';
6
+
7
+ import { render } from '../../utils';
8
+
9
+ describe('ViewChanger', () => {
10
+ beforeAll(() => {
11
+ initI18n();
12
+ });
13
+
14
+ test('zoom in', () => {
15
+ const { dispatch } = render(<ViewChanger />, {
16
+ initialState: {
17
+ scale: 0.5,
18
+ fitPreview: false
19
+ }
20
+ });
21
+ screen.getByTitle(/zoom in/i).click();
22
+
23
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
24
+ payload: 0.6,
25
+ type: 'app/change-preview-scale'
26
+ });
27
+ expect(dispatch).toHaveBeenNthCalledWith(2, {
28
+ payload: 'fixed',
29
+ type: 'app/change-preview-scale-mode'
30
+ });
31
+ });
32
+
33
+ test('zoom in with max scale', () => {
34
+ const { dispatch } = render(<ViewChanger />, {
35
+ initialState: {
36
+ scale: 1,
37
+ fitPreview: false
38
+ }
39
+ });
40
+ screen.getByTitle(/zoom in/i).click();
41
+
42
+ expect(dispatch).toHaveBeenCalledTimes(0);
43
+ });
44
+
45
+ test('zoom in to max scale', () => {
46
+ const { dispatch } = render(<ViewChanger />, {
47
+ initialState: {
48
+ scale: 0.95,
49
+ fitPreview: false
50
+ }
51
+ });
52
+ screen.getByTitle(/zoom in/i).click();
53
+
54
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
55
+ payload: 1,
56
+ type: 'app/change-preview-scale'
57
+ });
58
+ expect(dispatch).toHaveBeenNthCalledWith(2, {
59
+ payload: 'fixed',
60
+ type: 'app/change-preview-scale-mode'
61
+ });
62
+ });
63
+
64
+ test('zoom out', () => {
65
+ const { dispatch } = render(<ViewChanger />, {
66
+ initialState: {
67
+ scale: 1,
68
+ fitPreview: false
69
+ }
70
+ });
71
+ screen.getByTitle(/zoom out/i).click();
72
+
73
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
74
+ payload: 0.9,
75
+ type: 'app/change-preview-scale'
76
+ });
77
+ expect(dispatch).toHaveBeenNthCalledWith(2, {
78
+ payload: 'fixed',
79
+ type: 'app/change-preview-scale-mode'
80
+ });
81
+ });
82
+
83
+ test('zoom out with min scale', () => {
84
+ const { dispatch } = render(<ViewChanger />, {
85
+ initialState: {
86
+ scale: 0.1,
87
+ fitPreview: false
88
+ }
89
+ });
90
+ screen.getByTitle(/zoom out/i).click();
91
+
92
+ expect(dispatch).toHaveBeenCalledTimes(0);
93
+ });
94
+
95
+ test('zoom out with min scale', () => {
96
+ const { dispatch } = render(<ViewChanger />, {
97
+ initialState: {
98
+ scale: 0.12,
99
+ fitPreview: false
100
+ }
101
+ });
102
+ screen.getByTitle(/zoom out/i).click();
103
+
104
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
105
+ payload: 0.1,
106
+ type: 'app/change-preview-scale'
107
+ });
108
+ expect(dispatch).toHaveBeenNthCalledWith(2, {
109
+ payload: 'fixed',
110
+ type: 'app/change-preview-scale-mode'
111
+ });
112
+ });
113
+
114
+ test('select from dropdown', () => {
115
+ const { dispatch } = render(<ViewChanger />, {
116
+ initialState: {
117
+ scale: 1,
118
+ fitPreview: false
119
+ }
120
+ });
121
+ const combobox = screen.getByRole('combobox');
122
+
123
+ if (!combobox.parentElement) {
124
+ expect(combobox.parentElement).not.toBeNull();
125
+ return;
126
+ }
127
+ combobox.parentElement.querySelector('button')?.click();
128
+
129
+ screen.getByText('50%').click();
130
+
131
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
132
+ payload: 0.5,
133
+ type: 'app/change-preview-scale'
134
+ });
135
+ expect(dispatch).toHaveBeenNthCalledWith(2, {
136
+ payload: 'fixed',
137
+ type: 'app/change-preview-scale-mode'
138
+ });
139
+ });
140
+
141
+ test('select "fit" from dropdown', () => {
142
+ const { dispatch } = render(<ViewChanger />, {
143
+ initialState: {
144
+ scale: 1,
145
+ fitPreview: false
146
+ }
147
+ });
148
+ const combobox = screen.getByRole('combobox');
149
+
150
+ if (!combobox.parentElement) {
151
+ expect(combobox.parentElement).not.toBeNull();
152
+ return;
153
+ }
154
+ combobox.parentElement.querySelector('button')?.click();
155
+
156
+ screen.getByText(/fit/i).click();
157
+
158
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
159
+ payload: 'fit',
160
+ type: 'app/change-preview-scale-mode'
161
+ });
162
+ });
163
+
164
+ test('enter "new" value in combobox', () => {
165
+ const { dispatch } = render(<ViewChanger />, {
166
+ initialState: {
167
+ scale: 1,
168
+ fitPreview: true
169
+ }
170
+ });
171
+ const combobox = screen.getByRole('combobox');
172
+
173
+ if (!combobox.parentElement) {
174
+ expect(combobox.parentElement).not.toBeNull();
175
+ return;
176
+ }
177
+ const dropDownEditor = screen.getByTestId('testId-view-changer-combobox');
178
+ const dropDownEditorInput = dropDownEditor.querySelector('input');
179
+ if (dropDownEditorInput) {
180
+ fireEvent.focus(dropDownEditorInput);
181
+ fireEvent.input(dropDownEditorInput, { target: { value: '35%' } });
182
+ fireEvent.blur(dropDownEditorInput);
183
+ }
184
+
185
+ expect(dispatch).toHaveBeenNthCalledWith(1, {
186
+ payload: 0.35,
187
+ type: 'app/change-preview-scale'
188
+ });
189
+ });
190
+ });
@@ -0,0 +1,23 @@
1
+ import { getCachedValue, setCachedValue } from '../../../../src/panels/properties/propertyValuesCache';
2
+ import { InputType } from '../../../../src/panels/properties/types';
3
+
4
+ describe('propertyValuesCache', () => {
5
+ test('read/write', () => {
6
+ const controlId = 'testControlId';
7
+ const propertyName = 'testPropName';
8
+
9
+ setCachedValue(controlId, propertyName, InputType.expression, '{firstExpression}');
10
+
11
+ let cachedValue = getCachedValue('controlId2', propertyName, InputType.enumMember);
12
+ expect(cachedValue).toMatchInlineSnapshot(`null`);
13
+
14
+ cachedValue = getCachedValue(controlId, 'propertyName2', InputType.enumMember);
15
+ expect(cachedValue).toMatchInlineSnapshot(`null`);
16
+
17
+ cachedValue = getCachedValue(controlId, propertyName, InputType.enumMember);
18
+ expect(cachedValue).toMatchInlineSnapshot(`null`);
19
+
20
+ cachedValue = getCachedValue(controlId, propertyName, InputType.expression);
21
+ expect(cachedValue).toMatchInlineSnapshot(`"{firstExpression}"`);
22
+ });
23
+ });