intlayer-editor 2.0.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 +175 -0
- package/bin/start-server.js +16 -0
- package/dist/cjs/client/ContentEditionLayout.cjs +46 -0
- package/dist/cjs/client/ContentEditionLayout.cjs.map +1 -0
- package/dist/cjs/client/ContentEditionLayout.d.ts +12 -0
- package/dist/cjs/client/ContentSelectorWrapper.cjs +49 -0
- package/dist/cjs/client/ContentSelectorWrapper.cjs.map +1 -0
- package/dist/cjs/client/ContentSelectorWrapper.d.ts +12 -0
- package/dist/cjs/client/EditionPanel/EditionPanel.cjs +101 -0
- package/dist/cjs/client/EditionPanel/EditionPanel.cjs.map +1 -0
- package/dist/cjs/client/EditionPanel/EditionPanel.d.ts +11 -0
- package/dist/cjs/client/EditionPanel/index.cjs +27 -0
- package/dist/cjs/client/EditionPanel/index.cjs.map +1 -0
- package/dist/cjs/client/EditionPanel/index.d.ts +9 -0
- package/dist/cjs/client/EditionPanel/useEditedContentStore.cjs +90 -0
- package/dist/cjs/client/EditionPanel/useEditedContentStore.cjs.map +1 -0
- package/dist/cjs/client/EditionPanel/useEditedContentStore.d.ts +28 -0
- package/dist/cjs/client/EditionPanel/useEditionPanelStore.cjs +33 -0
- package/dist/cjs/client/EditionPanel/useEditionPanelStore.cjs.map +1 -0
- package/dist/cjs/client/EditionPanel/useEditionPanelStore.d.ts +16 -0
- package/dist/cjs/client/index.cjs +29 -0
- package/dist/cjs/client/index.cjs.map +1 -0
- package/dist/cjs/client/index.d.ts +13 -0
- package/dist/cjs/client/renderContentSelector.cjs +39 -0
- package/dist/cjs/client/renderContentSelector.cjs.map +1 -0
- package/dist/cjs/client/renderContentSelector.d.ts +6 -0
- package/dist/cjs/client/useEditorServer.cjs +53 -0
- package/dist/cjs/client/useEditorServer.cjs.map +1 -0
- package/dist/cjs/client/useEditorServer.d.ts +5 -0
- package/dist/cjs/server/content-editor.cjs +156 -0
- package/dist/cjs/server/content-editor.cjs.map +1 -0
- package/dist/cjs/server/content-editor.d.ts +12 -0
- package/dist/cjs/server/index.cjs +68 -0
- package/dist/cjs/server/index.cjs.map +1 -0
- package/dist/cjs/server/index.d.ts +3 -0
- package/dist/esm/client/ContentEditionLayout.d.mts +12 -0
- package/dist/esm/client/ContentEditionLayout.mjs +22 -0
- package/dist/esm/client/ContentEditionLayout.mjs.map +1 -0
- package/dist/esm/client/ContentSelectorWrapper.d.mts +12 -0
- package/dist/esm/client/ContentSelectorWrapper.mjs +25 -0
- package/dist/esm/client/ContentSelectorWrapper.mjs.map +1 -0
- package/dist/esm/client/EditionPanel/EditionPanel.d.mts +11 -0
- package/dist/esm/client/EditionPanel/EditionPanel.mjs +72 -0
- package/dist/esm/client/EditionPanel/EditionPanel.mjs.map +1 -0
- package/dist/esm/client/EditionPanel/index.d.mts +9 -0
- package/dist/esm/client/EditionPanel/index.mjs +4 -0
- package/dist/esm/client/EditionPanel/index.mjs.map +1 -0
- package/dist/esm/client/EditionPanel/useEditedContentStore.d.mts +28 -0
- package/dist/esm/client/EditionPanel/useEditedContentStore.mjs +66 -0
- package/dist/esm/client/EditionPanel/useEditedContentStore.mjs.map +1 -0
- package/dist/esm/client/EditionPanel/useEditionPanelStore.d.mts +16 -0
- package/dist/esm/client/EditionPanel/useEditionPanelStore.mjs +9 -0
- package/dist/esm/client/EditionPanel/useEditionPanelStore.mjs.map +1 -0
- package/dist/esm/client/index.d.mts +13 -0
- package/dist/esm/client/index.mjs +5 -0
- package/dist/esm/client/index.mjs.map +1 -0
- package/dist/esm/client/renderContentSelector.d.mts +6 -0
- package/dist/esm/client/renderContentSelector.mjs +15 -0
- package/dist/esm/client/renderContentSelector.mjs.map +1 -0
- package/dist/esm/client/useEditorServer.d.mts +5 -0
- package/dist/esm/client/useEditorServer.mjs +29 -0
- package/dist/esm/client/useEditorServer.mjs.map +1 -0
- package/dist/esm/server/content-editor.d.mts +12 -0
- package/dist/esm/server/content-editor.mjs +123 -0
- package/dist/esm/server/content-editor.mjs.map +1 -0
- package/dist/esm/server/index.d.mts +3 -0
- package/dist/esm/server/index.mjs +34 -0
- package/dist/esm/server/index.mjs.map +1 -0
- package/package.json +109 -0
- package/src/client/ContentEditionLayout.tsx +26 -0
- package/src/client/ContentSelectorWrapper.tsx +38 -0
- package/src/client/EditionPanel/EditionPanel.tsx +90 -0
- package/src/client/EditionPanel/index.ts +3 -0
- package/src/client/EditionPanel/useEditedContentStore.ts +98 -0
- package/src/client/EditionPanel/useEditionPanelStore.ts +19 -0
- package/src/client/index.ts +4 -0
- package/src/client/renderContentSelector.tsx +17 -0
- package/src/client/useEditorServer.ts +31 -0
- package/src/server/content-editor.ts +209 -0
- package/src/server/index.ts +40 -0
package/package.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "intlayer-editor",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "IntLayer Editor is a tool that allow you to edit your IntLayer declaration files using a graphical interface.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"intlayer",
|
|
8
|
+
"application",
|
|
9
|
+
"editor",
|
|
10
|
+
"typescript",
|
|
11
|
+
"javascript",
|
|
12
|
+
"json",
|
|
13
|
+
"file"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/aypineau/intlayer",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/aypineau/intlayer.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Aymeric PINEAU",
|
|
23
|
+
"url": "https://github.com/aypineau"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
"./client": {
|
|
27
|
+
"types": "./dist/esm/client/index.d.mts",
|
|
28
|
+
"require": "./dist/cjs/client/index.cjs",
|
|
29
|
+
"import": "./dist/esm/client/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./server": {
|
|
32
|
+
"types": "./dist/esm/server/index.d.mts",
|
|
33
|
+
"require": "./dist/cjs/server/index.cjs",
|
|
34
|
+
"import": "./dist/esm/server/index.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"typesVersions": {
|
|
39
|
+
"*": {
|
|
40
|
+
"package.json": [
|
|
41
|
+
"./package.json"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"bin": {
|
|
46
|
+
"intlayer-editor": "./bin/start-server.js"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"./dist",
|
|
50
|
+
"./src",
|
|
51
|
+
"./bin",
|
|
52
|
+
"./package.json"
|
|
53
|
+
],
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@types/body-parser": "^1.19.5",
|
|
56
|
+
"body-parser": "^1.20.2",
|
|
57
|
+
"commander": "^12.0.0",
|
|
58
|
+
"express": "^4.19.2",
|
|
59
|
+
"lucide-react": "^0.376.0",
|
|
60
|
+
"magic-regexp": "^0.8.0",
|
|
61
|
+
"react": "^18.2.0",
|
|
62
|
+
"react-dom": "^18.2.0",
|
|
63
|
+
"webpack": "^5.91.0",
|
|
64
|
+
"zustand": "^4.5.2",
|
|
65
|
+
"@intlayer/config": "^2.0.0",
|
|
66
|
+
"@intlayer/core": "^2.0.0",
|
|
67
|
+
"@intlayer/design-system": "^2.0.0",
|
|
68
|
+
"@intlayer/dictionaries-entry": "^2.0.0",
|
|
69
|
+
"intlayer": "^2.0.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@babel/generator": "7.24.4",
|
|
73
|
+
"@babel/parser": "7.24.4",
|
|
74
|
+
"@babel/types": "7.24.0",
|
|
75
|
+
"@changesets/changelog-github": "0.5.0",
|
|
76
|
+
"@changesets/cli": "2.27.1",
|
|
77
|
+
"@types/babel__generator": "^7.6.8",
|
|
78
|
+
"@types/express": "^4.17.21",
|
|
79
|
+
"@types/node": "^20.12.7",
|
|
80
|
+
"@types/react": "^18.2.79",
|
|
81
|
+
"@types/react-dom": "^18.2.25",
|
|
82
|
+
"rimraf": "5.0.5",
|
|
83
|
+
"ts-node": "^10.9.2",
|
|
84
|
+
"tsup": "^8.0.2",
|
|
85
|
+
"typescript": "^5.4.5",
|
|
86
|
+
"@utils/eslint-config": "^1.0.1",
|
|
87
|
+
"@utils/ts-config": "^1.0.1"
|
|
88
|
+
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=14.18"
|
|
91
|
+
},
|
|
92
|
+
"bug": {
|
|
93
|
+
"url": "https://github.com/aypineau/intlayer/issues"
|
|
94
|
+
},
|
|
95
|
+
"scripts": {
|
|
96
|
+
"build": "tsup",
|
|
97
|
+
"clean": "rimraf ./dist",
|
|
98
|
+
"dev": "tsup --watch",
|
|
99
|
+
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs",
|
|
100
|
+
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs --fix",
|
|
101
|
+
"prettier": "prettier --check src",
|
|
102
|
+
"prettier:fix": "prettier --write src",
|
|
103
|
+
"serve": "webpack serve --config ./webpack.config.ts",
|
|
104
|
+
"start:dev": "node --experimental-specifier-resolution=node --loader ts-node/esm --watch src/index.ts",
|
|
105
|
+
"transpile": "webpack --config ./webpack.config.ts",
|
|
106
|
+
"typecheck": "tsup--project ./tsconfig.json --noEmit",
|
|
107
|
+
"watch": "webpack --config ./webpack.config.ts --watch"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Locales } from '@intlayer/config/client';
|
|
2
|
+
import type { FC, ReactNode } from 'react';
|
|
3
|
+
import { EditionPanel } from './EditionPanel/index';
|
|
4
|
+
|
|
5
|
+
export type ContentEditionLayoutProps = {
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
locale: Locales;
|
|
8
|
+
localeList: Locales[];
|
|
9
|
+
setLocale: (locale: Locales) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ContentEditionLayout: FC<ContentEditionLayoutProps> = ({
|
|
13
|
+
children,
|
|
14
|
+
locale,
|
|
15
|
+
setLocale,
|
|
16
|
+
localeList,
|
|
17
|
+
}) => (
|
|
18
|
+
<>
|
|
19
|
+
{children}
|
|
20
|
+
<EditionPanel
|
|
21
|
+
locale={locale}
|
|
22
|
+
localeList={localeList}
|
|
23
|
+
setLocale={setLocale}
|
|
24
|
+
/>
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { KeyPath } from '@intlayer/core';
|
|
4
|
+
import { ContentSelector } from '@intlayer/design-system';
|
|
5
|
+
import type { FC } from 'react';
|
|
6
|
+
import { useEditedContentStore } from './EditionPanel/useEditedContentStore';
|
|
7
|
+
import { useEditionPanelStore } from './EditionPanel/useEditionPanelStore';
|
|
8
|
+
|
|
9
|
+
type ContentSelectorWrapperProps = {
|
|
10
|
+
children: string;
|
|
11
|
+
dictionaryId: string;
|
|
12
|
+
dictionaryPath: string;
|
|
13
|
+
keyPath: KeyPath[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ContentSelectorWrapper: FC<ContentSelectorWrapperProps> = ({
|
|
17
|
+
children,
|
|
18
|
+
dictionaryId,
|
|
19
|
+
dictionaryPath,
|
|
20
|
+
keyPath,
|
|
21
|
+
}) => {
|
|
22
|
+
const { setFocusedContent } = useEditionPanelStore();
|
|
23
|
+
const handleSelect = () =>
|
|
24
|
+
setFocusedContent({
|
|
25
|
+
dictionaryId,
|
|
26
|
+
dictionaryPath,
|
|
27
|
+
keyPath,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { getEditedContentValue } = useEditedContentStore();
|
|
31
|
+
const editedValue = getEditedContentValue(dictionaryPath, keyPath);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ContentSelector onSelect={handleSelect}>
|
|
35
|
+
{editedValue ?? children}
|
|
36
|
+
</ContentSelector>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Locales } from '@intlayer/config/client';
|
|
4
|
+
import type { ContentModule } from '@intlayer/core';
|
|
5
|
+
import {
|
|
6
|
+
useRightDrawerStore,
|
|
7
|
+
RightDrawer,
|
|
8
|
+
DictionaryEditor,
|
|
9
|
+
LocaleSwitcher,
|
|
10
|
+
} from '@intlayer/design-system';
|
|
11
|
+
/**
|
|
12
|
+
* @intlayer/dictionaries-entry is a package that only returns the dictionary entry path.
|
|
13
|
+
* Using an external package allow to alias it in the bundle configuration (such as webpack).
|
|
14
|
+
* The alias allow hot reload the app (such as nextjs) on any dictionary change.
|
|
15
|
+
*/
|
|
16
|
+
import dictionaries from '@intlayer/dictionaries-entry';
|
|
17
|
+
import { type FC, useEffect } from 'react';
|
|
18
|
+
import { useEditorServer } from '../useEditorServer';
|
|
19
|
+
import { useEditedContentStore } from './useEditedContentStore';
|
|
20
|
+
import { useEditionPanelStore } from './useEditionPanelStore';
|
|
21
|
+
|
|
22
|
+
type EditionPanelProps = {
|
|
23
|
+
locale: Locales;
|
|
24
|
+
localeList: Locales[];
|
|
25
|
+
setLocale: (locale: Locales) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const EditionPanel: FC<EditionPanelProps> = ({
|
|
29
|
+
locale,
|
|
30
|
+
localeList,
|
|
31
|
+
setLocale,
|
|
32
|
+
}) => {
|
|
33
|
+
const { open } = useRightDrawerStore();
|
|
34
|
+
const { focusedContent, setFocusedContent } = useEditionPanelStore();
|
|
35
|
+
const { editedContent, addEditedContent, clearEditedDictionaryContent } =
|
|
36
|
+
useEditedContentStore();
|
|
37
|
+
const { editContentRequest } = useEditorServer();
|
|
38
|
+
|
|
39
|
+
// Use effect to react to changes in focusedContent
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (focusedContent !== null) {
|
|
42
|
+
open(); // Call the open function from useRightDrawerStore
|
|
43
|
+
}
|
|
44
|
+
}, [focusedContent, open]); // Depend on focusedContent and open to trigger the effect
|
|
45
|
+
|
|
46
|
+
if (!focusedContent) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dictionary: ContentModule = dictionaries[focusedContent.dictionaryId];
|
|
51
|
+
|
|
52
|
+
if (!dictionary?.filePath) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const dictionaryPath = dictionary.filePath;
|
|
57
|
+
const editedDictionaryContent = editedContent[dictionaryPath] ?? [];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<RightDrawer
|
|
61
|
+
title={dictionary.id}
|
|
62
|
+
label={`Edit dictionary ${dictionary.id}`}
|
|
63
|
+
header={
|
|
64
|
+
<LocaleSwitcher
|
|
65
|
+
setLocale={setLocale}
|
|
66
|
+
locale={locale}
|
|
67
|
+
localeList={localeList}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<DictionaryEditor
|
|
72
|
+
dictionary={dictionary}
|
|
73
|
+
locale={locale}
|
|
74
|
+
focusedKeyPath={focusedContent.keyPath}
|
|
75
|
+
editedContent={editedDictionaryContent}
|
|
76
|
+
onFocusKeyPath={(keyPath) =>
|
|
77
|
+
setFocusedContent({ ...focusedContent, keyPath })
|
|
78
|
+
}
|
|
79
|
+
onContentChange={(keyPath, newValue) =>
|
|
80
|
+
addEditedContent(dictionaryPath, keyPath, newValue)
|
|
81
|
+
}
|
|
82
|
+
onValidEdition={editContentRequest}
|
|
83
|
+
onCancelEdition={() => {
|
|
84
|
+
clearEditedDictionaryContent(dictionaryPath);
|
|
85
|
+
setFocusedContent(null);
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</RightDrawer>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { KeyPath } from '@intlayer/core';
|
|
2
|
+
import type { FileContent } from '@intlayer/design-system';
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
5
|
+
|
|
6
|
+
type DictionaryPath = string;
|
|
7
|
+
|
|
8
|
+
export type EditedContent = Record<DictionaryPath, FileContent[]>;
|
|
9
|
+
|
|
10
|
+
type EditedContentStore = {
|
|
11
|
+
editedContent: EditedContent;
|
|
12
|
+
addEditedContent: (
|
|
13
|
+
dictionaryPath: DictionaryPath,
|
|
14
|
+
keyPath: KeyPath[],
|
|
15
|
+
newValue: string
|
|
16
|
+
) => void;
|
|
17
|
+
removeEditedContent: (
|
|
18
|
+
dictionaryPath: DictionaryPath,
|
|
19
|
+
keyPath: KeyPath[]
|
|
20
|
+
) => void;
|
|
21
|
+
clearEditedDictionaryContent: (dictionaryPath: DictionaryPath) => void;
|
|
22
|
+
clearEditedContent: () => void;
|
|
23
|
+
getEditedContentValue: (
|
|
24
|
+
dictionaryPath: DictionaryPath,
|
|
25
|
+
keyPath: KeyPath[]
|
|
26
|
+
) => string | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const isSameKeyPath = (keyPath1: KeyPath[], keyPath2: KeyPath[]) =>
|
|
30
|
+
keyPath1.every(
|
|
31
|
+
(element, index) =>
|
|
32
|
+
keyPath2[index] &&
|
|
33
|
+
keyPath2[index].key === element.key &&
|
|
34
|
+
keyPath2[index].type === element.type
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const useEditedContentStore = create(
|
|
38
|
+
persist<EditedContentStore>(
|
|
39
|
+
(set, get) => ({
|
|
40
|
+
editedContent: {},
|
|
41
|
+
addEditedContent: (dictionaryPath, keyPath, newValue) => {
|
|
42
|
+
set((state) => {
|
|
43
|
+
const editedContent = state.editedContent[dictionaryPath] ?? [];
|
|
44
|
+
return {
|
|
45
|
+
editedContent: {
|
|
46
|
+
...state.editedContent,
|
|
47
|
+
[dictionaryPath]: [
|
|
48
|
+
...editedContent.filter(
|
|
49
|
+
(content) => !isSameKeyPath(content.keyPath, keyPath)
|
|
50
|
+
),
|
|
51
|
+
{
|
|
52
|
+
keyPath,
|
|
53
|
+
newValue,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
removeEditedContent: (dictionaryPath, keyPath) => {
|
|
62
|
+
set((state) => {
|
|
63
|
+
const editedContent = state.editedContent[dictionaryPath] ?? [];
|
|
64
|
+
return {
|
|
65
|
+
editedContent: {
|
|
66
|
+
...state.editedContent,
|
|
67
|
+
[dictionaryPath]: editedContent.filter(
|
|
68
|
+
(content) => content.keyPath !== keyPath
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
clearEditedDictionaryContent: (dictionaryPath) => {
|
|
76
|
+
set((state) => ({
|
|
77
|
+
editedContent: {
|
|
78
|
+
...state.editedContent,
|
|
79
|
+
[dictionaryPath]: [],
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
clearEditedContent: () => {
|
|
85
|
+
set({ editedContent: {} });
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
getEditedContentValue: (dictionaryPath, keyPath): string | undefined =>
|
|
89
|
+
get().editedContent[dictionaryPath]?.find((content) =>
|
|
90
|
+
isSameKeyPath(content.keyPath, keyPath)
|
|
91
|
+
)?.newValue,
|
|
92
|
+
}),
|
|
93
|
+
{
|
|
94
|
+
name: 'edited-content-store',
|
|
95
|
+
storage: createJSONStorage(() => sessionStorage),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { KeyPath } from '@intlayer/core';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
|
|
4
|
+
type DictionaryPath = string;
|
|
5
|
+
type FileContent = {
|
|
6
|
+
dictionaryPath: DictionaryPath;
|
|
7
|
+
dictionaryId: string;
|
|
8
|
+
keyPath: KeyPath[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type EditionPanelStore = {
|
|
12
|
+
focusedContent: FileContent | null;
|
|
13
|
+
setFocusedContent: (content: FileContent | null) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const useEditionPanelStore = create<EditionPanelStore>((set) => ({
|
|
17
|
+
focusedContent: null,
|
|
18
|
+
setFocusedContent: (content) => set({ focusedContent: content }),
|
|
19
|
+
}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { KeyPath } from '@intlayer/core';
|
|
2
|
+
import { ContentSelectorWrapper } from './ContentSelectorWrapper';
|
|
3
|
+
|
|
4
|
+
export const renderContentEditor = (
|
|
5
|
+
content: string,
|
|
6
|
+
dictionaryId: string,
|
|
7
|
+
dictionaryPath: string,
|
|
8
|
+
keyPath: KeyPath[]
|
|
9
|
+
) => (
|
|
10
|
+
<ContentSelectorWrapper
|
|
11
|
+
dictionaryId={dictionaryId}
|
|
12
|
+
dictionaryPath={dictionaryPath}
|
|
13
|
+
keyPath={keyPath}
|
|
14
|
+
>
|
|
15
|
+
{content}
|
|
16
|
+
</ContentSelectorWrapper>
|
|
17
|
+
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getConfiguration } from '@intlayer/config/client';
|
|
2
|
+
import { useEditedContentStore } from './EditionPanel/useEditedContentStore';
|
|
3
|
+
|
|
4
|
+
export const useEditorServer = () => {
|
|
5
|
+
const { editedContent, clearEditedContent } = useEditedContentStore();
|
|
6
|
+
|
|
7
|
+
const editContentRequest = async () => {
|
|
8
|
+
const {
|
|
9
|
+
editor: { port },
|
|
10
|
+
} = getConfiguration();
|
|
11
|
+
|
|
12
|
+
await fetch(`http://localhost:${port}`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify(editedContent),
|
|
18
|
+
})
|
|
19
|
+
.then((response) => {
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error('Failed to edit content');
|
|
22
|
+
}
|
|
23
|
+
clearEditedContent();
|
|
24
|
+
})
|
|
25
|
+
.catch((error) => {
|
|
26
|
+
console.error('Failed to edit content:', error);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return { editContentRequest };
|
|
31
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import type {
|
|
5
|
+
ObjectExpression,
|
|
6
|
+
VariableDeclarator,
|
|
7
|
+
Program,
|
|
8
|
+
Identifier,
|
|
9
|
+
StringLiteral,
|
|
10
|
+
AssignmentExpression,
|
|
11
|
+
ObjectProperty,
|
|
12
|
+
CallExpression,
|
|
13
|
+
ObjectMethod,
|
|
14
|
+
SpreadElement,
|
|
15
|
+
} from '@babel/types';
|
|
16
|
+
import {
|
|
17
|
+
NodeType,
|
|
18
|
+
type KeyPath,
|
|
19
|
+
type ObjectExpressionNode,
|
|
20
|
+
type TranslationOrEnumerationNode,
|
|
21
|
+
} from '@intlayer/core';
|
|
22
|
+
import prettier from 'prettier';
|
|
23
|
+
import type { EditedContent } from '../client/index';
|
|
24
|
+
|
|
25
|
+
const requireFunction =
|
|
26
|
+
typeof import.meta.url === 'undefined'
|
|
27
|
+
? require
|
|
28
|
+
: createRequire(import.meta.url);
|
|
29
|
+
|
|
30
|
+
const { default: generate } = requireFunction('@babel/generator');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Helper function to find a nested property in an ObjectExpression based on a key path
|
|
34
|
+
*/
|
|
35
|
+
const findNestedProperty = (
|
|
36
|
+
obj: ObjectExpression,
|
|
37
|
+
keyPath: KeyPath[]
|
|
38
|
+
): ObjectExpression | undefined => {
|
|
39
|
+
let currentObj = obj;
|
|
40
|
+
for (const key of keyPath) {
|
|
41
|
+
let foundProperty:
|
|
42
|
+
| ObjectProperty
|
|
43
|
+
| ObjectMethod
|
|
44
|
+
| SpreadElement
|
|
45
|
+
| undefined;
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
// if the keyPath is an object, select the related node
|
|
49
|
+
(key as ObjectExpressionNode).type === 'ObjectExpression'
|
|
50
|
+
) {
|
|
51
|
+
foundProperty = currentObj.properties.find(
|
|
52
|
+
(prop) =>
|
|
53
|
+
'key' in prop &&
|
|
54
|
+
(prop.key as Identifier).name === (key as ObjectExpressionNode).key
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
// if the keypath is a translation or enumeration node, go across the function and select the related node
|
|
60
|
+
Object.values(NodeType).includes(
|
|
61
|
+
(key as TranslationOrEnumerationNode).type
|
|
62
|
+
)
|
|
63
|
+
) {
|
|
64
|
+
foundProperty = (
|
|
65
|
+
(currentObj as unknown as CallExpression)
|
|
66
|
+
.arguments[0] as ObjectExpression
|
|
67
|
+
).properties.find(
|
|
68
|
+
(prop) => 'key' in prop && (prop.key as Identifier).name === key.key
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (foundProperty && 'value' in foundProperty) {
|
|
73
|
+
currentObj = foundProperty.value as ObjectExpression;
|
|
74
|
+
} else {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return currentObj;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find and update specific content based on key path
|
|
84
|
+
*/
|
|
85
|
+
const findAndUpdate = (
|
|
86
|
+
objExpr: ObjectExpression,
|
|
87
|
+
keyPath: KeyPath[],
|
|
88
|
+
newValue: string
|
|
89
|
+
) => {
|
|
90
|
+
const lastKey = keyPath[keyPath.length - 1]; // Get the last key in the path
|
|
91
|
+
|
|
92
|
+
if (lastKey) {
|
|
93
|
+
const propertyToUpdate = findNestedProperty(objExpr, keyPath); // Traverse the key path
|
|
94
|
+
|
|
95
|
+
if (propertyToUpdate && 'value' in propertyToUpdate) {
|
|
96
|
+
(propertyToUpdate as unknown as StringLiteral).value = newValue; // Update the value of the specified key
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Traverse the AST and update based on key path and new value
|
|
103
|
+
*/
|
|
104
|
+
const traverseNode = (node: Program, keyPath: KeyPath[], newValue: string) => {
|
|
105
|
+
if (Array.isArray(node.body)) {
|
|
106
|
+
(node.body as unknown as Program[]).forEach((subNode) =>
|
|
107
|
+
traverseNode(subNode, keyPath, newValue)
|
|
108
|
+
);
|
|
109
|
+
} else if (node.body) {
|
|
110
|
+
traverseNode(node.body, keyPath, newValue);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
// For ES Module (e.g., `const variable = ...; export default variable`)
|
|
115
|
+
'declarations' in node
|
|
116
|
+
) {
|
|
117
|
+
(node.declarations as VariableDeclarator[]).forEach((declaration) => {
|
|
118
|
+
if (declaration.init?.type === 'ObjectExpression') {
|
|
119
|
+
findAndUpdate(declaration.init, keyPath, newValue);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
// For ES Module (e.g., `export default { ... }`)
|
|
126
|
+
'declaration' in node &&
|
|
127
|
+
(node.declaration as ObjectExpression).type === 'ObjectExpression'
|
|
128
|
+
) {
|
|
129
|
+
return findAndUpdate(
|
|
130
|
+
node.declaration as ObjectExpression,
|
|
131
|
+
keyPath,
|
|
132
|
+
newValue
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
// For CommonJS (e.g., `module.exports = ...`)
|
|
138
|
+
'expression' in node &&
|
|
139
|
+
(node.expression as AssignmentExpression).right.type === 'ObjectExpression'
|
|
140
|
+
) {
|
|
141
|
+
return findAndUpdate(
|
|
142
|
+
(node.expression as AssignmentExpression).right as ObjectExpression,
|
|
143
|
+
keyPath,
|
|
144
|
+
newValue
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// throw new Error('Could not find the specified key path in the AST.');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format the content with Prettier
|
|
153
|
+
*/
|
|
154
|
+
const format = async (content: string) => {
|
|
155
|
+
// Resolve the configuration from the project
|
|
156
|
+
let options: prettier.Options = {};
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Resolve the prettier configuration from the project
|
|
160
|
+
options =
|
|
161
|
+
(await prettier.resolveConfig(content, {
|
|
162
|
+
editorconfig: true,
|
|
163
|
+
})) ?? {};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error('Failed to resolve Prettier configuration:', error);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Add the parser option to the resolved config
|
|
169
|
+
const config: prettier.Options = { ...options, parser: 'typescript' };
|
|
170
|
+
|
|
171
|
+
return prettier.format(content, config);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Edit the content of a file based on the key path and new value
|
|
176
|
+
*/
|
|
177
|
+
export const editContent = async (editedContent: EditedContent) => {
|
|
178
|
+
// Loop into each dictionary path
|
|
179
|
+
for (const dictionaryPath of Object.keys(editedContent)) {
|
|
180
|
+
// Read the file
|
|
181
|
+
const fileContent = readFileSync(dictionaryPath, 'utf-8');
|
|
182
|
+
|
|
183
|
+
// Parse the content with TypeScript support
|
|
184
|
+
const parsed = parse(fileContent, {
|
|
185
|
+
sourceType: 'module',
|
|
186
|
+
plugins: ['jsx', 'typescript'],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Loop into each key path and new value
|
|
190
|
+
for (const { keyPath, newValue } of editedContent[dictionaryPath]) {
|
|
191
|
+
// Update values based on key paths
|
|
192
|
+
traverseNode(parsed.program, keyPath, newValue);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Generate the updated code
|
|
196
|
+
const updatedContent = generate(parsed).code;
|
|
197
|
+
|
|
198
|
+
if (fileContent === updatedContent) {
|
|
199
|
+
console.info(`Could not find specified key path in ${dictionaryPath}.`);
|
|
200
|
+
} else {
|
|
201
|
+
const formattedContent = await format(updatedContent);
|
|
202
|
+
|
|
203
|
+
// Write back to the file
|
|
204
|
+
writeFileSync(dictionaryPath, formattedContent, 'utf-8');
|
|
205
|
+
|
|
206
|
+
console.info('Updated the file successfully.');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getConfiguration } from '@intlayer/config';
|
|
2
|
+
import bodyParser from 'body-parser';
|
|
3
|
+
import express, { type Request, type Response } from 'express';
|
|
4
|
+
import type { EditedContent } from '../client';
|
|
5
|
+
import { editContent } from './content-editor';
|
|
6
|
+
|
|
7
|
+
export const startIntlayerEditor = () => {
|
|
8
|
+
const app = express();
|
|
9
|
+
const {
|
|
10
|
+
editor: { port },
|
|
11
|
+
} = getConfiguration();
|
|
12
|
+
|
|
13
|
+
app.use(bodyParser.json());
|
|
14
|
+
app.use(bodyParser.urlencoded({ extended: true }));
|
|
15
|
+
|
|
16
|
+
app.post(
|
|
17
|
+
'/',
|
|
18
|
+
async (
|
|
19
|
+
req: Request<undefined, undefined, EditedContent>,
|
|
20
|
+
res: Response
|
|
21
|
+
) => {
|
|
22
|
+
const editedContent = req.body;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await editContent(editedContent);
|
|
26
|
+
|
|
27
|
+
res.send({ success: true, editedContent });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(error);
|
|
30
|
+
res.status(500).send({ success: false });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
app.listen(port, () => {
|
|
36
|
+
console.info(
|
|
37
|
+
`Intlayer editor server is running on http://localhost:${port}`
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
};
|