@tolgee/web 4.9.3-rc.04b22e6.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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/tolgee-backend-fetch.cjs.min.js +2 -0
- package/dist/tolgee-backend-fetch.cjs.min.js.map +1 -0
- package/dist/tolgee-backend-fetch.esm.min.mjs +2 -0
- package/dist/tolgee-backend-fetch.esm.min.mjs.map +1 -0
- package/dist/tolgee-backend-fetch.umd.min.js +2 -0
- package/dist/tolgee-backend-fetch.umd.min.js.map +1 -0
- package/dist/tolgee-context-ui.cjs.min.js +143 -0
- package/dist/tolgee-context-ui.cjs.min.js.map +1 -0
- package/dist/tolgee-context-ui.esm.min.mjs +143 -0
- package/dist/tolgee-context-ui.esm.min.mjs.map +1 -0
- package/dist/tolgee-context-ui.umd.min.js +143 -0
- package/dist/tolgee-context-ui.umd.min.js.map +1 -0
- package/dist/tolgee-in-context-tools.cjs.min.js +143 -0
- package/dist/tolgee-in-context-tools.cjs.min.js.map +1 -0
- package/dist/tolgee-in-context-tools.esm.min.mjs +143 -0
- package/dist/tolgee-in-context-tools.esm.min.mjs.map +1 -0
- package/dist/tolgee-in-context-tools.umd.min.js +143 -0
- package/dist/tolgee-in-context-tools.umd.min.js.map +1 -0
- package/dist/tolgee-invisible-observer.cjs.min.js +2 -0
- package/dist/tolgee-invisible-observer.cjs.min.js.map +1 -0
- package/dist/tolgee-invisible-observer.esm.min.mjs +2 -0
- package/dist/tolgee-invisible-observer.esm.min.mjs.map +1 -0
- package/dist/tolgee-invisible-observer.umd.min.js +2 -0
- package/dist/tolgee-invisible-observer.umd.min.js.map +1 -0
- package/dist/tolgee-language-detector.cjs.min.js +2 -0
- package/dist/tolgee-language-detector.cjs.min.js.map +1 -0
- package/dist/tolgee-language-detector.esm.min.mjs +2 -0
- package/dist/tolgee-language-detector.esm.min.mjs.map +1 -0
- package/dist/tolgee-language-detector.umd.min.js +2 -0
- package/dist/tolgee-language-detector.umd.min.js.map +1 -0
- package/dist/tolgee-language-storage.cjs.min.js +2 -0
- package/dist/tolgee-language-storage.cjs.min.js.map +1 -0
- package/dist/tolgee-language-storage.esm.min.mjs +2 -0
- package/dist/tolgee-language-storage.esm.min.mjs.map +1 -0
- package/dist/tolgee-language-storage.umd.min.js +2 -0
- package/dist/tolgee-language-storage.umd.min.js.map +1 -0
- package/dist/tolgee-text-observer.cjs.min.js +2 -0
- package/dist/tolgee-text-observer.cjs.min.js.map +1 -0
- package/dist/tolgee-text-observer.esm.min.mjs +2 -0
- package/dist/tolgee-text-observer.esm.min.mjs.map +1 -0
- package/dist/tolgee-text-observer.umd.min.js +2 -0
- package/dist/tolgee-text-observer.umd.min.js.map +1 -0
- package/dist/tolgee-web-tolgee.cjs.min.js +2 -0
- package/dist/tolgee-web-tolgee.cjs.min.js.map +1 -0
- package/dist/tolgee-web-tolgee.esm.min.mjs +2 -0
- package/dist/tolgee-web-tolgee.esm.min.mjs.map +1 -0
- package/dist/tolgee-web-tolgee.umd.min.js +2 -0
- package/dist/tolgee-web-tolgee.umd.min.js.map +1 -0
- package/dist/tolgee-web.cjs.js +27079 -0
- package/dist/tolgee-web.cjs.js.map +1 -0
- package/dist/tolgee-web.cjs.min.js +143 -0
- package/dist/tolgee-web.cjs.min.js.map +1 -0
- package/dist/tolgee-web.esm.js +27055 -0
- package/dist/tolgee-web.esm.js.map +1 -0
- package/dist/tolgee-web.esm.min.mjs +143 -0
- package/dist/tolgee-web.esm.min.mjs.map +1 -0
- package/dist/tolgee-web.umd.js +27085 -0
- package/dist/tolgee-web.umd.js.map +1 -0
- package/dist/tolgee-web.umd.min.js +143 -0
- package/dist/tolgee-web.umd.min.js.map +1 -0
- package/lib/BackendFetch.d.ts +4 -0
- package/lib/BrowserExtensionPlugin/BrowserExtensionPlugin.d.ts +8 -0
- package/lib/BrowserExtensionPlugin/constants.d.ts +3 -0
- package/lib/BrowserExtensionPlugin/loadInContextLib.d.ts +1 -0
- package/lib/ContextUi.d.ts +2 -0
- package/lib/DevBackend.d.ts +2 -0
- package/lib/DevTools.d.ts +3 -0
- package/lib/InContextTools.d.ts +2 -0
- package/lib/InvisibleObserver.d.ts +2 -0
- package/lib/LanguageDetector.d.ts +3 -0
- package/lib/LanguageStorage.d.ts +3 -0
- package/lib/TextObserver.d.ts +2 -0
- package/lib/WebTolgee.d.ts +3 -0
- package/lib/constants.d.ts +5 -0
- package/lib/index.d.ts +5 -0
- package/lib/observers/general/DomHelper.d.ts +4 -0
- package/lib/observers/general/ElementHighlighter.d.ts +10 -0
- package/lib/observers/general/ElementMeta.d.ts +3 -0
- package/lib/observers/general/ElementRegistry.d.ts +11 -0
- package/lib/observers/general/ElementStore.d.ts +10 -0
- package/lib/observers/general/GeneralObserver.d.ts +12 -0
- package/lib/observers/general/MouseEventHandler.d.ts +13 -0
- package/lib/observers/general/NodeHandler.d.ts +6 -0
- package/lib/observers/general/helpers.d.ts +7 -0
- package/lib/observers/invisible/InvisibleWrapper.d.ts +2 -0
- package/lib/observers/invisible/ValueMemory.d.ts +5 -0
- package/lib/observers/invisible/encoderPolyfill.d.ts +8 -0
- package/lib/observers/invisible/secret.d.ts +6 -0
- package/lib/observers/text/TextWrapper.d.ts +8 -0
- package/lib/observers/text/helpers.d.ts +3 -0
- package/lib/tools/decodeApiKey.d.ts +1 -0
- package/lib/tools/extension.d.ts +28 -0
- package/lib/typedIndex.d.ts +11 -0
- package/lib/types.d.ts +28 -0
- package/lib/ui/KeyContextMenu/KeyContextMenu.d.ts +19 -0
- package/lib/ui/KeyDialog/KeyDialog.d.ts +23 -0
- package/lib/ui/KeyDialog/KeyForm.d.ts +2 -0
- package/lib/ui/KeyDialog/LanguageSelect.d.ts +2 -0
- package/lib/ui/KeyDialog/NewWindow.d.ts +2 -0
- package/lib/ui/KeyDialog/NsSelect.d.ts +9 -0
- package/lib/ui/KeyDialog/ScreenshotGallery/ExtensionPrompt.d.ts +6 -0
- package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotDetail.d.ts +8 -0
- package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotDropzone.d.ts +6 -0
- package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotGallery.d.ts +2 -0
- package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotThumbnail.d.ts +8 -0
- package/lib/ui/KeyDialog/ScreenshotGallery/utils.d.ts +3 -0
- package/lib/ui/KeyDialog/TranslationDialog.d.ts +2 -0
- package/lib/ui/KeyDialog/TranslationDialogContextProvider.d.ts +128 -0
- package/lib/ui/KeyDialog/TranslationDialogWrapper.d.ts +2 -0
- package/lib/ui/KeyDialog/TranslationFields.d.ts +2 -0
- package/lib/ui/KeyDialog/languageHelpers.d.ts +12 -0
- package/lib/ui/KeyDialog/tools.d.ts +3 -0
- package/lib/ui/ThemeProvider.d.ts +2 -0
- package/lib/ui/client/QueryProvider.d.ts +12 -0
- package/lib/ui/client/apiSchema.generated.d.ts +3283 -0
- package/lib/ui/client/client.d.ts +5 -0
- package/lib/ui/client/types.d.ts +58 -0
- package/lib/ui/client/useQueryApi.d.ts +84 -0
- package/lib/ui/common/BodyEnd.d.ts +13 -0
- package/lib/ui/common/FieldTitle.d.ts +2 -0
- package/lib/ui/common/LoadingButton.d.ts +7 -0
- package/lib/ui/index.d.ts +13 -0
- package/lib/ui/tools/createProvider.d.ts +5 -0
- package/lib/ui/tools/isLanguagePermitted.d.ts +1 -0
- package/lib/ui/tools/sleep.d.ts +1 -0
- package/package.json +91 -0
- package/src/BackendFetch.ts +64 -0
- package/src/BrowserExtensionPlugin/BrowserExtensionPlugin.ts +98 -0
- package/src/BrowserExtensionPlugin/constants.ts +3 -0
- package/src/BrowserExtensionPlugin/loadInContextLib.ts +32 -0
- package/src/ContextUi.ts +7 -0
- package/src/DevBackend.ts +30 -0
- package/src/DevTools.ts +9 -0
- package/src/InContextTools.ts +20 -0
- package/src/InvisibleObserver.ts +16 -0
- package/src/LanguageDetector.test.ts +19 -0
- package/src/LanguageDetector.ts +32 -0
- package/src/LanguageStorage.ts +23 -0
- package/src/TextObserver.ts +45 -0
- package/src/WebTolgee.ts +8 -0
- package/src/__test__/browser.extension.test.ts +70 -0
- package/src/__test__/observer.test.ts +13 -0
- package/src/__test__/testObserver.ts +106 -0
- package/src/__test__/testRetranslate.ts +47 -0
- package/src/constants.ts +12 -0
- package/src/index.ts +8 -0
- package/src/observers/general/DomHelper.ts +46 -0
- package/src/observers/general/ElementHighlighter.ts +72 -0
- package/src/observers/general/ElementMeta.ts +17 -0
- package/src/observers/general/ElementRegistry.ts +159 -0
- package/src/observers/general/ElementStore.ts +34 -0
- package/src/observers/general/GeneralObserver.ts +133 -0
- package/src/observers/general/MouseEventHandler.ts +199 -0
- package/src/observers/general/NodeHandler.ts +39 -0
- package/src/observers/general/helpers.ts +65 -0
- package/src/observers/invisible/InvisibleWrapper.test.ts +17 -0
- package/src/observers/invisible/InvisibleWrapper.ts +96 -0
- package/src/observers/invisible/ValueMemory.test.ts +25 -0
- package/src/observers/invisible/ValueMemory.ts +20 -0
- package/src/observers/invisible/encoderPolyfill.ts +96 -0
- package/src/observers/invisible/secret.test.ts +61 -0
- package/src/observers/invisible/secret.ts +68 -0
- package/src/observers/text/TextWrapper.ts +258 -0
- package/src/observers/text/helpers.ts +56 -0
- package/src/tools/decodeApiKey.test.ts +14 -0
- package/src/tools/decodeApiKey.ts +74 -0
- package/src/tools/extension.test.ts +159 -0
- package/src/tools/extension.ts +119 -0
- package/src/typedIndex.ts +13 -0
- package/src/types.ts +33 -0
- package/src/ui/KeyContextMenu/KeyContextMenu.test.ts +51 -0
- package/src/ui/KeyContextMenu/KeyContextMenu.tsx +108 -0
- package/src/ui/KeyDialog/KeyDialog.tsx +67 -0
- package/src/ui/KeyDialog/KeyForm.tsx +208 -0
- package/src/ui/KeyDialog/LanguageSelect.tsx +78 -0
- package/src/ui/KeyDialog/NewWindow.tsx +106 -0
- package/src/ui/KeyDialog/NsSelect.tsx +67 -0
- package/src/ui/KeyDialog/ScreenshotGallery/ExtensionPrompt.tsx +97 -0
- package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotDetail.tsx +33 -0
- package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotDropzone.tsx +138 -0
- package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotGallery.tsx +240 -0
- package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotThumbnail.tsx +113 -0
- package/src/ui/KeyDialog/ScreenshotGallery/utils.ts +17 -0
- package/src/ui/KeyDialog/TranslationDialog.tsx +14 -0
- package/src/ui/KeyDialog/TranslationDialogContextProvider.tsx +464 -0
- package/src/ui/KeyDialog/TranslationDialogWrapper.tsx +44 -0
- package/src/ui/KeyDialog/TranslationFields.tsx +113 -0
- package/src/ui/KeyDialog/languageHelpers.ts +18 -0
- package/src/ui/KeyDialog/tools.ts +30 -0
- package/src/ui/ThemeProvider.tsx +71 -0
- package/src/ui/client/QueryProvider.tsx +38 -0
- package/src/ui/client/apiSchema.generated.ts +3281 -0
- package/src/ui/client/client.ts +155 -0
- package/src/ui/client/types.ts +113 -0
- package/src/ui/client/useQueryApi.ts +121 -0
- package/src/ui/common/BodyEnd.tsx +44 -0
- package/src/ui/common/FieldTitle.tsx +9 -0
- package/src/ui/common/LoadingButton.tsx +45 -0
- package/src/ui/index.ts +88 -0
- package/src/ui/screenshots/ScreenshotPreview.tsx +18 -0
- package/src/ui/tools/createProvider.tsx +54 -0
- package/src/ui/tools/isLanguagePermitted.ts +14 -0
- package/src/ui/tools/sleep.ts +2 -0
- package/types/index.d.ts +9 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
message: string;
|
|
3
|
+
recievingMessage: string[];
|
|
4
|
+
data?: any;
|
|
5
|
+
attempts?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function listen<T = any>(type: string[], callback: (data?: T) => any) {
|
|
9
|
+
const handler = (e: MessageEvent) => {
|
|
10
|
+
if (type.includes(e.data?.type)) {
|
|
11
|
+
callback(e.data?.data);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
window.addEventListener('message', handler, false);
|
|
15
|
+
return {
|
|
16
|
+
unsubscribe: () => {
|
|
17
|
+
window.removeEventListener('message', handler);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sendAndRecieve<T>({
|
|
23
|
+
message,
|
|
24
|
+
recievingMessage,
|
|
25
|
+
data,
|
|
26
|
+
attempts = 1,
|
|
27
|
+
}: Props) {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
const makeAttempt = () =>
|
|
30
|
+
new Promise<T>((resolve, reject) => {
|
|
31
|
+
const listener = listen(recievingMessage, handler);
|
|
32
|
+
window.postMessage({ type: message, data }, window.origin);
|
|
33
|
+
const timer = setTimeout(timeout, 300);
|
|
34
|
+
|
|
35
|
+
function handler(data: any) {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
removeEventListener();
|
|
38
|
+
resolve(data);
|
|
39
|
+
}
|
|
40
|
+
function removeEventListener() {
|
|
41
|
+
listener.unsubscribe();
|
|
42
|
+
}
|
|
43
|
+
function timeout() {
|
|
44
|
+
removeEventListener();
|
|
45
|
+
reject();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const getData = async (): Promise<T> => {
|
|
50
|
+
for (let i = 0; i < attempts; i++) {
|
|
51
|
+
if (cancelled) {
|
|
52
|
+
return new Promise<T>(() => {});
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const result = await makeAttempt();
|
|
56
|
+
return result;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!cancelled) {
|
|
62
|
+
throw `Didn't recieve ${recievingMessage.join(' or ')} in time.`;
|
|
63
|
+
}
|
|
64
|
+
return new Promise<T>(() => {});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
cancel: () => (cancelled = true),
|
|
69
|
+
promise: getData(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function takeScreenshot(): Promise<string> {
|
|
74
|
+
return sendAndRecieve({
|
|
75
|
+
message: 'TOLGEE_TAKE_SCREENSHOT',
|
|
76
|
+
recievingMessage: ['TOLGEE_SCREENSHOT_TAKEN'],
|
|
77
|
+
}).promise as Promise<string>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function detectExtension(): Promise<boolean> {
|
|
81
|
+
try {
|
|
82
|
+
await sendAndRecieve({
|
|
83
|
+
message: 'TOLGEE_PING',
|
|
84
|
+
recievingMessage: ['TOLGEE_PONG'],
|
|
85
|
+
attempts: 2,
|
|
86
|
+
}).promise;
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type LibConfig = {
|
|
94
|
+
uiPresent: boolean;
|
|
95
|
+
uiVersion?: string;
|
|
96
|
+
mode: 'production' | 'development';
|
|
97
|
+
config: {
|
|
98
|
+
apiUrl: string;
|
|
99
|
+
apiKey: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function Handshaker() {
|
|
104
|
+
let cancelLast: undefined | (() => void) = undefined;
|
|
105
|
+
async function update(data: LibConfig): Promise<boolean> {
|
|
106
|
+
cancelLast?.();
|
|
107
|
+
const { cancel, promise } = sendAndRecieve<boolean>({
|
|
108
|
+
message: 'TOLGEE_READY',
|
|
109
|
+
recievingMessage: ['TOLGEE_PLUGIN_READY', 'TOLGEE_PLUGIN_UPDATED'],
|
|
110
|
+
data,
|
|
111
|
+
attempts: 4,
|
|
112
|
+
});
|
|
113
|
+
cancelLast = cancel;
|
|
114
|
+
return promise;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
update,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { InvisibleObserver } from './InvisibleObserver';
|
|
2
|
+
export { TextObserver } from './TextObserver';
|
|
3
|
+
export { DevBackend } from './DevBackend';
|
|
4
|
+
export { getProjectIdFromApiKey } from './tools/decodeApiKey';
|
|
5
|
+
export { BrowserExtensionPlugin } from './BrowserExtensionPlugin/BrowserExtensionPlugin';
|
|
6
|
+
export { LanguageStorage } from './LanguageStorage';
|
|
7
|
+
export { LanguageDetector } from './LanguageDetector';
|
|
8
|
+
export { BackendFetch } from './BackendFetch';
|
|
9
|
+
|
|
10
|
+
export * from './types';
|
|
11
|
+
export * from '@tolgee/core';
|
|
12
|
+
// Tolgee with injected browser extension plugin
|
|
13
|
+
export { Tolgee, TolgeeCore } from './WebTolgee';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type NodeLock = {
|
|
2
|
+
locked?: boolean;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type TolgeeElement = Element &
|
|
6
|
+
ElementCSSInlineStyle & {
|
|
7
|
+
_tolgee?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
import type { BackendGetRecordProps, TreeTranslationsData } from '@tolgee/core';
|
|
11
|
+
|
|
12
|
+
export type BackendOptions = Omit<RequestInit, 'headers'> & {
|
|
13
|
+
/**
|
|
14
|
+
* Path prefix (default: '/i18n')
|
|
15
|
+
*/
|
|
16
|
+
prefix: string;
|
|
17
|
+
/**
|
|
18
|
+
* Generate own path based on namespace, language and prefix
|
|
19
|
+
*/
|
|
20
|
+
getPath: GetPath;
|
|
21
|
+
/**
|
|
22
|
+
* Custom headers
|
|
23
|
+
*/
|
|
24
|
+
headers: Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Extract data from fetch response (default: (r) => r.json())
|
|
27
|
+
*/
|
|
28
|
+
getData: (r: Response) => Promise<TreeTranslationsData | undefined>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type GetPath = (
|
|
32
|
+
options: BackendGetRecordProps & { prefix: string }
|
|
33
|
+
) => string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { UI } from '../index';
|
|
3
|
+
import { sleep } from '../tools/sleep';
|
|
4
|
+
import { screen } from '@testing-library/dom';
|
|
5
|
+
import { DEVTOOLS_ID } from '../../constants';
|
|
6
|
+
|
|
7
|
+
function getShadowRootElement(testId: string, textContent: string) {
|
|
8
|
+
const shadowRoot = document.getElementById(DEVTOOLS_ID)!.shadowRoot!;
|
|
9
|
+
|
|
10
|
+
return Array.from(
|
|
11
|
+
shadowRoot.querySelectorAll(`*[data-testid="${testId}"]`)
|
|
12
|
+
).find((el) => el.textContent === textContent);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('it selects the key', async () => {
|
|
16
|
+
const ui = new UI({
|
|
17
|
+
apiKey: 'test',
|
|
18
|
+
apiUrl: 'test',
|
|
19
|
+
highlight: () => ({ unhighlight: () => {} }),
|
|
20
|
+
changeTranslation: () => ({ revert: () => {} }),
|
|
21
|
+
});
|
|
22
|
+
const mouseEvent = new MouseEvent('click');
|
|
23
|
+
const keys = new Map([
|
|
24
|
+
['key 1', 'Key 1'],
|
|
25
|
+
['key 2', 'Key 2'],
|
|
26
|
+
]);
|
|
27
|
+
// open context menu and wait for select
|
|
28
|
+
const resultPromise = ui.getKey({ openEvent: mouseEvent, keys: keys });
|
|
29
|
+
|
|
30
|
+
await sleep(10);
|
|
31
|
+
const shadowRoot = document.getElementById(DEVTOOLS_ID)!.shadowRoot!;
|
|
32
|
+
|
|
33
|
+
Array.from(shadowRoot.children).forEach((child) => {
|
|
34
|
+
screen.debug(child);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
keys.forEach((translation, key) => {
|
|
38
|
+
expect(
|
|
39
|
+
getShadowRootElement('key_context_menu_translation', translation)
|
|
40
|
+
).toBeTruthy();
|
|
41
|
+
expect(getShadowRootElement('key_context_menu_key', key)).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
getShadowRootElement(
|
|
45
|
+
'key_context_menu_translation',
|
|
46
|
+
'Key 2'
|
|
47
|
+
)!.parentElement!.click();
|
|
48
|
+
|
|
49
|
+
const result = await resultPromise;
|
|
50
|
+
expect(result).toEqual('key 2');
|
|
51
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Menu from '@mui/material/Menu';
|
|
3
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
4
|
+
import { styled } from '@mui/material/styles';
|
|
5
|
+
|
|
6
|
+
import { DEVTOOLS_ID, DEVTOOLS_Z_INDEX } from '../../constants';
|
|
7
|
+
import { ThemeProvider } from '../ThemeProvider';
|
|
8
|
+
|
|
9
|
+
const ScMenuItem = styled(MenuItem)`
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
height: 50px;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: flex-start;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const ScTranslation = styled('div')`
|
|
18
|
+
display: flex;
|
|
19
|
+
padding: 3px;
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const ScKey = styled('div')`
|
|
23
|
+
display: flex;
|
|
24
|
+
margin-top: -5px;
|
|
25
|
+
padding: 3px;
|
|
26
|
+
font-weight: bold;
|
|
27
|
+
font-size: 12px;
|
|
28
|
+
font-family: Monospace, 'Courier New', Courier;
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export interface KeyContextMenuParams {
|
|
32
|
+
openEvent: MouseEvent;
|
|
33
|
+
keys: Map<string, string | undefined>;
|
|
34
|
+
onSelect: (key: string | undefined) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type KeyContextMenuState = Partial<KeyContextMenuParams> & {
|
|
38
|
+
opened: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class KeyContextMenu extends React.Component {
|
|
42
|
+
state: KeyContextMenuState & { opened: boolean } = {
|
|
43
|
+
opened: false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async show(params: KeyContextMenuParams) {
|
|
47
|
+
this.setState({ ...params, opened: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
keyDown = (e: KeyboardEvent) => {
|
|
51
|
+
if (e.key === 'Escape') {
|
|
52
|
+
this.setState((s) => ({ ...s, opened: false }));
|
|
53
|
+
this.state.onSelect && this.state.onSelect(undefined);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
componentDidMount() {
|
|
58
|
+
document.addEventListener('keydown', this.keyDown);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
componentWillUnmount() {
|
|
62
|
+
document.removeEventListener('keydown', this.keyDown);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render() {
|
|
66
|
+
return (
|
|
67
|
+
<ThemeProvider>
|
|
68
|
+
{this.state.opened && (
|
|
69
|
+
<Menu
|
|
70
|
+
disablePortal
|
|
71
|
+
disableEnforceFocus
|
|
72
|
+
anchorEl={this.state.openEvent?.target as Element}
|
|
73
|
+
anchorOrigin={{
|
|
74
|
+
vertical: 'bottom',
|
|
75
|
+
horizontal: 'center',
|
|
76
|
+
}}
|
|
77
|
+
open
|
|
78
|
+
onClose={() => {
|
|
79
|
+
this.setState({ opened: false });
|
|
80
|
+
this.state.onSelect?.(undefined);
|
|
81
|
+
}}
|
|
82
|
+
container={document.getElementById(DEVTOOLS_ID)}
|
|
83
|
+
style={{ zIndex: DEVTOOLS_Z_INDEX }}
|
|
84
|
+
>
|
|
85
|
+
{Array.from(this.state.keys || []).map(
|
|
86
|
+
([key, translation], index) => (
|
|
87
|
+
<ScMenuItem
|
|
88
|
+
onClick={() => {
|
|
89
|
+
this.state.onSelect?.(key);
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
this.setState({ opened: false });
|
|
92
|
+
});
|
|
93
|
+
}}
|
|
94
|
+
key={index}
|
|
95
|
+
>
|
|
96
|
+
<ScTranslation data-testid="key_context_menu_translation">
|
|
97
|
+
{translation}
|
|
98
|
+
</ScTranslation>
|
|
99
|
+
<ScKey data-testid="key_context_menu_key">{key}</ScKey>
|
|
100
|
+
</ScMenuItem>
|
|
101
|
+
)
|
|
102
|
+
)}
|
|
103
|
+
</Menu>
|
|
104
|
+
)}
|
|
105
|
+
</ThemeProvider>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { BodyEnd } from '../common/BodyEnd';
|
|
3
|
+
import { DialogProvider } from './TranslationDialogContextProvider';
|
|
4
|
+
import { TranslationDialog } from './TranslationDialog';
|
|
5
|
+
import type { FallbackNSTranslation, UiProps } from '@tolgee/core';
|
|
6
|
+
import { QueryProvider } from '../../ui/client/QueryProvider';
|
|
7
|
+
|
|
8
|
+
export type ComponentDependencies = UiProps;
|
|
9
|
+
|
|
10
|
+
export type Props = UiProps;
|
|
11
|
+
|
|
12
|
+
type State = {
|
|
13
|
+
key: null | string;
|
|
14
|
+
defaultValue: undefined | string;
|
|
15
|
+
dialogOpened: boolean;
|
|
16
|
+
ns: FallbackNSTranslation;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class KeyDialog extends React.Component<Props, State> {
|
|
20
|
+
state = {
|
|
21
|
+
key: null,
|
|
22
|
+
defaultValue: undefined,
|
|
23
|
+
dialogOpened: false,
|
|
24
|
+
ns: undefined,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
constructor(props: Props) {
|
|
28
|
+
super(props);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public translationEdit(
|
|
32
|
+
key: string,
|
|
33
|
+
defaultValue: string | undefined,
|
|
34
|
+
ns: FallbackNSTranslation
|
|
35
|
+
) {
|
|
36
|
+
this.setState({
|
|
37
|
+
...this.state,
|
|
38
|
+
dialogOpened: true,
|
|
39
|
+
defaultValue: defaultValue,
|
|
40
|
+
key,
|
|
41
|
+
ns,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public render = () => (
|
|
46
|
+
<BodyEnd>
|
|
47
|
+
<QueryProvider apiUrl={this.props.apiUrl} apiKey={this.props.apiKey}>
|
|
48
|
+
{this.state.dialogOpened && (
|
|
49
|
+
<DialogProvider
|
|
50
|
+
uiProps={this.props}
|
|
51
|
+
defaultValue={this.state.defaultValue || ''}
|
|
52
|
+
open={this.state.dialogOpened}
|
|
53
|
+
keyName={this.state.key!}
|
|
54
|
+
ns={this.state.ns}
|
|
55
|
+
onClose={this.onClose}
|
|
56
|
+
>
|
|
57
|
+
<TranslationDialog />
|
|
58
|
+
</DialogProvider>
|
|
59
|
+
)}
|
|
60
|
+
</QueryProvider>
|
|
61
|
+
</BodyEnd>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
private onClose = () => {
|
|
65
|
+
this.setState({ ...this.state, dialogOpened: false });
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RESTRICTED_ASCENDANT_ATTRIBUTE } from '@tolgee/core';
|
|
3
|
+
|
|
4
|
+
import IconButton from '@mui/material/IconButton';
|
|
5
|
+
import Button from '@mui/material/Button';
|
|
6
|
+
import { styled } from '@mui/material/styles';
|
|
7
|
+
import OpenInNew from '@mui/icons-material/OpenInNew';
|
|
8
|
+
|
|
9
|
+
import { TranslationFields } from './TranslationFields';
|
|
10
|
+
import { LanguageSelect } from './LanguageSelect';
|
|
11
|
+
import { LoadingButton } from '../common/LoadingButton';
|
|
12
|
+
import { ScreenshotGallery } from './ScreenshotGallery/ScreenshotGallery';
|
|
13
|
+
import { ScFieldTitle } from '../common/FieldTitle';
|
|
14
|
+
import {
|
|
15
|
+
useDialogContext,
|
|
16
|
+
useDialogDispatch,
|
|
17
|
+
} from './TranslationDialogContextProvider';
|
|
18
|
+
import { isAuthorizedTo } from './ScreenshotGallery/utils';
|
|
19
|
+
import { NsSelect } from './NsSelect';
|
|
20
|
+
|
|
21
|
+
const ScContainer = styled('div')`
|
|
22
|
+
font-family: Rubik, Roboto, Arial;
|
|
23
|
+
padding: 20px;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
max-width: 100%;
|
|
26
|
+
width: 700px;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const ScHeading = styled('div')`
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 5px;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const ScHeadingTitle = styled('div')`
|
|
38
|
+
display: flex;
|
|
39
|
+
margin: 0px;
|
|
40
|
+
font-size: 19px;
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const ScHeadingRight = styled('div')`
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: flex-end;
|
|
46
|
+
align-items: center;
|
|
47
|
+
flex-grow: 1;
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const ScKey = styled('p')`
|
|
51
|
+
margin: 0px;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const ScKeyHint = styled('span')`
|
|
55
|
+
color: grey;
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const ScFieldsWrapper = styled('div')`
|
|
59
|
+
margin-top: 20px;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const ScGalleryWrapper = styled('div')`
|
|
63
|
+
margin-top: 10px;
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const ScControls = styled('div')`
|
|
67
|
+
display: flex;
|
|
68
|
+
justify-content: flex-end;
|
|
69
|
+
margin-top: 20px;
|
|
70
|
+
min-height: 36px;
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const ScRestriction = styled('div')`
|
|
74
|
+
margin-top: 8px;
|
|
75
|
+
color: ${({ theme }) => theme.palette.text.secondary};
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const ScError = styled('div')`
|
|
79
|
+
color: red;
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
export const KeyForm = () => {
|
|
83
|
+
const dispatch = useDialogDispatch();
|
|
84
|
+
|
|
85
|
+
const linkToPlatform = useDialogContext((c) => c.linkToPlatform);
|
|
86
|
+
const useBrowserWindow = useDialogContext((c) => c.useBrowserWindow);
|
|
87
|
+
const input = useDialogContext((c) => c.input);
|
|
88
|
+
const translations = useDialogContext((c) => c.translations);
|
|
89
|
+
const formDisabled = useDialogContext((c) => c.formDisabled);
|
|
90
|
+
const loading = useDialogContext((c) => c.loading);
|
|
91
|
+
const error = useDialogContext((c) => c.error);
|
|
92
|
+
const saving = useDialogContext((c) => c.saving);
|
|
93
|
+
const success = useDialogContext((c) => c.success);
|
|
94
|
+
const keyExists = useDialogContext((c) => c.keyExists);
|
|
95
|
+
const scopes = useDialogContext((c) => c.scopes);
|
|
96
|
+
const ns = useDialogContext((c) => c.ns);
|
|
97
|
+
const selectedNs = useDialogContext((c) => c.selectedNs);
|
|
98
|
+
|
|
99
|
+
const screenshotsView = isAuthorizedTo('screenshots.view', scopes);
|
|
100
|
+
|
|
101
|
+
const setUseBrowserWindow = (value: boolean) => {
|
|
102
|
+
dispatch({ type: 'SET_USE_BROWSER_WINDOW', payload: value });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onClose = () => {
|
|
106
|
+
dispatch({ type: 'ON_CLOSE' });
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onSave = () => {
|
|
110
|
+
dispatch({ type: 'ON_SAVE' });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const onNamespaceChange = (ns: string) => {
|
|
114
|
+
dispatch({ type: 'SELECTED_NS_CHANGE', payload: { ns } });
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const ready = !loading && !error;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ScContainer {...{ [RESTRICTED_ASCENDANT_ATTRIBUTE]: 'true' }}>
|
|
121
|
+
<ScHeading>
|
|
122
|
+
<a
|
|
123
|
+
href={linkToPlatform}
|
|
124
|
+
target="_blank"
|
|
125
|
+
rel="noreferrer noopener"
|
|
126
|
+
id="_tolgee_platform_link"
|
|
127
|
+
>
|
|
128
|
+
<svg
|
|
129
|
+
viewBox="0 0 200 200"
|
|
130
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
131
|
+
opacity="0.99"
|
|
132
|
+
fill="#822B55"
|
|
133
|
+
style={{
|
|
134
|
+
fillRule: 'evenodd',
|
|
135
|
+
clipRule: 'evenodd',
|
|
136
|
+
strokeLinejoin: 'round',
|
|
137
|
+
strokeMiterlimit: 2,
|
|
138
|
+
height: 23,
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<path d="M97.16,7.27a16.94,16.94,0,0,0-1.9,24.47,16.36,16.36,0,0,0,5,3.83,3.23,3.23,0,0,1-2.9,5.77,23.14,23.14,0,0,1-11.41-13C73.83,31.1,63.46,37.09,52.82,46.51c-27.44,24.3-34.35,61.74-16.38,85.26-4.57,5.79-8,12.22-8.9,18.69a20.88,20.88,0,0,0,5.62,18c9.18,9.61,21.42,7.13,31.26,5.14,6.58-1.34,12.8-2.6,16.5-.23,3.22,2.07,3.47,3.87,3.61,4.45,2.1,9.32-5.79,13.89-7.67,16.27a1.48,1.48,0,0,0,1.13,2.4c3.48,0,9-1.18,12.34-4.08s7.16-7.9,5.89-16.32c-.08-.5-.18-1-.32-1.58-.86-3.35-3.1-7.57-8.61-11.09-7.72-4.95-17-3.07-25.22-1.41-9.76,2-16,2.85-20.37-1.71a9.13,9.13,0,0,1-2.46-8.19c.54-3.77,2.65-7.89,5.62-11.86,21.71,16.89,56.87,13.47,82.67-9.39a75.34,75.34,0,0,0,20.81-28.09A23.14,23.14,0,0,1,134.8,89a3.23,3.23,0,0,1,6.08-2.19,16.37,16.37,0,0,0,3.2,5.39,16.85,16.85,0,1,0,11.48-28,3.23,3.23,0,0,1-.51-6.44,23.41,23.41,0,0,1,12.88,2.69c2.6-14.08,3.34-31.41-2.06-37.51-4.08-4.61-20.62-8-35.18-7.76A23.48,23.48,0,0,1,130.8,25a3.23,3.23,0,0,1-6.33-1.28A16.94,16.94,0,0,0,97.16,7.27Zm63.25,21a5.29,5.29,0,0,1-.57,6.19c-1.29,1.14-2.72-.51-4.1-2.06s-3.1-3.42-1.81-4.56A5.74,5.74,0,0,1,160.41,28.27Z"></path>
|
|
142
|
+
</svg>
|
|
143
|
+
</a>
|
|
144
|
+
|
|
145
|
+
<ScHeadingTitle>Quick translation</ScHeadingTitle>
|
|
146
|
+
|
|
147
|
+
{!useBrowserWindow && (
|
|
148
|
+
<IconButton
|
|
149
|
+
title="Open in new window"
|
|
150
|
+
onClick={() => setUseBrowserWindow(true)}
|
|
151
|
+
color="inherit"
|
|
152
|
+
size="small"
|
|
153
|
+
>
|
|
154
|
+
<OpenInNew fontSize="small" />
|
|
155
|
+
</IconButton>
|
|
156
|
+
)}
|
|
157
|
+
<ScHeadingRight>{!loading && <LanguageSelect />}</ScHeadingRight>
|
|
158
|
+
</ScHeading>
|
|
159
|
+
|
|
160
|
+
<ScFieldTitle>Key</ScFieldTitle>
|
|
161
|
+
<ScKey>
|
|
162
|
+
{input}
|
|
163
|
+
<ScKeyHint>
|
|
164
|
+
{!keyExists && ready && " (key doesn't exist yet)"}
|
|
165
|
+
</ScKeyHint>
|
|
166
|
+
</ScKey>
|
|
167
|
+
|
|
168
|
+
<NsSelect options={ns} value={selectedNs} onChange={onNamespaceChange} />
|
|
169
|
+
|
|
170
|
+
<ScFieldsWrapper>
|
|
171
|
+
<TranslationFields />
|
|
172
|
+
</ScFieldsWrapper>
|
|
173
|
+
|
|
174
|
+
{screenshotsView && ready && (
|
|
175
|
+
<ScGalleryWrapper>
|
|
176
|
+
<ScreenshotGallery />
|
|
177
|
+
</ScGalleryWrapper>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{formDisabled && ready && (
|
|
181
|
+
<ScRestriction>{`Modification is restricted due to missing ${
|
|
182
|
+
translations?.keyId !== undefined ? 'translations.edit' : 'keys.edit'
|
|
183
|
+
} scope in current api key settings.`}</ScRestriction>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{error && <ScError>{error}</ScError>}
|
|
187
|
+
<ScControls>
|
|
188
|
+
<Button onClick={onClose} color="secondary">
|
|
189
|
+
{useBrowserWindow ? 'Close' : 'Cancel'}
|
|
190
|
+
</Button>
|
|
191
|
+
<LoadingButton
|
|
192
|
+
loading={saving}
|
|
193
|
+
disabled={saving || formDisabled}
|
|
194
|
+
onClick={onSave}
|
|
195
|
+
color="primary"
|
|
196
|
+
variant="contained"
|
|
197
|
+
style={{ marginLeft: '10px' }}
|
|
198
|
+
>
|
|
199
|
+
{success
|
|
200
|
+
? 'Saved! ✓'
|
|
201
|
+
: translations?.keyId === undefined
|
|
202
|
+
? 'Create'
|
|
203
|
+
: 'Update'}
|
|
204
|
+
</LoadingButton>
|
|
205
|
+
</ScControls>
|
|
206
|
+
</ScContainer>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
|
4
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
5
|
+
import Checkbox from '@mui/material/Checkbox';
|
|
6
|
+
import ListItemText from '@mui/material/ListItemText';
|
|
7
|
+
import OutlinedInput from '@mui/material/OutlinedInput';
|
|
8
|
+
import FormControl from '@mui/material/FormControl';
|
|
9
|
+
import { styled } from '@mui/material/styles';
|
|
10
|
+
|
|
11
|
+
import { DEVTOOLS_Z_INDEX } from '../../constants';
|
|
12
|
+
import {
|
|
13
|
+
useDialogContext,
|
|
14
|
+
useDialogDispatch,
|
|
15
|
+
} from './TranslationDialogContextProvider';
|
|
16
|
+
|
|
17
|
+
const ScFormControl = styled(FormControl)`
|
|
18
|
+
min-width: 200px;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export const LanguageSelect: React.FC = () => {
|
|
22
|
+
const dispatch = useDialogDispatch();
|
|
23
|
+
const availableLanguages = useDialogContext((c) => c.availableLanguages);
|
|
24
|
+
const selectedLanguages = useDialogContext((c) => c.selectedLanguages);
|
|
25
|
+
|
|
26
|
+
const options = availableLanguages
|
|
27
|
+
? [...availableLanguages].map((lang) => ({
|
|
28
|
+
label: lang.name,
|
|
29
|
+
value: lang.tag,
|
|
30
|
+
}))
|
|
31
|
+
: [];
|
|
32
|
+
|
|
33
|
+
const selected = options.filter((o) => selectedLanguages.includes(o.value));
|
|
34
|
+
const onChange = (e: SelectChangeEvent<string[]>) => {
|
|
35
|
+
const value = e.target.value;
|
|
36
|
+
const languages = typeof value === 'string' ? value.split(',') : value;
|
|
37
|
+
dispatch({
|
|
38
|
+
type: 'ON_SELECTED_LANGUAGES_CHANGE',
|
|
39
|
+
payload: { languages },
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{availableLanguages && (
|
|
46
|
+
<ScFormControl
|
|
47
|
+
variant="outlined"
|
|
48
|
+
size="small"
|
|
49
|
+
style={{ maxWidth: 250 }}
|
|
50
|
+
>
|
|
51
|
+
<Select
|
|
52
|
+
multiple
|
|
53
|
+
value={selected.map((o) => o.value)}
|
|
54
|
+
onChange={(value) => onChange(value)}
|
|
55
|
+
input={<OutlinedInput />}
|
|
56
|
+
renderValue={(selected) => selected.join(', ')}
|
|
57
|
+
MenuProps={{
|
|
58
|
+
style: { zIndex: DEVTOOLS_Z_INDEX },
|
|
59
|
+
disablePortal: true,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{options.map((option) => (
|
|
63
|
+
<MenuItem key={option.value} value={option.value} dense>
|
|
64
|
+
<Checkbox
|
|
65
|
+
size="small"
|
|
66
|
+
checked={Boolean(
|
|
67
|
+
selected.find((o) => o.value === option.value)
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
<ListItemText>{option.label}</ListItemText>
|
|
71
|
+
</MenuItem>
|
|
72
|
+
))}
|
|
73
|
+
</Select>
|
|
74
|
+
</ScFormControl>
|
|
75
|
+
)}
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
};
|