@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,159 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useDispatch } from 'react-redux';
|
|
5
|
+
|
|
6
|
+
import { Stack, StackItem, Text } from '@fluentui/react';
|
|
7
|
+
import { UIIcon, UIIconButton, UiIcons, UIDialog } from '@sap-ux/ui-components';
|
|
8
|
+
|
|
9
|
+
import type { PropertyChangeDeletionDetails } from '@sap-ux-private/control-property-editor-common';
|
|
10
|
+
import { convertCamelCaseToPascalCase, deletePropertyChanges } from '@sap-ux-private/control-property-editor-common';
|
|
11
|
+
import { IconName } from '../../icons';
|
|
12
|
+
|
|
13
|
+
import styles from './PropertyChange.module.scss';
|
|
14
|
+
import { getFormattedDateAndTime } from './utils';
|
|
15
|
+
|
|
16
|
+
export interface PropertyChangeProps {
|
|
17
|
+
controlId: string;
|
|
18
|
+
controlName: string;
|
|
19
|
+
changeIndex: number;
|
|
20
|
+
propertyName: string;
|
|
21
|
+
value: string | number | boolean;
|
|
22
|
+
isActive: boolean;
|
|
23
|
+
timestamp?: number;
|
|
24
|
+
fileName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Class used for showing and hiding actions
|
|
27
|
+
*/
|
|
28
|
+
actionClassName: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* React element for property change.
|
|
33
|
+
*
|
|
34
|
+
* @param propertyChangeProps PropertyChangeProps
|
|
35
|
+
* @returns ReactElement
|
|
36
|
+
*/
|
|
37
|
+
export function PropertyChange(propertyChangeProps: PropertyChangeProps): ReactElement {
|
|
38
|
+
const { controlId, propertyName, value, isActive, timestamp, fileName, actionClassName } = propertyChangeProps;
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
const dispatch = useDispatch();
|
|
41
|
+
const [dialogState, setDialogState] = useState<PropertyChangeDeletionDetails | undefined>(undefined);
|
|
42
|
+
|
|
43
|
+
const valueIcon = getValueIcon(value);
|
|
44
|
+
|
|
45
|
+
function onConfirmDelete(): void {
|
|
46
|
+
if (dialogState) {
|
|
47
|
+
dispatch(deletePropertyChanges(dialogState));
|
|
48
|
+
setDialogState(undefined);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onCancelDelete(): void {
|
|
53
|
+
setDialogState(undefined);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<Stack
|
|
59
|
+
tokens={{
|
|
60
|
+
childrenGap: 5
|
|
61
|
+
}}
|
|
62
|
+
className={styles.container}
|
|
63
|
+
style={{
|
|
64
|
+
opacity: isActive ? 1 : 0.4
|
|
65
|
+
}}>
|
|
66
|
+
<Stack.Item className={styles.property}>
|
|
67
|
+
<Stack
|
|
68
|
+
horizontal
|
|
69
|
+
horizontalAlign={'space-between'}
|
|
70
|
+
tokens={{
|
|
71
|
+
childrenGap: 5
|
|
72
|
+
}}>
|
|
73
|
+
<Stack.Item>
|
|
74
|
+
<Text className={styles.text}>{convertCamelCaseToPascalCase(propertyName)}</Text>
|
|
75
|
+
<UIIcon iconName={IconName.arrow} className={styles.text} />
|
|
76
|
+
{valueIcon && (
|
|
77
|
+
<UIIcon
|
|
78
|
+
className={styles.valueIcon}
|
|
79
|
+
iconName={valueIcon}
|
|
80
|
+
style={{
|
|
81
|
+
marginRight: 5,
|
|
82
|
+
background: 'var(--vscode-terminal-ansiBlue)',
|
|
83
|
+
borderRadius: '50%',
|
|
84
|
+
height: 16,
|
|
85
|
+
width: 16,
|
|
86
|
+
verticalAlign: 'bottom'
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
<Text className={styles.text}>{value}</Text>
|
|
91
|
+
</Stack.Item>
|
|
92
|
+
{fileName && (
|
|
93
|
+
<Stack.Item className={actionClassName}>
|
|
94
|
+
<UIIconButton
|
|
95
|
+
iconProps={{ iconName: UiIcons.TrashCan }}
|
|
96
|
+
onClick={(): void => {
|
|
97
|
+
if (controlId) {
|
|
98
|
+
setDialogState({
|
|
99
|
+
controlId,
|
|
100
|
+
propertyName,
|
|
101
|
+
fileName
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
</Stack.Item>
|
|
107
|
+
)}
|
|
108
|
+
</Stack>
|
|
109
|
+
</Stack.Item>
|
|
110
|
+
|
|
111
|
+
{timestamp && (
|
|
112
|
+
<StackItem>
|
|
113
|
+
<Stack horizontal horizontalAlign="space-between">
|
|
114
|
+
<Text className={styles.timestamp}>{getFormattedDateAndTime(timestamp)}</Text>
|
|
115
|
+
</Stack>
|
|
116
|
+
</StackItem>
|
|
117
|
+
)}
|
|
118
|
+
</Stack>
|
|
119
|
+
{dialogState && (
|
|
120
|
+
<UIDialog
|
|
121
|
+
hidden={dialogState === undefined}
|
|
122
|
+
onAccept={onConfirmDelete}
|
|
123
|
+
acceptButtonText={t('CONFIRM_DELETE')}
|
|
124
|
+
cancelButtonText={t('CANCEL_DELETE')}
|
|
125
|
+
onCancel={onCancelDelete}
|
|
126
|
+
dialogContentProps={{
|
|
127
|
+
title: t('CONFIRM_DELETE_TITLE'),
|
|
128
|
+
subText: t('CONFIRM_CHANGE_SUMMARY_DELETE_SUBTEXT')
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get value icon based on type.
|
|
138
|
+
*
|
|
139
|
+
* @param value string | number | boolean
|
|
140
|
+
* @returns string | undefined
|
|
141
|
+
*/
|
|
142
|
+
function getValueIcon(value: string | number | boolean): string | undefined {
|
|
143
|
+
if (typeof value === 'string') {
|
|
144
|
+
if (value.trim().startsWith('{') && value.trim().endsWith('}')) {
|
|
145
|
+
return IconName.expression;
|
|
146
|
+
} else {
|
|
147
|
+
return IconName.string;
|
|
148
|
+
}
|
|
149
|
+
} else if (typeof value === 'number') {
|
|
150
|
+
return IconName.number;
|
|
151
|
+
} else if (typeof value === 'boolean') {
|
|
152
|
+
if (value === true) {
|
|
153
|
+
return IconName.boolTrue;
|
|
154
|
+
} else {
|
|
155
|
+
return IconName.boolFalse;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.item {
|
|
2
|
+
padding: 5px 15px 5px 15px;
|
|
3
|
+
}
|
|
4
|
+
.text {
|
|
5
|
+
line-height: 18px;
|
|
6
|
+
display: inline-block;
|
|
7
|
+
text-overflow: ellipsis;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
white-space: nowrap;
|
|
10
|
+
color: var(--vscode-editor-foreground);
|
|
11
|
+
width: 240px;
|
|
12
|
+
}
|
|
13
|
+
.timestamp {
|
|
14
|
+
color: var(--vscode-editor-foreground);
|
|
15
|
+
font-size: 11px;
|
|
16
|
+
line-height: 15px;
|
|
17
|
+
opacity: 0.5;
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
.textHeader {
|
|
21
|
+
display: inline-block;
|
|
22
|
+
color: var(--vscode-textLink-foreground);
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
font-weight: bold;
|
|
25
|
+
text-overflow: ellipsis;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
line-height: 18px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.property {
|
|
32
|
+
overflow-wrap: anywhere;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.item:hover {
|
|
36
|
+
background-color: var(--vscode-dropdown-background);
|
|
37
|
+
color: var(--vscode-dropdown-foreground);
|
|
38
|
+
outline: 1px dashed var(--vscode-contrastActiveBorder);
|
|
39
|
+
.actions {
|
|
40
|
+
visibility: visible;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.actions {
|
|
45
|
+
visibility: hidden;
|
|
46
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Text, Stack } from '@fluentui/react';
|
|
5
|
+
import { useDispatch } from 'react-redux';
|
|
6
|
+
import styles from './UnknownChange.module.scss';
|
|
7
|
+
import { UIIconButton, UiIcons, UIDialog } from '@sap-ux/ui-components';
|
|
8
|
+
import type { PropertyChangeDeletionDetails } from '@sap-ux-private/control-property-editor-common';
|
|
9
|
+
import { deletePropertyChanges, convertCamelCaseToPascalCase } from '@sap-ux-private/control-property-editor-common';
|
|
10
|
+
import { getFormattedDateAndTime } from './utils';
|
|
11
|
+
|
|
12
|
+
export interface UnknownChangeProps {
|
|
13
|
+
fileName: string;
|
|
14
|
+
timestamp?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* React element for unknown change in change stack.
|
|
19
|
+
*
|
|
20
|
+
* @param unknownChangeProps UnknownChangeProps
|
|
21
|
+
* @returns ReactElement
|
|
22
|
+
*/
|
|
23
|
+
export function UnknownChange(unknownChangeProps: UnknownChangeProps): ReactElement {
|
|
24
|
+
const { fileName, timestamp } = unknownChangeProps;
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const dispatch = useDispatch();
|
|
27
|
+
const [dialogState, setDialogState] = useState<PropertyChangeDeletionDetails | undefined>(undefined);
|
|
28
|
+
|
|
29
|
+
function onConfirmDelete(): void {
|
|
30
|
+
if (dialogState) {
|
|
31
|
+
dispatch(deletePropertyChanges(dialogState));
|
|
32
|
+
setDialogState(undefined);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onCancelDelete(): void {
|
|
37
|
+
setDialogState(undefined);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parts = fileName.split('_');
|
|
41
|
+
const changeName = parts[parts.length - 1];
|
|
42
|
+
const name = convertCamelCaseToPascalCase(changeName);
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Stack className={styles.item}>
|
|
46
|
+
<Stack.Item className={styles.property}>
|
|
47
|
+
<Stack horizontal>
|
|
48
|
+
<Stack.Item>
|
|
49
|
+
<Text className={styles.textHeader}>
|
|
50
|
+
{name} {t('CHANGE')}
|
|
51
|
+
</Text>
|
|
52
|
+
<Text className={styles.text} title={fileName}>
|
|
53
|
+
{t('FILE')}
|
|
54
|
+
{fileName}
|
|
55
|
+
</Text>
|
|
56
|
+
</Stack.Item>
|
|
57
|
+
{fileName && (
|
|
58
|
+
<Stack.Item className={styles.actions}>
|
|
59
|
+
<UIIconButton
|
|
60
|
+
iconProps={{ iconName: UiIcons.TrashCan }}
|
|
61
|
+
onClick={(): void => {
|
|
62
|
+
setDialogState({
|
|
63
|
+
controlId: '',
|
|
64
|
+
propertyName: '',
|
|
65
|
+
fileName
|
|
66
|
+
});
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
</Stack.Item>
|
|
70
|
+
)}
|
|
71
|
+
</Stack>
|
|
72
|
+
</Stack.Item>
|
|
73
|
+
{timestamp && (
|
|
74
|
+
<Stack.Item>
|
|
75
|
+
<Stack horizontal horizontalAlign="space-between">
|
|
76
|
+
<Text className={styles.timestamp}>{getFormattedDateAndTime(timestamp)}</Text>
|
|
77
|
+
</Stack>
|
|
78
|
+
</Stack.Item>
|
|
79
|
+
)}
|
|
80
|
+
</Stack>
|
|
81
|
+
{dialogState && (
|
|
82
|
+
<UIDialog
|
|
83
|
+
hidden={dialogState === undefined}
|
|
84
|
+
onAccept={onConfirmDelete}
|
|
85
|
+
acceptButtonText={t('CONFIRM_DELETE')}
|
|
86
|
+
cancelButtonText={t('CANCEL_DELETE')}
|
|
87
|
+
onCancel={onCancelDelete}
|
|
88
|
+
dialogContentProps={{
|
|
89
|
+
title: t('CONFIRM_DELETE_TITLE'),
|
|
90
|
+
subText: t('CONFIRM_CHANGE_SUMMARY_DELETE_SUBTEXT')
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const localeTimeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', hour12: false };
|
|
2
|
+
const localeDateOptions: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: '2-digit' };
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gets locale.
|
|
6
|
+
*
|
|
7
|
+
* @returns string
|
|
8
|
+
*/
|
|
9
|
+
function getLocale(): string {
|
|
10
|
+
if (globalThis?.navigator?.languages) {
|
|
11
|
+
const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf([...globalThis.navigator.languages], {
|
|
12
|
+
...localeDateOptions,
|
|
13
|
+
...localeTimeOptions
|
|
14
|
+
});
|
|
15
|
+
if (supportedLocales.length) {
|
|
16
|
+
return supportedLocales[0];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return 'en-GB';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets formatted date time based on locale.
|
|
24
|
+
*
|
|
25
|
+
* @param timestamp number
|
|
26
|
+
* @returns string
|
|
27
|
+
*/
|
|
28
|
+
export function getFormattedDateAndTime(timestamp: number): string {
|
|
29
|
+
const date = new Date(timestamp);
|
|
30
|
+
const locale = getLocale();
|
|
31
|
+
|
|
32
|
+
return `${date.toLocaleTimeString(locale, localeTimeOptions)} ${date.toLocaleDateString(
|
|
33
|
+
locale,
|
|
34
|
+
localeDateOptions
|
|
35
|
+
)}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { UICheckbox, UIIconButton, UICallout, UICalloutContentPadding } from '@sap-ux/ui-components';
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
import { IconName } from '../../icons';
|
|
7
|
+
import type { FilterOptions } from '../../slice';
|
|
8
|
+
import { FilterName, filterNodes } from '../../slice';
|
|
9
|
+
import type { RootState } from '../../store';
|
|
10
|
+
|
|
11
|
+
const TARGET = 'control-property-editor-funnel-callout-target-id';
|
|
12
|
+
|
|
13
|
+
const Funnel = (): ReactElement => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const filterQuery = useSelector<RootState, FilterOptions[]>((state) => state.filterQuery);
|
|
16
|
+
const dispatch = useDispatch();
|
|
17
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
18
|
+
const showCallout = () => {
|
|
19
|
+
setIsVisible(true);
|
|
20
|
+
};
|
|
21
|
+
const onChange = (name: FilterName, isChecked = false) => {
|
|
22
|
+
const action = filterNodes([{ name, value: isChecked }]);
|
|
23
|
+
dispatch(action);
|
|
24
|
+
};
|
|
25
|
+
const focusChecked = filterQuery.filter((item) => item.name === FilterName.focusEditable)[0].value as boolean;
|
|
26
|
+
const focusCommonlyUsedChecked = filterQuery.filter((item) => item.name === FilterName.focusCommonlyUsed)[0]
|
|
27
|
+
.value as boolean;
|
|
28
|
+
const checked = focusChecked || focusCommonlyUsedChecked;
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<UIIconButton
|
|
32
|
+
id={TARGET}
|
|
33
|
+
className={`funnel-icon`}
|
|
34
|
+
iconProps={{ iconName: IconName.funnel }}
|
|
35
|
+
checked={checked}
|
|
36
|
+
onClick={showCallout}></UIIconButton>
|
|
37
|
+
{isVisible && (
|
|
38
|
+
<UICallout
|
|
39
|
+
target={`#${TARGET}`}
|
|
40
|
+
isBeakVisible={true}
|
|
41
|
+
beakWidth={5}
|
|
42
|
+
directionalHint={4}
|
|
43
|
+
onDismiss={() => setIsVisible(false)}
|
|
44
|
+
contentPadding={UICalloutContentPadding.Standard}>
|
|
45
|
+
<UICheckbox
|
|
46
|
+
className={'funnel-call-out-checkbox'}
|
|
47
|
+
label={t('FOCUS_EDITABLE')}
|
|
48
|
+
checked={focusChecked}
|
|
49
|
+
onChange={(ev?: React.FormEvent<HTMLElement | HTMLInputElement>, isChecked?: boolean) => {
|
|
50
|
+
onChange(FilterName.focusEditable, isChecked);
|
|
51
|
+
}}></UICheckbox>
|
|
52
|
+
<UICheckbox
|
|
53
|
+
label={t('FOCUS_COMMONLY_USED')}
|
|
54
|
+
checked={focusCommonlyUsedChecked}
|
|
55
|
+
onChange={(ev?: React.FormEvent<HTMLElement | HTMLInputElement>, isChecked?: boolean) => {
|
|
56
|
+
onChange(FilterName.focusCommonlyUsed, isChecked);
|
|
57
|
+
}}></UICheckbox>
|
|
58
|
+
</UICallout>
|
|
59
|
+
)}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { Funnel };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from '@fluentui/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
const NoControlFound = () => {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Text className="tree-no-control-found">{t('NO_CONTROL_FOUND')}</Text>
|
|
10
|
+
<Text className="tree-modify-search-input">{t('MODIFY_SEARCH_INPUT')}</Text>
|
|
11
|
+
<div className="tree-search-icon">
|
|
12
|
+
<svg
|
|
13
|
+
data-testid="Control-Property-Editor-No-Search-Matched-Icon"
|
|
14
|
+
width="49"
|
|
15
|
+
height="49"
|
|
16
|
+
viewBox="0 0 49 49"
|
|
17
|
+
fill="none"
|
|
18
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
19
|
+
<path
|
|
20
|
+
fillRule="evenodd"
|
|
21
|
+
clipRule="evenodd"
|
|
22
|
+
d="M20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.9543 0 0 8.9543 0 20C0 31.0457 8.9543 40 20 40ZM20 37C29.3888 37 37 29.3888 37 20C37 10.6112 29.3888 3 20 3C10.6112 3 3 10.6112 3 20C3 29.3888 10.6112 37 20 37Z"
|
|
23
|
+
fill="var(--vscode-icon-foreground)"
|
|
24
|
+
/>
|
|
25
|
+
<rect
|
|
26
|
+
x="33"
|
|
27
|
+
y="35.1213"
|
|
28
|
+
width="3"
|
|
29
|
+
height="19.2712"
|
|
30
|
+
transform="rotate(-45 33 35.1213)"
|
|
31
|
+
fill="var(--vscode-icon-foreground)"
|
|
32
|
+
/>
|
|
33
|
+
<rect x="15" y="13" width="2" height="7" fill="var(--vscode-focusBorder)" />
|
|
34
|
+
<rect x="23" y="13" width="2" height="7" fill="var(--vscode-focusBorder)" />
|
|
35
|
+
<path
|
|
36
|
+
d="M12.5815 28C13.7683 25.0682 16.6426 23 20 23C23.3574 23 26.2317 25.0682 27.4185 28H25.1973C24.1599 26.2066 22.2208 25 20 25C17.7792 25 15.8401 26.2066 14.8027 28H12.5815Z"
|
|
37
|
+
fill="var(--vscode-focusBorder)"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
</div>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export { NoControlFound };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
.filter-outline {
|
|
2
|
+
display: flex;
|
|
3
|
+
margin: 20px 14px 30px 14px;
|
|
4
|
+
flex-direction: row;
|
|
5
|
+
align-items: center;
|
|
6
|
+
overflow-y: auto;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.funnel-icon {
|
|
10
|
+
margin-left: 16px;
|
|
11
|
+
i {
|
|
12
|
+
line-height: 20px;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.funnel-call-out-checkbox {
|
|
17
|
+
padding-bottom: 5px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.tree-no-control-found {
|
|
21
|
+
font-style: normal;
|
|
22
|
+
font-weight: bold;
|
|
23
|
+
font-size: 24px;
|
|
24
|
+
line-height: 29px;
|
|
25
|
+
text-align: center;
|
|
26
|
+
color: var(--vscode-foreground);
|
|
27
|
+
}
|
|
28
|
+
.tree-modify-search-input {
|
|
29
|
+
font-style: normal;
|
|
30
|
+
font-weight: normal;
|
|
31
|
+
font-size: 13px;
|
|
32
|
+
line-height: 18px;
|
|
33
|
+
text-align: center;
|
|
34
|
+
padding-top: 20px;
|
|
35
|
+
color: var(--vscode-foreground);
|
|
36
|
+
}
|
|
37
|
+
.tree-search-icon {
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tree-cell {
|
|
42
|
+
display: flex;
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
font-weight: normal;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
text-overflow: ellipsis;
|
|
47
|
+
height: 18px;
|
|
48
|
+
padding: 5px 0;
|
|
49
|
+
min-width: 0px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.tree-row {
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
&:hover {
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
background-color: var(--vscode-dropdown-background);
|
|
59
|
+
color: var(--vscode-dropdown-foreground);
|
|
60
|
+
outline: 1px dashed var(--vscode-contrastActiveBorder);
|
|
61
|
+
}
|
|
62
|
+
align-items: center;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.app-panel-scroller > div {
|
|
66
|
+
padding-top: 0px;
|
|
67
|
+
}
|
|
68
|
+
.focusEditable {
|
|
69
|
+
opacity: 0.55;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.tree-row-icon {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-wrap: nowrap;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
position: relative;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.right-chevron-icon {
|
|
80
|
+
margin-right: 3px;
|
|
81
|
+
transform: rotate(0deg);
|
|
82
|
+
transform-origin: 50% 50%;
|
|
83
|
+
transition: transform 0.1s linear 0s;
|
|
84
|
+
}
|
|
85
|
+
.down-chevron-icon {
|
|
86
|
+
margin-right: 3px;
|
|
87
|
+
transform: rotate(90deg);
|
|
88
|
+
transform-origin: 50% 50%;
|
|
89
|
+
transition: transform 0.1s linear 0s;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.app-panel-selected-bg.tree-row {
|
|
93
|
+
color: var(--vscode-list-activeSelectionForeground);
|
|
94
|
+
background-color: var(--vscode-list-activeSelectionBackground);
|
|
95
|
+
outline: 1px solid var(--vscode-contrastActiveBorder);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useDispatch } from 'react-redux';
|
|
5
|
+
import { UISearchBox } from '@sap-ux/ui-components';
|
|
6
|
+
import { FilterName, filterNodes } from '../../slice';
|
|
7
|
+
import './OutlinePanel.scss';
|
|
8
|
+
|
|
9
|
+
import { Tree } from './Tree';
|
|
10
|
+
import { Funnel } from './Funnel';
|
|
11
|
+
|
|
12
|
+
const OutlinePanel = (): ReactElement => {
|
|
13
|
+
const dispatch = useDispatch();
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const onFilterChange = (
|
|
16
|
+
event?: React.ChangeEvent<HTMLInputElement> | undefined,
|
|
17
|
+
newValue?: string | undefined
|
|
18
|
+
): void => {
|
|
19
|
+
const action = filterNodes([{ name: FilterName.query, value: newValue ?? '' }]);
|
|
20
|
+
dispatch(action);
|
|
21
|
+
};
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<div className="filter-outline">
|
|
25
|
+
<UISearchBox
|
|
26
|
+
autoFocus={false}
|
|
27
|
+
disableAnimation={false}
|
|
28
|
+
placeholder={t('SEARCH_OUTLINE')}
|
|
29
|
+
onChange={onFilterChange}
|
|
30
|
+
/>
|
|
31
|
+
<Funnel />
|
|
32
|
+
</div>
|
|
33
|
+
<Tree />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { OutlinePanel };
|