mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support i18n in typescript (#2948)
This commit is contained in:
parent
3d72b6fa12
commit
0dae8cf2f9
1
frontend/appflowy_tauri/.gitignore
vendored
1
frontend/appflowy_tauri/.gitignore
vendored
@ -25,3 +25,4 @@ dist-ssr
|
|||||||
|
|
||||||
**/src/services/backend/models/
|
**/src/services/backend/models/
|
||||||
**/src/services/backend/events/
|
**/src/services/backend/events/
|
||||||
|
**/src/appflowy_app/i18n/translations/
|
@ -5,14 +5,16 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "pnpm sync:i18n && tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||||
"test:errors": "tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .",
|
"test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .",
|
||||||
"test:prettier": "yarn prettier --list-different src",
|
"test:prettier": "pnpm prettier --list-different src",
|
||||||
"tauri:clean": "cargo make --cwd .. tauri_clean",
|
"tauri:clean": "cargo make --cwd .. tauri_clean",
|
||||||
"tauri:dev": "tauri dev"
|
"tauri:dev": "pnpm sync:i18n && tauri dev",
|
||||||
|
"sync:i18n": "node scripts/i18n/index.cjs",
|
||||||
|
"css:variables": "node style-dictionary/config.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
@ -25,13 +27,14 @@
|
|||||||
"@slate-yjs/core": "^1.0.0",
|
"@slate-yjs/core": "^1.0.0",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@tauri-apps/api": "^1.2.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.9",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"emoji-regex": "^10.2.1",
|
"emoji-regex": "^10.2.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"google-protobuf": "^3.21.2",
|
"google-protobuf": "^3.21.2",
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^22.4.10",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
|
"i18next-resources-to-backend": "^1.1.4",
|
||||||
"is-hotkey": "^0.2.0",
|
"is-hotkey": "^0.2.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"katex": "^0.16.7",
|
"katex": "^0.16.7",
|
||||||
@ -56,7 +59,6 @@
|
|||||||
"slate-react": "^0.94.2",
|
"slate-react": "^0.94.2",
|
||||||
"ts-results": "^3.3.0",
|
"ts-results": "^3.3.0",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
"y-indexeddb": "^9.0.9",
|
|
||||||
"yjs": "^13.5.51"
|
"yjs": "^13.5.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -83,6 +85,7 @@
|
|||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||||
|
"style-dictionary": "^3.8.0",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
@ -32,8 +32,8 @@ dependencies:
|
|||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.7
|
specifier: ^1.11.9
|
||||||
version: 1.11.7
|
version: 1.11.9
|
||||||
emoji-mart:
|
emoji-mart:
|
||||||
specifier: ^5.5.2
|
specifier: ^5.5.2
|
||||||
version: 5.5.2
|
version: 5.5.2
|
||||||
@ -52,6 +52,9 @@ dependencies:
|
|||||||
i18next-browser-languagedetector:
|
i18next-browser-languagedetector:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
|
i18next-resources-to-backend:
|
||||||
|
specifier: ^1.1.4
|
||||||
|
version: 1.1.4
|
||||||
is-hotkey:
|
is-hotkey:
|
||||||
specifier: ^0.2.0
|
specifier: ^0.2.0
|
||||||
version: 0.2.0
|
version: 0.2.0
|
||||||
@ -124,9 +127,6 @@ dependencies:
|
|||||||
utf8:
|
utf8:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
y-indexeddb:
|
|
||||||
specifier: ^9.0.9
|
|
||||||
version: 9.0.11(yjs@13.6.1)
|
|
||||||
yjs:
|
yjs:
|
||||||
specifier: ^13.5.51
|
specifier: ^13.5.51
|
||||||
version: 13.6.1
|
version: 13.6.1
|
||||||
@ -201,6 +201,9 @@ devDependencies:
|
|||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.8(prettier@2.8.4)
|
version: 0.2.8(prettier@2.8.4)
|
||||||
|
style-dictionary:
|
||||||
|
specifier: ^3.8.0
|
||||||
|
version: 3.8.0
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.2.7
|
specifier: ^3.2.7
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@ -2193,6 +2196,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
/camel-case@4.1.2:
|
||||||
|
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
|
||||||
|
dependencies:
|
||||||
|
pascal-case: 3.1.2
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/camelcase-css@2.0.1:
|
/camelcase-css@2.0.1:
|
||||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -2211,6 +2221,14 @@ packages:
|
|||||||
/caniuse-lite@1.0.30001487:
|
/caniuse-lite@1.0.30001487:
|
||||||
resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==}
|
resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==}
|
||||||
|
|
||||||
|
/capital-case@1.0.4:
|
||||||
|
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
upper-case-first: 2.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/chalk@2.4.2:
|
/chalk@2.4.2:
|
||||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -2226,6 +2244,23 @@ packages:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
/change-case@4.1.2:
|
||||||
|
resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==}
|
||||||
|
dependencies:
|
||||||
|
camel-case: 4.1.2
|
||||||
|
capital-case: 1.0.4
|
||||||
|
constant-case: 3.0.4
|
||||||
|
dot-case: 3.0.4
|
||||||
|
header-case: 2.0.4
|
||||||
|
no-case: 3.0.4
|
||||||
|
param-case: 3.0.4
|
||||||
|
pascal-case: 3.1.2
|
||||||
|
path-case: 3.0.4
|
||||||
|
sentence-case: 3.0.4
|
||||||
|
snake-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/char-regex@1.0.2:
|
/char-regex@1.0.2:
|
||||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2308,7 +2343,6 @@ packages:
|
|||||||
/commander@8.3.0:
|
/commander@8.3.0:
|
||||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/compute-scroll-into-view@1.0.20:
|
/compute-scroll-into-view@1.0.20:
|
||||||
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
||||||
@ -2317,6 +2351,14 @@ packages:
|
|||||||
/concat-map@0.0.1:
|
/concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
/constant-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
upper-case: 2.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/convert-source-map@1.9.0:
|
/convert-source-map@1.9.0:
|
||||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||||
|
|
||||||
@ -2358,8 +2400,8 @@ packages:
|
|||||||
/csstype@3.1.2:
|
/csstype@3.1.2:
|
||||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||||
|
|
||||||
/dayjs@1.11.7:
|
/dayjs@1.11.9:
|
||||||
resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==}
|
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/debug@4.3.4:
|
/debug@4.3.4:
|
||||||
@ -2455,6 +2497,13 @@ packages:
|
|||||||
csstype: 3.1.2
|
csstype: 3.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dot-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/electron-to-chromium@1.4.394:
|
/electron-to-chromium@1.4.394:
|
||||||
resolution: {integrity: sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==}
|
resolution: {integrity: sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==}
|
||||||
|
|
||||||
@ -2884,6 +2933,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fs-extra@10.1.0:
|
||||||
|
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jsonfile: 6.1.0
|
||||||
|
universalify: 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fs.realpath@1.0.0:
|
/fs.realpath@1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
@ -3029,7 +3087,6 @@ packages:
|
|||||||
|
|
||||||
/graceful-fs@4.2.11:
|
/graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/grapheme-splitter@1.0.4:
|
/grapheme-splitter@1.0.4:
|
||||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||||
@ -3072,6 +3129,13 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
function-bind: 1.1.1
|
||||||
|
|
||||||
|
/header-case@2.0.4:
|
||||||
|
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
|
||||||
|
dependencies:
|
||||||
|
capital-case: 1.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/hoist-non-react-statics@3.3.2:
|
/hoist-non-react-statics@3.3.2:
|
||||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3099,6 +3163,12 @@ packages:
|
|||||||
'@babel/runtime': 7.21.5
|
'@babel/runtime': 7.21.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/i18next-resources-to-backend@1.1.4:
|
||||||
|
resolution: {integrity: sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/i18next@22.4.15:
|
/i18next@22.4.15:
|
||||||
resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==}
|
resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3825,6 +3895,18 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/jsonc-parser@3.2.0:
|
||||||
|
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/jsonfile@6.1.0:
|
||||||
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
dependencies:
|
||||||
|
universalify: 2.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
dev: true
|
||||||
|
|
||||||
/jsx-ast-utils@3.3.3:
|
/jsx-ast-utils@3.3.3:
|
||||||
resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
|
resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@ -3904,7 +3986,6 @@ packages:
|
|||||||
|
|
||||||
/lodash@4.17.21:
|
/lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/loose-envify@1.4.0:
|
/loose-envify@1.4.0:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
@ -3912,6 +3993,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
/lower-case@2.0.2:
|
||||||
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lru-cache@5.1.1:
|
/lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4003,6 +4090,13 @@ packages:
|
|||||||
/natural-compare@1.4.0:
|
/natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
/no-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||||
|
dependencies:
|
||||||
|
lower-case: 2.0.2
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/node-int64@0.4.0:
|
/node-int64@0.4.0:
|
||||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -4151,6 +4245,13 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/param-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||||
|
dependencies:
|
||||||
|
dot-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/parchment@1.1.4:
|
/parchment@1.1.4:
|
||||||
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
|
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
|
||||||
|
|
||||||
@ -4170,6 +4271,20 @@ packages:
|
|||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/pascal-case@3.1.2:
|
||||||
|
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/path-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==}
|
||||||
|
dependencies:
|
||||||
|
dot-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/path-exists@4.0.0:
|
/path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -4799,6 +4914,14 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
|
|
||||||
|
/sentence-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
upper-case-first: 2.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/shebang-command@2.0.0:
|
/shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -4858,6 +4981,13 @@ packages:
|
|||||||
tiny-warning: 1.0.3
|
tiny-warning: 1.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/snake-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||||
|
dependencies:
|
||||||
|
dot-case: 3.0.4
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/source-map-js@1.0.2:
|
/source-map-js@1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -4966,6 +5096,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/style-dictionary@3.8.0:
|
||||||
|
resolution: {integrity: sha512-wHlB/f5eO3mDcYv6WtOz6gvQC477jBKrwuIXe+PtHskTCBsJdAOvL8hCquczJxDui2TnwpeNE+2msK91JJomZg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
change-case: 4.1.2
|
||||||
|
commander: 8.3.0
|
||||||
|
fs-extra: 10.1.0
|
||||||
|
glob: 7.2.3
|
||||||
|
json5: 2.2.3
|
||||||
|
jsonc-parser: 3.2.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
tinycolor2: 1.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/stylis@4.2.0:
|
/stylis@4.2.0:
|
||||||
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5077,6 +5223,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tinycolor2@1.6.0:
|
||||||
|
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tmpl@1.0.5:
|
/tmpl@1.0.5:
|
||||||
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5105,7 +5255,6 @@ packages:
|
|||||||
|
|
||||||
/tslib@2.5.0:
|
/tslib@2.5.0:
|
||||||
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
|
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tsutils@3.21.0(typescript@4.9.5):
|
/tsutils@3.21.0(typescript@4.9.5):
|
||||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||||
@ -5161,6 +5310,11 @@ packages:
|
|||||||
which-boxed-primitive: 1.0.2
|
which-boxed-primitive: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/universalify@2.0.0:
|
||||||
|
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/update-browserslist-db@1.0.11(browserslist@4.21.5):
|
/update-browserslist-db@1.0.11(browserslist@4.21.5):
|
||||||
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
|
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -5171,6 +5325,18 @@ packages:
|
|||||||
escalade: 3.1.1
|
escalade: 3.1.1
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
|
|
||||||
|
/upper-case-first@2.0.2:
|
||||||
|
resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/upper-case@2.0.2:
|
||||||
|
resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/uri-js@4.4.1:
|
/uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5313,15 +5479,6 @@ packages:
|
|||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/y-indexeddb@9.0.11(yjs@13.6.1):
|
|
||||||
resolution: {integrity: sha512-HOKQ70qW1h2WJGtOKu9rE8fbX86ExVZedecndMuhwax3yM4DQsQzCTGHt/jvTrFZr/9Ahvd8neD6aZ4dMMjtdg==}
|
|
||||||
peerDependencies:
|
|
||||||
yjs: ^13.0.0
|
|
||||||
dependencies:
|
|
||||||
lib0: 0.2.74
|
|
||||||
yjs: 13.6.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/y-protocols@1.0.5:
|
/y-protocols@1.0.5:
|
||||||
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
|
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
53
frontend/appflowy_tauri/scripts/i18n/index.cjs
Normal file
53
frontend/appflowy_tauri/scripts/i18n/index.cjs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const languages = [
|
||||||
|
'ar-SA',
|
||||||
|
'ca-ES',
|
||||||
|
'de-DE',
|
||||||
|
'en',
|
||||||
|
'es-VE',
|
||||||
|
'eu-ES',
|
||||||
|
'fr-FR',
|
||||||
|
'hu-HU',
|
||||||
|
'id-ID',
|
||||||
|
'it-IT',
|
||||||
|
'ja-JP',
|
||||||
|
'ko-KR',
|
||||||
|
'pl-PL',
|
||||||
|
'pt-BR',
|
||||||
|
'pt-PT',
|
||||||
|
'ru-RU',
|
||||||
|
'sv',
|
||||||
|
'tr-TR',
|
||||||
|
'zh-CN',
|
||||||
|
'zh-TW',
|
||||||
|
];
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
languages.forEach(language => {
|
||||||
|
const json = require(`../../../resources/translations/${language}.json`);
|
||||||
|
const outputJSON = flattenJSON(json);
|
||||||
|
const output = JSON.stringify(outputJSON);
|
||||||
|
const isExistDir = fs.existsSync('./src/appflowy_app/i18n/translations');
|
||||||
|
if (!isExistDir) {
|
||||||
|
fs.mkdirSync('./src/appflowy_app/i18n/translations');
|
||||||
|
}
|
||||||
|
fs.writeFile(`./src/appflowy_app/i18n/translations/${language}.json`, new Uint8Array(Buffer.from(output)), (res) => {
|
||||||
|
if (res) {
|
||||||
|
console.error(res);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
function flattenJSON(obj, prefix = '') {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let key in obj) {
|
||||||
|
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||||
|
const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`);
|
||||||
|
result = { ...result, ...nestedKeys };
|
||||||
|
} else {
|
||||||
|
result[`${prefix}${key}`] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
8
frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts
vendored
Normal file
8
frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import resources from './resources';
|
||||||
|
|
||||||
|
declare module 'i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
defaultNS: 'translation';
|
||||||
|
resources: typeof resources;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import translation from '$app/i18n/translations/en.json';
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
translation,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default resources;
|
@ -4,14 +4,12 @@ import { Provider } from 'react-redux';
|
|||||||
import { store } from './stores/store';
|
import { store } from './stores/store';
|
||||||
|
|
||||||
import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
|
import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
|
||||||
import initializeI18n from './stores/i18n/initializeI18n';
|
import '$app/i18n/config';
|
||||||
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
import AppMain from '$app/AppMain';
|
import AppMain from '$app/AppMain';
|
||||||
|
|
||||||
initializeI18n();
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
@ -19,10 +19,20 @@ export function useUserSetting() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userSettingController?.getAppearanceSetting().then((res) => {
|
userSettingController?.getAppearanceSetting().then((res) => {
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
const locale = res.locale;
|
||||||
|
let language = 'en';
|
||||||
|
|
||||||
|
if (locale.language_code && locale.country_code) {
|
||||||
|
language = `${locale.language_code}-${locale.country_code}`;
|
||||||
|
} else if (locale.language_code) {
|
||||||
|
language = locale.language_code;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
currentUserActions.setUserSetting({
|
currentUserActions.setUserSetting({
|
||||||
themeMode: res.theme_mode,
|
themeMode: res.theme_mode,
|
||||||
theme: res.theme as Theme,
|
theme: res.theme as Theme,
|
||||||
|
language: language,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -14,23 +14,23 @@ export const Button = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-onfill');
|
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill');
|
||||||
break;
|
break;
|
||||||
case 'medium':
|
case 'medium':
|
||||||
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-onfill');
|
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill');
|
||||||
break;
|
break;
|
||||||
case 'small':
|
case 'small':
|
||||||
setCls(
|
setCls(
|
||||||
'w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-content-default text-content-onfill text-xs hover:bg-content-hover'
|
'w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill text-xs hover:bg-content-hover'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'medium-transparent':
|
case 'medium-transparent':
|
||||||
setCls(
|
setCls(
|
||||||
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-content-default text-content-default transition-colors duration-300 hover:bg-content-hover hover:text-content-onfill'
|
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-content-default text-content-default transition-colors duration-300 hover:bg-content-blue-50 hover:text-content-on-fill'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'box-small-transparent':
|
case 'box-small-transparent':
|
||||||
setCls('text-icon-default w-[24px] h-[24px] rounded hover:bg-fill-hover');
|
setCls('text-icon-default w-[24px] h-[24px] rounded hover:bg-fill-list-hover');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [size]);
|
}, [size]);
|
||||||
|
@ -32,7 +32,7 @@ export const ChangeFieldTypePopup = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onClick(t)}
|
onClick={() => onClick(t)}
|
||||||
key={i}
|
key={i}
|
||||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<FieldTypeIcon fieldType={t}></FieldTypeIcon>
|
<FieldTypeIcon fieldType={t}></FieldTypeIcon>
|
||||||
|
@ -37,7 +37,7 @@ export const CheckListOption = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onToggleOptionClick(
|
onToggleOptionClick(
|
||||||
new SelectOptionPB({
|
new SelectOptionPB({
|
||||||
|
@ -69,7 +69,7 @@ export const EditCheckListPopup = ({
|
|||||||
top={top}
|
top={top}
|
||||||
>
|
>
|
||||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||||
<div className={'flex flex-1 items-center gap-2 rounded border border-line-border bg-fill-hover px-2 '}>
|
<div className={'flex flex-1 items-center gap-2 rounded border border-line-divider bg-fill-list-hover px-2 '}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={'py-2'}
|
className={'py-2'}
|
||||||
@ -78,11 +78,13 @@ export const EditCheckListPopup = ({
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onBlur={() => onBlur()}
|
onBlur={() => onBlur()}
|
||||||
/>
|
/>
|
||||||
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
|
<div className={'text-shade-3 font-mono'}>{value.length}/30</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => onDeleteOptionClick()}
|
onClick={() => onDeleteOptionClick()}
|
||||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-fill-default hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-fill-default hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<TrashSvg></TrashSvg>
|
<TrashSvg></TrashSvg>
|
||||||
|
@ -80,7 +80,9 @@ function PopupItem({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => changeFormat(format)}
|
onClick={() => changeFormat(format)}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|
||||||
|
@ -87,20 +87,24 @@ export const DateTypeOptions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex flex-col'}>
|
||||||
<hr className={'-mx-2 my-2 border-shade-6'} />
|
<hr className={'border-shade-6 -mx-2 my-2'} />
|
||||||
<button
|
<button
|
||||||
onClick={_onDateFormatClick}
|
onClick={_onDateFormatClick}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span>{t('grid.field.dateFormat')}</span>
|
<span>{t('grid.field.dateFormat')}</span>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<MoreSvg></MoreSvg>
|
<MoreSvg></MoreSvg>
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
<hr className={'-mx-2 my-2 border-line-border'} />
|
<hr className={'-mx-2 my-2 border-line-divider'} />
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleIncludeTime()}
|
onClick={() => toggleIncludeTime()}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex items-center gap-2'}>
|
<div className={'flex items-center gap-2'}>
|
||||||
<span>{t('grid.field.includeTime')}</span>
|
<span>{t('grid.field.includeTime')}</span>
|
||||||
@ -112,7 +116,9 @@ export const DateTypeOptions = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={_onTimeFormatClick}
|
onClick={_onTimeFormatClick}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span>{t('grid.field.timeFormat')}</span>
|
<span>{t('grid.field.timeFormat')}</span>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
|
@ -93,7 +93,9 @@ const FormatButton = ({ title, checked, onClick }: { title: string; checked: boo
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className={'block pr-8'}>{title}</span>
|
<span className={'block pr-8'}>{title}</span>
|
||||||
{checked && (
|
{checked && (
|
||||||
|
@ -41,7 +41,9 @@ export const TimeFormatPopup = ({
|
|||||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||||
<button
|
<button
|
||||||
onClick={() => changeFormat(TimeFormatPB.TwelveHour)}
|
onClick={() => changeFormat(TimeFormatPB.TwelveHour)}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t('grid.field.timeFormatTwelveHour')}
|
{t('grid.field.timeFormatTwelveHour')}
|
||||||
|
|
||||||
@ -53,7 +55,9 @@ export const TimeFormatPopup = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => changeFormat(TimeFormatPB.TwentyFourHour)}
|
onClick={() => changeFormat(TimeFormatPB.TwentyFourHour)}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t('grid.field.timeFormatTwentyFourHour')}
|
{t('grid.field.timeFormatTwentyFourHour')}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ export const EditCellWrapper = ({
|
|||||||
<div
|
<div
|
||||||
ref={el}
|
ref={el}
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
className={'flex h-5 w-5 rounded text-icon-default hover:bg-fill-hover'}
|
className={'text-icon-default flex h-5 w-5 rounded hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<DragElementSvg></DragElementSvg>
|
<DragElementSvg></DragElementSvg>
|
||||||
</div>
|
</div>
|
||||||
@ -72,7 +72,7 @@ export const EditCellWrapper = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'w-full cursor-pointer rounded-lg pl-3 text-sm hover:bg-fill-selector'}>
|
<div className={'w-full cursor-pointer rounded-lg pl-3 text-sm hover:bg-content-blue-50'}>
|
||||||
{(cellIdentifier.fieldType === FieldType.SingleSelect ||
|
{(cellIdentifier.fieldType === FieldType.SingleSelect ||
|
||||||
cellIdentifier.fieldType === FieldType.MultiSelect) &&
|
cellIdentifier.fieldType === FieldType.MultiSelect) &&
|
||||||
cellController && (
|
cellController && (
|
||||||
|
@ -101,7 +101,7 @@ export const EditFieldPopup = ({
|
|||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
onBlur={() => save()}
|
onBlur={() => save()}
|
||||||
className={
|
className={
|
||||||
'flex-1 rounded border border-line-border px-2 py-2 hover:border-fill-default focus:border-fill-default'
|
'flex-1 rounded border border-line-divider px-2 py-2 hover:border-fill-default focus:border-fill-default'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export const EditFieldPopup = ({
|
|||||||
ref={changeTypeButtonRef}
|
ref={changeTypeButtonRef}
|
||||||
onClick={() => onChangeFieldTypeClick()}
|
onClick={() => onChangeFieldTypeClick()}
|
||||||
className={
|
className={
|
||||||
'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-text-title hover:bg-fill-hover'
|
'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-text-title hover:bg-fill-list-hover'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg pl-2'}>
|
<button className={'flex cursor-pointer items-center gap-2 rounded-lg pl-2'}>
|
||||||
@ -129,10 +129,12 @@ export const EditFieldPopup = ({
|
|||||||
|
|
||||||
{cellIdentifier.fieldType === FieldType.Number && (
|
{cellIdentifier.fieldType === FieldType.Number && (
|
||||||
<>
|
<>
|
||||||
<hr className={'-mx-2 border-line-border'} />
|
<hr className={'-mx-2 border-line-divider'} />
|
||||||
<button
|
<button
|
||||||
onClick={onNumberFormatClick}
|
onClick={onNumberFormatClick}
|
||||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-lg py-2 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className={'pl-2'}>{t('grid.field.numberFormat')}</span>
|
<span className={'pl-2'}>{t('grid.field.numberFormat')}</span>
|
||||||
<span className={'pr-2'}>
|
<span className={'pr-2'}>
|
||||||
|
@ -206,13 +206,13 @@ export const EditRow = ({
|
|||||||
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-bg-body `}
|
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-bg-body `}
|
||||||
>
|
>
|
||||||
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
|
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
|
||||||
<button className={'block h-8 w-8 rounded-lg text-text-title hover:bg-fill-hover'}>
|
<button className={'block h-8 w-8 rounded-lg text-text-title hover:bg-fill-list-hover'}>
|
||||||
<CloseSvg></CloseSvg>
|
<CloseSvg></CloseSvg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex h-full'}>
|
<div className={'flex h-full'}>
|
||||||
<div className={'flex h-full flex-1 flex-col border-r border-line-border pb-4 pt-6'}>
|
<div className={'flex h-full flex-1 flex-col border-r border-line-divider pb-4 pt-6'}>
|
||||||
<div className={'pb-4 pl-12'}>
|
<div className={'pb-4 pl-12'}>
|
||||||
<button className={'flex items-center gap-2 p-4'}>
|
<button className={'flex items-center gap-2 p-4'}>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
@ -254,10 +254,10 @@ export const EditRow = ({
|
|||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
|
||||||
<div className={'border-t border-line-border px-8 pt-2'}>
|
<div className={'border-t border-line-divider px-8 pt-2'}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onNewColumnClick()}
|
onClick={() => onNewColumnClick()}
|
||||||
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-fill-hover'}
|
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<AddSvg></AddSvg>
|
<AddSvg></AddSvg>
|
||||||
|
@ -53,9 +53,9 @@ export const CellOption = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onToggleOptionClick}
|
onClick={onToggleOptionClick}
|
||||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-content-onfill`}>{option.title}</div>
|
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-text-title`}>{option.title}</div>
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
{checked && (
|
{checked && (
|
||||||
<button className={'h-5 w-5 p-1'}>
|
<button className={'h-5 w-5 p-1'}>
|
||||||
|
@ -19,13 +19,9 @@ export const CellOptions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ref} onClick={onClick} className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs'}>
|
||||||
ref={ref}
|
|
||||||
onClick={onClick}
|
|
||||||
className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs text-content-onfill'}
|
|
||||||
>
|
|
||||||
{data?.select_options?.map((option, index) => (
|
{data?.select_options?.map((option, index) => (
|
||||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
|
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-text-title`} key={index}>
|
||||||
{option?.name ?? ''}
|
{option?.name ?? ''}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -59,7 +59,7 @@ export const CellOptionsPopup = ({
|
|||||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex flex-1 items-center gap-2 rounded border border-line-border px-2 hover:border-fill-default focus:border-fill-default'
|
'flex flex-1 items-center gap-2 rounded border border-line-divider px-2 hover:border-fill-default focus:border-fill-default'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-wrap items-center gap-2 text-text-title'}>
|
<div className={'flex flex-wrap items-center gap-2 text-text-title'}>
|
||||||
|
@ -86,7 +86,7 @@ export const EditCellOptionPopup = ({
|
|||||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex flex-1 items-center gap-2 rounded border border-line-border px-2 hover:border-fill-hover focus:border-fill-hover'
|
'flex flex-1 items-center gap-2 rounded border border-line-divider px-2 hover:border-fill-hover focus:border-fill-hover'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -101,7 +101,9 @@ export const EditCellOptionPopup = ({
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => onDeleteOptionClick()}
|
onClick={() => onDeleteOptionClick()}
|
||||||
className={'text-main-alert flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'text-main-alert flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<TrashSvg></TrashSvg>
|
<TrashSvg></TrashSvg>
|
||||||
@ -184,7 +186,7 @@ const ColorItem = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={'flex cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-fill-list-hover'}
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
>
|
>
|
||||||
<div className={'flex items-center gap-2'}>
|
<div className={'flex items-center gap-2'}>
|
||||||
|
@ -20,7 +20,7 @@ export const SelectedOption = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5 text-content-onfill`}>
|
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5 text-content-on-fill`}>
|
||||||
<span>{option?.name ?? ''}</span>
|
<span>{option?.name ?? ''}</span>
|
||||||
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}>
|
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}>
|
||||||
<CloseSvg></CloseSvg>
|
<CloseSvg></CloseSvg>
|
||||||
|
@ -103,7 +103,7 @@ export const PropertiesPanel = ({
|
|||||||
<div
|
<div
|
||||||
onClick={() => setShowAddedProperties(!showAddedProperties)}
|
onClick={() => setShowAddedProperties(!showAddedProperties)}
|
||||||
className={
|
className={
|
||||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 text-text-title hover:bg-bg-base'
|
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 text-text-title hover:bg-fill-list-active'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'text-sm'}>Added Properties</div>
|
<div className={'text-sm'}>Added Properties</div>
|
||||||
@ -118,7 +118,7 @@ export const PropertiesPanel = ({
|
|||||||
key={cellIndex}
|
key={cellIndex}
|
||||||
onMouseEnter={() => setHoveredPropertyIndex(cellIndex)}
|
onMouseEnter={() => setHoveredPropertyIndex(cellIndex)}
|
||||||
className={
|
className={
|
||||||
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2 py-1 hover:bg-fill-hover'
|
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2 py-1 hover:bg-fill-list-hover'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex items-center gap-2 text-text-title '}>
|
<div className={'flex items-center gap-2 text-text-title '}>
|
||||||
@ -148,7 +148,9 @@ export const PropertiesPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowBasicProperties(!showBasicProperties)}
|
onClick={() => setShowBasicProperties(!showBasicProperties)}
|
||||||
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-list-active'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={'text-sm'}>Basic Properties</div>
|
<div className={'text-sm'}>Basic Properties</div>
|
||||||
<i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}>
|
<i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}>
|
||||||
@ -162,7 +164,7 @@ export const PropertiesPanel = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => addSelectedFieldType(type)}
|
onClick={() => addSelectedFieldType(type)}
|
||||||
key={i}
|
key={i}
|
||||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<FieldTypeIcon fieldType={type}></FieldTypeIcon>
|
<FieldTypeIcon fieldType={type}></FieldTypeIcon>
|
||||||
@ -177,7 +179,9 @@ export const PropertiesPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)}
|
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)}
|
||||||
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={
|
||||||
|
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-list-active'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={'text-sm'}>Advanced Properties</div>
|
<div className={'text-sm'}>Advanced Properties</div>
|
||||||
<i className={`h-5 w-5 transition-transform duration-500 ${showAdvancedProperties && 'rotate-180'}`}>
|
<i className={`h-5 w-5 transition-transform duration-500 ${showAdvancedProperties && 'rotate-180'}`}>
|
||||||
@ -187,19 +191,25 @@ export const PropertiesPanel = ({
|
|||||||
<div className={'flex flex-col gap-2 text-xs'}>
|
<div className={'flex flex-col gap-2 text-xs'}>
|
||||||
{showAdvancedProperties && (
|
{showAdvancedProperties && (
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex flex-col'}>
|
||||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}>
|
<button
|
||||||
|
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||||
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<MultiSelectTypeSvg></MultiSelectTypeSvg>
|
<MultiSelectTypeSvg></MultiSelectTypeSvg>
|
||||||
</i>
|
</i>
|
||||||
<span>Last edited time</span>
|
<span>Last edited time</span>
|
||||||
</button>
|
</button>
|
||||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}>
|
<button
|
||||||
|
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||||
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<DocumentSvg></DocumentSvg>
|
<DocumentSvg></DocumentSvg>
|
||||||
</i>
|
</i>
|
||||||
<span>Document</span>
|
<span>Document</span>
|
||||||
</button>
|
</button>
|
||||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}>
|
<button
|
||||||
|
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||||
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<SingleSelectTypeSvg></SingleSelectTypeSvg>
|
<SingleSelectTypeSvg></SingleSelectTypeSvg>
|
||||||
</i>
|
</i>
|
||||||
|
@ -39,7 +39,7 @@ export const PopupSelect = ({
|
|||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||||
onClick={(e) => handleClick(e, item)}
|
onClick={(e) => handleClick(e, item)}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
|
@ -42,7 +42,7 @@ export const PopupWindow = ({
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={
|
className={
|
||||||
'fixed z-10 rounded-lg bg-bg-base shadow-md transition-opacity duration-300 ' +
|
'fixed z-10 rounded-lg bg-bg-body shadow-md transition-opacity duration-300 ' +
|
||||||
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
|
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
|
||||||
(className ?? '')
|
(className ?? '')
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ export const SearchInput = () => {
|
|||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center rounded-lg border p-2 ${active ? 'border-fill-default' : 'border-line-border'}`}>
|
<div className={`flex items-center rounded-lg border p-2 ${active ? 'border-fill-default' : 'border-line-divider'}`}>
|
||||||
<i className='mr-2 h-5 w-5'>
|
<i className='mr-2 h-5 w-5'>
|
||||||
<SearchSvg />
|
<SearchSvg />
|
||||||
</i>
|
</i>
|
||||||
|
@ -3,23 +3,23 @@ import { SelectOptionColorPB } from '../../../services/backend';
|
|||||||
export const getBgColor = (color: SelectOptionColorPB | undefined): string => {
|
export const getBgColor = (color: SelectOptionColorPB | undefined): string => {
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case SelectOptionColorPB.Purple:
|
case SelectOptionColorPB.Purple:
|
||||||
return 'bg-tint-1';
|
return 'bg-tint-purple';
|
||||||
case SelectOptionColorPB.Pink:
|
case SelectOptionColorPB.Pink:
|
||||||
return 'bg-tint-2';
|
return 'bg-tint-pink';
|
||||||
case SelectOptionColorPB.LightPink:
|
case SelectOptionColorPB.LightPink:
|
||||||
return 'bg-tint-3';
|
return 'bg-tint-red';
|
||||||
case SelectOptionColorPB.Orange:
|
case SelectOptionColorPB.Orange:
|
||||||
return 'bg-tint-4';
|
return 'bg-tint-orange';
|
||||||
case SelectOptionColorPB.Yellow:
|
case SelectOptionColorPB.Yellow:
|
||||||
return 'bg-tint-5';
|
return 'bg-tint-yellow';
|
||||||
case SelectOptionColorPB.Lime:
|
case SelectOptionColorPB.Lime:
|
||||||
return 'bg-tint-6';
|
return 'bg-tint-lime';
|
||||||
case SelectOptionColorPB.Green:
|
case SelectOptionColorPB.Green:
|
||||||
return 'bg-tint-7';
|
return 'bg-tint-green';
|
||||||
case SelectOptionColorPB.Aqua:
|
case SelectOptionColorPB.Aqua:
|
||||||
return 'bg-tint-8';
|
return 'bg-tint-aqua';
|
||||||
case SelectOptionColorPB.Blue:
|
case SelectOptionColorPB.Blue:
|
||||||
return 'bg-tint-9';
|
return 'bg-tint-blue';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
export const EditorCheckSvg = () => {
|
export const EditorCheckSvg = () => {
|
||||||
return (
|
return (
|
||||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<rect x='2' y='2' width='12' height='12' rx='4' fill={'var(--color-fill-default)'} />
|
<rect x='2' y='2' width='12' height='12' rx='4' fill={'var(--fill-default)'} />
|
||||||
<path
|
<path
|
||||||
d='M6 8L7.61538 9.5L10.5 6.5'
|
d='M6 8L7.61538 9.5L10.5 6.5'
|
||||||
stroke={'var(--color-content-onfill)'}
|
stroke={'var(--content-on-fill)'}
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
strokeLinejoin='round'
|
strokeLinejoin='round'
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export const EditorUncheckSvg = () => {
|
export const EditorUncheckSvg = () => {
|
||||||
return (
|
return (
|
||||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<rect x='2.5' y='2.5' width='11' height='11' rx='3.5' stroke={'var(--color-icon-secondary)'} />
|
<rect x='2.5' y='2.5' width='11' height='11' rx='3.5' stroke={'var(--line-border)'} />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
export const FullView = () => {
|
export const FullView = () => {
|
||||||
return (
|
return (
|
||||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<path d='M6 13H3V10' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M6 13H3V10' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
<path d='M10 3H13V6' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M10 3H13V6' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
<path d='M3 13L7 9' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M3 13L7 9' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
<path d='M13 3L9 7' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M13 3L9 7' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,29 +3,29 @@ export const GroupBySvg = () => {
|
|||||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<path
|
<path
|
||||||
d='M10 2H13C13.5523 2 14 2.44772 14 3V6'
|
d='M10 2H13C13.5523 2 14 2.44772 14 3V6'
|
||||||
stroke='var(--color-text-title)'
|
stroke='var(--text-title)'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
strokeLinejoin='round'
|
strokeLinejoin='round'
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d='M6 2H3C2.44772 2 2 2.44772 2 3V6'
|
d='M6 2H3C2.44772 2 2 2.44772 2 3V6'
|
||||||
stroke='var(--color-text-title)'
|
stroke='var(--text-title)'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
strokeLinejoin='round'
|
strokeLinejoin='round'
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d='M6 14H3C2.44772 14 2 13.5523 2 13V10'
|
d='M6 14H3C2.44772 14 2 13.5523 2 13V10'
|
||||||
stroke='var(--color-text-title)'
|
stroke='var(--text-title)'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
strokeLinejoin='round'
|
strokeLinejoin='round'
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d='M10 14H13C13.5523 14 14 13.5523 14 13V10'
|
d='M10 14H13C13.5523 14 14 13.5523 14 13V10'
|
||||||
stroke='var(--color-text-title)'
|
stroke='var(--text-title)'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
strokeLinejoin='round'
|
strokeLinejoin='round'
|
||||||
/>
|
/>
|
||||||
<rect x='6' y='6' width='4' height='4' rx='1' stroke='var(--color-text-title)' />
|
<rect x='6' y='6' width='4' height='4' rx='1' stroke='var(--text-title)' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -64,9 +64,12 @@ export const BoardCard = ({
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
onClick={() => onOpenRow(rowInfo)}
|
onClick={() => onOpenRow(rowInfo)}
|
||||||
className={`relative cursor-pointer select-none rounded-lg border border-line-border bg-bg-body px-3 py-2 transition-transform duration-100 hover:bg-fill-selector `}
|
className={`relative cursor-pointer select-none rounded-lg bg-bg-body px-3 py-2 transition-transform duration-100 hover:bg-content-blue-50 `}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onDetailClick}
|
||||||
|
className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-fill-hover'}>
|
|
||||||
<Details2Svg></Details2Svg>
|
<Details2Svg></Details2Svg>
|
||||||
</button>
|
</button>
|
||||||
<div className={'flex flex-col gap-3'}>
|
<div className={'flex flex-col gap-3'}>
|
||||||
@ -95,7 +98,7 @@ export const BoardCard = ({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||||
onClick={() => onDeleteRowClick()}
|
onClick={() => onDeleteRowClick()}
|
||||||
>
|
>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
|
@ -15,7 +15,7 @@ export const BoardFieldsPopup = ({ hidePopup }: { hidePopup: () => void }) => {
|
|||||||
<div ref={ref} className={'absolute left-full top-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
<div ref={ref} className={'absolute left-full top-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<div
|
<div
|
||||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<div className={'flex items-center gap-2 '}>
|
<div className={'flex items-center gap-2 '}>
|
||||||
|
@ -52,10 +52,10 @@ export const BoardGroup = ({
|
|||||||
<span className={'text-shade-4'}>({group.rows.length})</span>
|
<span className={'text-shade-4'}>({group.rows.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex items-center gap-2'}>
|
<div className={'flex items-center gap-2'}>
|
||||||
<button className={'h-5 w-5 rounded hover:bg-fill-hover'}>
|
<button className={'h-5 w-5 rounded hover:bg-fill-list-hover'}>
|
||||||
<Details2Svg></Details2Svg>
|
<Details2Svg></Details2Svg>
|
||||||
</button>
|
</button>
|
||||||
<button className={'h-5 w-5 rounded hover:bg-fill-hover'}>
|
<button className={'h-5 w-5 rounded hover:bg-fill-list-hover'}>
|
||||||
<AddSvg></AddSvg>
|
<AddSvg></AddSvg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -86,7 +86,7 @@ export const BoardGroup = ({
|
|||||||
<div className={'p-2'}>
|
<div className={'p-2'}>
|
||||||
<button
|
<button
|
||||||
onClick={onNewRowClick}
|
onClick={onNewRowClick}
|
||||||
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||||
>
|
>
|
||||||
<span className={'h-5 w-5'}>
|
<span className={'h-5 w-5'}>
|
||||||
<AddSvg></AddSvg>
|
<AddSvg></AddSvg>
|
||||||
|
@ -15,7 +15,7 @@ export const BoardGroupFieldsPopup = ({ hidePopup }: { hidePopup: () => void })
|
|||||||
<div ref={ref} className={'absolute left-full top-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
<div ref={ref} className={'absolute left-full top-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<div
|
<div
|
||||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<div className={'flex items-center gap-2 '}>
|
<div className={'flex items-center gap-2 '}>
|
||||||
|
@ -8,6 +8,7 @@ import { Keyboard } from '$app/constants/document/keyboard';
|
|||||||
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
||||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
import { BlockType } from '$app/interfaces/document';
|
import { BlockType } from '$app/interfaces/document';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
enum BlockMenuOption {
|
enum BlockMenuOption {
|
||||||
Duplicate = 'Duplicate',
|
Duplicate = 'Duplicate',
|
||||||
@ -27,6 +28,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
|||||||
const { node } = useSubscribeNode(id);
|
const { node } = useSubscribeNode(id);
|
||||||
const [subMenuOpened, setSubMenuOpened] = useState(false);
|
const [subMenuOpened, setSubMenuOpened] = useState(false);
|
||||||
const [hovered, setHovered] = useState<BlockMenuOption | null>(null);
|
const [hovered, setHovered] = useState<BlockMenuOption | null>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hovered !== BlockMenuOption.TurnInto) {
|
if (hovered !== BlockMenuOption.TurnInto) {
|
||||||
@ -53,7 +55,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
|||||||
operate: () => {
|
operate: () => {
|
||||||
return handleClick({ operate: handleDelete });
|
return handleClick({ operate: handleDelete });
|
||||||
},
|
},
|
||||||
title: 'Delete',
|
title: t('document.plugins.optionAction.delete'),
|
||||||
icon: <Delete />,
|
icon: <Delete />,
|
||||||
key: BlockMenuOption.Delete,
|
key: BlockMenuOption.Delete,
|
||||||
},
|
},
|
||||||
@ -61,7 +63,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
|||||||
operate: () => {
|
operate: () => {
|
||||||
return handleClick({ operate: handleDuplicate });
|
return handleClick({ operate: handleDuplicate });
|
||||||
},
|
},
|
||||||
title: 'Duplicate',
|
title: t('document.plugins.optionAction.duplicate'),
|
||||||
icon: <ContentCopy />,
|
icon: <ContentCopy />,
|
||||||
key: BlockMenuOption.Duplicate,
|
key: BlockMenuOption.Duplicate,
|
||||||
},
|
},
|
||||||
@ -69,9 +71,10 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
|||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
key: BlockMenuOption.TurnInto,
|
key: BlockMenuOption.TurnInto,
|
||||||
|
title: t('document.plugins.optionAction.turnInto'),
|
||||||
},
|
},
|
||||||
].filter((item) => item !== null) as Option[],
|
].filter((item) => item !== null) as Option[],
|
||||||
[excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate]
|
[excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
@ -128,13 +131,19 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'p-2'}>
|
<div className={'p-2'}>
|
||||||
<TextField autoFocus label='Search' placeholder='Search actions...' variant='standard' />
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
label={t('search.label')}
|
||||||
|
placeholder={t('search.placeholder.actions')}
|
||||||
|
variant='standard'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
if (option.key === BlockMenuOption.TurnInto) {
|
if (option.key === BlockMenuOption.TurnInto) {
|
||||||
return (
|
return (
|
||||||
<BlockMenuTurnInto
|
<BlockMenuTurnInto
|
||||||
key={option.key}
|
key={option.key}
|
||||||
|
lable={option.title}
|
||||||
onHovered={() => {
|
onHovered={() => {
|
||||||
setHovered(BlockMenuOption.TurnInto);
|
setHovered(BlockMenuOption.TurnInto);
|
||||||
setSubMenuOpened(true);
|
setSubMenuOpened(true);
|
||||||
|
@ -9,12 +9,14 @@ function BlockMenuTurnInto({
|
|||||||
onHovered,
|
onHovered,
|
||||||
isHovered,
|
isHovered,
|
||||||
menuOpened,
|
menuOpened,
|
||||||
|
lable,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onHovered: (e: MouseEvent) => void;
|
onHovered: (e: MouseEvent) => void;
|
||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
menuOpened: boolean;
|
menuOpened: boolean;
|
||||||
|
lable?: string;
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
|
const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
|
||||||
@ -37,7 +39,7 @@ function BlockMenuTurnInto({
|
|||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
title='Turn into'
|
title={lable}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
icon={<Transform />}
|
icon={<Transform />}
|
||||||
extra={<ArrowRight />}
|
extra={<ArrowRight />}
|
||||||
|
@ -11,10 +11,12 @@ import { addBlockBelowClickThunk } from '$app_reducers/document/async-actions/me
|
|||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
||||||
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
|
export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { docId, controller } = useSubscribeDocument();
|
const { docId, controller } = useSubscribeDocument();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { nodeId, style, ref } = useBlockSideToolbar({ container });
|
const { nodeId, style, ref } = useBlockSideToolbar({ container });
|
||||||
const isDragging = useAppSelector(
|
const isDragging = useAppSelector(
|
||||||
@ -42,7 +44,7 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
|
|||||||
>
|
>
|
||||||
{/** Add Block below */}
|
{/** Add Block below */}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
tooltip={'Add a new block below'}
|
tooltip={t('tooltip.addBlockBelow')}
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!nodeId || !controller) return;
|
if (!nodeId || !controller) return;
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -58,7 +60,7 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
|
|||||||
|
|
||||||
{/** Open menu or drag */}
|
{/** Open menu or drag */}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
tooltip={'Click to open Menu'}
|
tooltip={t('tooltip.openMenu')}
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!nodeId) return;
|
if (!nodeId) return;
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -27,7 +27,6 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe
|
|||||||
import { slashCommandActions } from '$app_reducers/document/slice';
|
import { slashCommandActions } from '$app_reducers/document/slice';
|
||||||
import { Keyboard } from '$app/constants/document/keyboard';
|
import { Keyboard } from '$app/constants/document/keyboard';
|
||||||
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
||||||
import { blockEditActions } from '$app_reducers/document/block_edit_slice';
|
|
||||||
|
|
||||||
function BlockSlashMenu({
|
function BlockSlashMenu({
|
||||||
id,
|
id,
|
||||||
@ -60,7 +59,7 @@ function BlockSlashMenu({
|
|||||||
);
|
);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
},
|
},
|
||||||
[controller, dispatch, docId, id, onClose]
|
[controller, dispatch, id, onClose]
|
||||||
);
|
);
|
||||||
|
|
||||||
const options: (SlashCommandOption & {
|
const options: (SlashCommandOption & {
|
||||||
@ -293,7 +292,7 @@ function BlockSlashMenu({
|
|||||||
<div ref={ref} className={'min-h-0 flex-1 overflow-y-auto overflow-x-hidden'}>
|
<div ref={ref} className={'min-h-0 flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||||
{Object.entries(optionsByGroup).map(([group, options]) => (
|
{Object.entries(optionsByGroup).map(([group, options]) => (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
<div className={'px-2 py-2 text-sm text-shade-3'}>{group}</div>
|
<div className={'text-shade-3 px-2 py-2 text-sm'}>{group}</div>
|
||||||
<div>
|
<div>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
import BlockSlashMenu from '$app/components/document/BlockSlash/BlockSlashMenu';
|
import BlockSlashMenu from '$app/components/document/BlockSlash/BlockSlashMenu';
|
||||||
import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks';
|
import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks';
|
||||||
import { Keyboard } from '$app/constants/document/keyboard';
|
|
||||||
|
|
||||||
function BlockSlash({ container }: { container: HTMLDivElement }) {
|
function BlockSlash({ container }: { container: HTMLDivElement }) {
|
||||||
const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash();
|
const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash();
|
||||||
|
@ -17,7 +17,7 @@ export default function CalloutBlock({
|
|||||||
const { openEmojiSelect, open, closeEmojiSelect, id, anchorEl, onEmojiSelect } = useCalloutBlock(node.id);
|
const { openEmojiSelect, open, closeEmojiSelect, id, anchorEl, onEmojiSelect } = useCalloutBlock(node.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'my-1 flex rounded border border-solid border-line-border bg-fill-selector p-4'}>
|
<div className={'my-1 flex rounded border border-solid border-line-divider bg-content-blue-50 p-4'}>
|
||||||
<div className={'w-[1.5em]'} onMouseDown={(e) => e.stopPropagation()}>
|
<div className={'w-[1.5em]'} onMouseDown={(e) => e.stopPropagation()}>
|
||||||
<div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}>
|
<div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useContext } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||||
@ -6,15 +6,17 @@ import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
|||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { supportLanguage } from '$app/constants/document/code';
|
import { supportLanguage } from '$app/constants/document/code';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function SelectLanguage({ id, language }: { id: string; language: string }) {
|
function SelectLanguage({ id, language }: { id: string; language: string }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { controller } = useSubscribeDocument();
|
const { controller } = useSubscribeDocument();
|
||||||
|
const { t } = useTranslation();
|
||||||
const onLanguageSelect = useCallback(
|
const onLanguageSelect = useCallback(
|
||||||
(event: SelectChangeEvent) => {
|
(event: SelectChangeEvent) => {
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
const language = event.target.value;
|
const language = event.target.value;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateNodeDataThunk({
|
updateNodeDataThunk({
|
||||||
id,
|
id,
|
||||||
@ -34,7 +36,8 @@ function SelectLanguage({ id, language }: { id: string; language: string }) {
|
|||||||
className={'h-[28px] w-[150px]'}
|
className={'h-[28px] w-[150px]'}
|
||||||
value={language || 'javascript'}
|
value={language || 'javascript'}
|
||||||
onChange={onLanguageSelect}
|
onChange={onLanguageSelect}
|
||||||
label='Language'
|
placeholder={t('document.codeBlock.language.placeholder')}
|
||||||
|
label={t('document.codeBlock.language.label')}
|
||||||
>
|
>
|
||||||
{supportLanguage.map((item) => (
|
{supportLanguage.map((item) => (
|
||||||
<MenuItem key={item.id} value={item.id}>
|
<MenuItem key={item.id} value={item.id}>
|
||||||
|
@ -22,7 +22,10 @@ export default function CodeBlock({
|
|||||||
const isDark = useAppSelector((state) => state.currentUser.userSetting.themeMode === ThemeMode.Dark);
|
const isDark = useAppSelector((state) => state.currentUser.userSetting.themeMode === ThemeMode.Dark);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props} className={`my-1 rounded border border-solid border-line-border bg-fill-selector p-6 ${className}`}>
|
<div
|
||||||
|
{...props}
|
||||||
|
className={`my-1 rounded border border-solid border-line-divider bg-content-blue-50 p-6 ${className}`}
|
||||||
|
>
|
||||||
<div className={'mb-2 w-[100%]'}>
|
<div className={'mb-2 w-[100%]'}>
|
||||||
<SelectLanguage id={id} language={language} />
|
<SelectLanguage id={id} language={language} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDocumentTitle } from './DocumentTitle.hooks';
|
import { useDocumentTitle } from './DocumentTitle.hooks';
|
||||||
import TextBlock from '../TextBlock';
|
import TextBlock from '../TextBlock';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function DocumentTitle({ id }: { id: string }) {
|
export default function DocumentTitle({ id }: { id: string }) {
|
||||||
const { node } = useDocumentTitle(id);
|
const { node } = useDocumentTitle(id);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
return (
|
return (
|
||||||
<div data-block-id={node.id} className='doc-title relative mb-2 pt-[50px] text-4xl font-bold'>
|
<div data-block-id={node.id} className='doc-title relative mb-2 pt-[50px] text-4xl font-bold'>
|
||||||
<TextBlock placeholder='Untitled' childIds={[]} node={node} />
|
<TextBlock placeholder={t('document.title.placeholder')} childIds={[]} node={node} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { useBlockPopover } from '$app/components/document/_shared/BlockPopover/B
|
|||||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) {
|
function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) {
|
||||||
const formula = node.data.formula;
|
const formula = node.data.formula;
|
||||||
@ -60,19 +61,21 @@ function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> })
|
|||||||
});
|
});
|
||||||
const displayFormula = open ? value : formula;
|
const displayFormula = open ? value : formula;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={anchorElRef}
|
ref={anchorElRef}
|
||||||
onClick={openPopover}
|
onClick={openPopover}
|
||||||
className={'my-1 flex min-h-[59px] cursor-pointer flex-col overflow-hidden rounded hover:bg-fill-selector'}
|
className={'my-1 flex min-h-[59px] cursor-pointer flex-col overflow-hidden rounded hover:bg-content-blue-50'}
|
||||||
>
|
>
|
||||||
{displayFormula ? (
|
{displayFormula ? (
|
||||||
<KatexMath latex={displayFormula} />
|
<KatexMath latex={displayFormula} />
|
||||||
) : (
|
) : (
|
||||||
<div className={'flex h-[100%] w-[100%] flex-1 items-center bg-fill-selector px-1 text-text-title'}>
|
<div className={'flex h-[100%] w-[100%] flex-1 items-center bg-content-blue-50 px-1 text-text-caption'}>
|
||||||
<Functions />
|
<Functions />
|
||||||
<span>Add a TeX equation</span>
|
<span>{t('document.plugins.mathEquation.addMathEquation')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ import { useAppDispatch } from '$app/stores/store';
|
|||||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import UploadImage from '$app/components/document/_shared/UploadImage';
|
import UploadImage from '$app/components/document/_shared/UploadImage';
|
||||||
import { isTauri } from '$app/utils/env';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
enum TAB_KEYS {
|
enum TAB_KEYS {
|
||||||
UPLOAD = 'upload',
|
UPLOAD = 'upload',
|
||||||
@ -13,6 +13,7 @@ enum TAB_KEYS {
|
|||||||
|
|
||||||
function EditImage({ id, url, onClose }: { id: string; url: string; onClose: () => void }) {
|
function EditImage({ id, url, onClose }: { id: string; url: string; onClose: () => void }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { controller } = useSubscribeDocument();
|
const { controller } = useSubscribeDocument();
|
||||||
const [linkVal, setLinkVal] = useState<string>(url);
|
const [linkVal, setLinkVal] = useState<string>(url);
|
||||||
const [tabKey, setTabKey] = useState<TAB_KEYS>(TAB_KEYS.UPLOAD);
|
const [tabKey, setTabKey] = useState<TAB_KEYS>(TAB_KEYS.UPLOAD);
|
||||||
@ -41,31 +42,29 @@ function EditImage({ id, url, onClose }: { id: string; url: string; onClose: ()
|
|||||||
<div className={'w-[540px]'}>
|
<div className={'w-[540px]'}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
<Tabs value={tabKey} onChange={handleChange}>
|
<Tabs value={tabKey} onChange={handleChange}>
|
||||||
{isTauri() && <Tab label={'Upload Image'} value={TAB_KEYS.UPLOAD} />}
|
<Tab label={t('document.imageBlock.upload.label')} value={TAB_KEYS.UPLOAD} />
|
||||||
|
|
||||||
<Tab label='URL Image' value={TAB_KEYS.LINK} />
|
<Tab label={t('document.imageBlock.url.label')} value={TAB_KEYS.LINK} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
{isTauri() && (
|
|
||||||
<TabPanel value={tabKey} index={TAB_KEYS.UPLOAD}>
|
<TabPanel value={tabKey} index={TAB_KEYS.UPLOAD}>
|
||||||
<UploadImage onChange={handleConfirmUrl} />
|
<UploadImage onChange={handleConfirmUrl} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
)}
|
|
||||||
|
|
||||||
<TabPanel className={'flex flex-col p-3'} value={tabKey} index={TAB_KEYS.LINK}>
|
<TabPanel className={'flex flex-col p-3'} value={tabKey} index={TAB_KEYS.LINK}>
|
||||||
<TextField
|
<TextField
|
||||||
value={linkVal}
|
value={linkVal}
|
||||||
onChange={(e) => setLinkVal(e.target.value)}
|
onChange={(e) => setLinkVal(e.target.value)}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
label={'URL'}
|
label={t('document.imageBlock.url.label')}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '10px',
|
marginBottom: '10px',
|
||||||
}}
|
}}
|
||||||
placeholder={'Please enter the URL of the image'}
|
placeholder={t('document.imageBlock.url.placeholder')}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => handleConfirmUrl(linkVal)} variant='contained'>
|
<Button onClick={() => handleConfirmUrl(linkVal)} variant='contained'>
|
||||||
Upload
|
{t('button.upload')}
|
||||||
</Button>
|
</Button>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,8 +4,9 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe
|
|||||||
import { Align } from '$app/interfaces/document';
|
import { Align } from '$app/interfaces/document';
|
||||||
import { FormatAlignCenter, FormatAlignLeft, FormatAlignRight } from '@mui/icons-material';
|
import { FormatAlignCenter, FormatAlignLeft, FormatAlignRight } from '@mui/icons-material';
|
||||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||||
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
|
import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function ImageAlign({
|
function ImageAlign({
|
||||||
id,
|
id,
|
||||||
@ -21,6 +22,7 @@ function ImageAlign({
|
|||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();
|
const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();
|
||||||
const popoverOpen = Boolean(anchorEl);
|
const popoverOpen = Boolean(anchorEl);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (popoverOpen) {
|
if (popoverOpen) {
|
||||||
@ -61,7 +63,7 @@ function ImageAlign({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuTooltip title='Align'>
|
<ToolbarTooltip title={t('document.plugins.optionAction.align')}>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className='flex items-center justify-center p-1'
|
className='flex items-center justify-center p-1'
|
||||||
@ -71,7 +73,7 @@ function ImageAlign({
|
|||||||
>
|
>
|
||||||
{renderAlign(align)}
|
{renderAlign(align)}
|
||||||
</div>
|
</div>
|
||||||
</MenuTooltip>
|
</ToolbarTooltip>
|
||||||
<Popover
|
<Popover
|
||||||
open={popoverOpen}
|
open={popoverOpen}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
@ -87,7 +89,7 @@ function ImageAlign({
|
|||||||
onClose={() => setAnchorEl(undefined)}
|
onClose={() => setAnchorEl(undefined)}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
style: {
|
style: {
|
||||||
backgroundColor: 'var(--color-bg-body)',
|
backgroundColor: 'var(--bg-body)',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Alert, CircularProgress } from '@mui/material';
|
import { Alert, CircularProgress } from '@mui/material';
|
||||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function ImagePlaceholder({
|
function ImagePlaceholder({
|
||||||
error,
|
error,
|
||||||
@ -20,6 +21,7 @@ function ImagePlaceholder({
|
|||||||
openPopover: () => void;
|
openPopover: () => void;
|
||||||
}) {
|
}) {
|
||||||
const visible = loading || error || isEmpty;
|
const visible = loading || error || isEmpty;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -40,12 +42,12 @@ function ImagePlaceholder({
|
|||||||
{isEmpty && (
|
{isEmpty && (
|
||||||
<div
|
<div
|
||||||
onClick={openPopover}
|
onClick={openPopover}
|
||||||
className={'flex h-[100%] w-[100%] flex-1 items-center rounded bg-fill-selector px-1 text-text-title'}
|
className={'flex h-[100%] w-[100%] flex-1 items-center rounded bg-content-blue-50 px-1 text-text-caption'}
|
||||||
>
|
>
|
||||||
<i className={'mx-2 h-5 w-5'}>
|
<i className={'mx-2 h-5 w-5'}>
|
||||||
<ImageSvg />
|
<ImageSvg />
|
||||||
</i>
|
</i>
|
||||||
<span>Add an image</span>
|
<span>{t('document.imageBlock.placeholder')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@ function ImageRender({
|
|||||||
} top-0 flex h-[100%] w-[15px] cursor-col-resize items-center justify-center`}
|
} top-0 flex h-[100%] w-[15px] cursor-col-resize items-center justify-center`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`h-[48px] max-h-[50%] w-2 rounded-[20px] border border-solid border-line-border bg-line-border ${
|
className={`h-[48px] max-h-[50%] w-2 rounded-[20px] border border-solid border-line-divider bg-line-border ${
|
||||||
toolbarOpen ? 'opacity-1' : 'opacity-0'
|
toolbarOpen ? 'opacity-1' : 'opacity-0'
|
||||||
} transition-opacity duration-300 `}
|
} transition-opacity duration-300 `}
|
||||||
/>
|
/>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Align } from '$app/interfaces/document';
|
import { Align } from '$app/interfaces/document';
|
||||||
import ImageAlign from '$app/components/document/ImageBlock/ImageAlign';
|
import ImageAlign from '$app/components/document/ImageBlock/ImageAlign';
|
||||||
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
|
import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||||
import { DeleteOutline } from '@mui/icons-material';
|
import { DeleteOutline } from '@mui/icons-material';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { deleteNodeThunk } from '$app_reducers/document/async-actions';
|
import { deleteNodeThunk } from '$app_reducers/document/async-actions';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: Align }) {
|
function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: Align }) {
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
@ -13,6 +14,8 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { controller } = useSubscribeDocument();
|
const { controller } = useSubscribeDocument();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -21,7 +24,7 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
|
|||||||
} absolute right-2 top-2 z-[1px] flex h-[26px] max-w-[calc(100%-16px)] cursor-pointer items-center justify-center whitespace-nowrap rounded bg-bg-body text-sm text-text-title transition-opacity`}
|
} absolute right-2 top-2 z-[1px] flex h-[26px] max-w-[calc(100%-16px)] cursor-pointer items-center justify-center whitespace-nowrap rounded bg-bg-body text-sm text-text-title transition-opacity`}
|
||||||
>
|
>
|
||||||
<ImageAlign id={id} align={align} onOpen={() => setPopoverOpen(true)} onClose={() => setPopoverOpen(false)} />
|
<ImageAlign id={id} align={align} onOpen={() => setPopoverOpen(true)} onClose={() => setPopoverOpen(false)} />
|
||||||
<MenuTooltip title={'Delete'}>
|
<ToolbarTooltip title={t('button.delete')}>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(deleteNodeThunk({ id, controller }));
|
dispatch(deleteNodeThunk({ id, controller }));
|
||||||
@ -30,7 +33,7 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
|
|||||||
>
|
>
|
||||||
<DeleteOutline />
|
<DeleteOutline />
|
||||||
</div>
|
</div>
|
||||||
</MenuTooltip>
|
</ToolbarTooltip>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,7 @@ import CodeBlock from '$app/components/document/CodeBlock';
|
|||||||
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
|
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
import EquationBlock from '$app/components/document/EquationBlock';
|
import EquationBlock from '$app/components/document/EquationBlock';
|
||||||
import ImageBlock from '$app/components/document/ImageBlock';
|
import ImageBlock from '$app/components/document/ImageBlock';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const { node, childIds, isSelected, ref } = useNode(id);
|
const { node, childIds, isSelected, ref } = useNode(id);
|
||||||
@ -82,7 +83,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
|||||||
{renderBlock()}
|
{renderBlock()}
|
||||||
<BlockOverlay id={id} />
|
<BlockOverlay id={id} />
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
<div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-fill-hover' />
|
<div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-content-blue-100' />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</NodeIdContext.Provider>
|
</NodeIdContext.Provider>
|
||||||
@ -94,9 +95,11 @@ const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const UnSupportedBlock = () => {
|
const UnSupportedBlock = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert severity='info' className='mb-2'>
|
<Alert severity='info' className='mb-2'>
|
||||||
<p>The current version does not support this Block.</p>
|
<p>{t('unSupportBlock')}</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
|
|||||||
style={{
|
style={{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
}}
|
}}
|
||||||
className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-bg-base leading-tight text-text-title shadow-lg transition-opacity duration-100'
|
className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-fill-toolbar leading-tight text-content-on-fill shadow-md transition-opacity duration-100'
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
// prevent toolbar from taking focus away from editor
|
// prevent toolbar from taking focus away from editor
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { TemporaryType, TextAction } from '$app/interfaces/document';
|
import { TemporaryType, TextAction } from '$app/interfaces/document';
|
||||||
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
|
import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||||
import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
|
import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
@ -18,18 +17,19 @@ import {
|
|||||||
StrikethroughSOutlined,
|
StrikethroughSOutlined,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import LinkIcon from '@mui/icons-material/AddLink';
|
import LinkIcon from '@mui/icons-material/AddLink';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const iconSize = { width: 18, height: 18 };
|
export const iconSize = { width: 18, height: 18 };
|
||||||
|
|
||||||
const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
|
const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { docId, controller } = useSubscribeDocument();
|
const { docId, controller } = useSubscribeDocument();
|
||||||
|
const { t } = useTranslation();
|
||||||
const focusId = useAppSelector((state) => state[RANGE_NAME][docId]?.focus?.id || '');
|
const focusId = useAppSelector((state) => state[RANGE_NAME][docId]?.focus?.id || '');
|
||||||
const { node: focusNode } = useSubscribeNode(focusId);
|
const { node: focusNode } = useSubscribeNode(focusId);
|
||||||
|
|
||||||
const [isActive, setIsActive] = React.useState(false);
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
const color = useMemo(() => (isActive ? 'text-content-hover' : 'text-text-title'), [isActive]);
|
const color = useMemo(() => (isActive ? 'text-content-on-fill-hover' : ''), [isActive]);
|
||||||
|
|
||||||
const isFormatActive = useCallback(async () => {
|
const isFormatActive = useCallback(async () => {
|
||||||
if (!focusNode) return false;
|
if (!focusNode) return false;
|
||||||
@ -82,15 +82,15 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
|||||||
|
|
||||||
const formatTooltips: Record<string, string> = useMemo(
|
const formatTooltips: Record<string, string> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
[TextAction.Bold]: 'Bold',
|
[TextAction.Bold]: t('toolbar.bold'),
|
||||||
[TextAction.Italic]: 'Italic',
|
[TextAction.Italic]: t('toolbar.italic'),
|
||||||
[TextAction.Underline]: 'Underline',
|
[TextAction.Underline]: t('toolbar.underline'),
|
||||||
[TextAction.Strikethrough]: 'Strike through',
|
[TextAction.Strikethrough]: t('toolbar.strike'),
|
||||||
[TextAction.Code]: 'Mark as Code',
|
[TextAction.Code]: t('toolbar.inlineCode'),
|
||||||
[TextAction.Link]: 'Add Link',
|
[TextAction.Link]: t('toolbar.addLink'),
|
||||||
[TextAction.Equation]: 'Create equation',
|
[TextAction.Equation]: t('document.plugins.mathEquation.addMathEquation'),
|
||||||
}),
|
}),
|
||||||
[]
|
[t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatClick = useCallback(
|
const formatClick = useCallback(
|
||||||
@ -132,7 +132,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
|||||||
marginRight: '0.25rem',
|
marginRight: '0.25rem',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={'underline'}>Link</div>
|
<div className={'underline'}>{t('toolbar.link')}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case TextAction.Equation:
|
case TextAction.Equation:
|
||||||
@ -140,14 +140,14 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
|||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [icon]);
|
}, [icon, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuTooltip title={formatTooltips[format]}>
|
<ToolbarTooltip title={formatTooltips[format]}>
|
||||||
<div className={`${color} cursor-pointer px-1 hover:text-fill-default`} onClick={() => formatClick(format)}>
|
<div className={`${color} cursor-pointer px-1 hover:text-fill-default`} onClick={() => formatClick(format)}>
|
||||||
{formatIcon}
|
{formatIcon}
|
||||||
</div>
|
</div>
|
||||||
</MenuTooltip>
|
</ToolbarTooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
|
||||||
|
|
||||||
function MenuTooltip({ title, children }: { children: JSX.Element; title?: string }) {
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
slotProps={{ tooltip: { style: { background: '#E0F8FF', borderRadius: 8 } } }}
|
|
||||||
title={title}
|
|
||||||
placement='top-start'
|
|
||||||
>
|
|
||||||
<div>{children}</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MenuTooltip;
|
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
|
import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
|
||||||
import MenuTooltip from './MenuTooltip';
|
|
||||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ToolbarTooltip from '../../_shared/ToolbarTooltip';
|
||||||
|
|
||||||
function TurnIntoSelect({ id }: { id: string }) {
|
function TurnIntoSelect({ id }: { id: string }) {
|
||||||
const [anchorPosition, setAnchorPosition] = React.useState<{
|
const [anchorPosition, setAnchorPosition] = React.useState<{
|
||||||
@ -26,15 +26,16 @@ function TurnIntoSelect({ id }: { id: string }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const open = Boolean(anchorPosition);
|
const open = Boolean(anchorPosition);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuTooltip title='Turn into'>
|
<ToolbarTooltip title={t('document.plugins.optionAction.turnInto')}>
|
||||||
<div onClick={handleClick} className='flex cursor-pointer items-center px-2 text-sm text-fill-default'>
|
<div onClick={handleClick} className='flex cursor-pointer items-center px-2 text-sm text-fill-default'>
|
||||||
<span>{node.type}</span>
|
<span>{node.type}</span>
|
||||||
<ArrowDropDown />
|
<ArrowDropDown />
|
||||||
</div>
|
</div>
|
||||||
</MenuTooltip>
|
</ToolbarTooltip>
|
||||||
<TurnIntoPopover
|
<TurnIntoPopover
|
||||||
id={id}
|
id={id}
|
||||||
open={open}
|
open={open}
|
||||||
|
@ -31,7 +31,7 @@ function TextActionMenuList() {
|
|||||||
{groupItems.map(
|
{groupItems.map(
|
||||||
(group, i: number) =>
|
(group, i: number) =>
|
||||||
group.length > 0 && (
|
group.length > 0 && (
|
||||||
<div className={'flex border-r border-solid border-line-border px-1 last:border-r-0'} key={i}>
|
<div className={'flex border-r border-solid border-line-on-toolbar px-1 last:border-r-0'} key={i}>
|
||||||
{group.map((item) => (
|
{group.map((item) => (
|
||||||
<div key={item} className={'flex items-center'}>
|
<div key={item} className={'flex items-center'}>
|
||||||
{renderNode(item)}
|
{renderNode(item)}
|
||||||
|
@ -5,6 +5,7 @@ import { useChange } from '$app/components/document/_shared/EditorHooks/useChang
|
|||||||
import NodeChildren from '$app/components/document/Node/NodeChildren';
|
import NodeChildren from '$app/components/document/Node/NodeChildren';
|
||||||
import { useKeyDown } from '$app/components/document/TextBlock/useKeyDown';
|
import { useKeyDown } from '$app/components/document/TextBlock/useKeyDown';
|
||||||
import { useSelection } from '$app/components/document/_shared/EditorHooks/useSelection';
|
import { useSelection } from '$app/components/document/_shared/EditorHooks/useSelection';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: NestedBlock;
|
node: NestedBlock;
|
||||||
@ -15,10 +16,17 @@ function TextBlock({ node, childIds, placeholder }: Props) {
|
|||||||
const { value, onChange } = useChange(node);
|
const { value, onChange } = useChange(node);
|
||||||
const selectionProps = useSelection(node.id);
|
const selectionProps = useSelection(node.id);
|
||||||
const { onKeyDown } = useKeyDown(node.id);
|
const { onKeyDown } = useKeyDown(node.id);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Editor value={value} onChange={onChange} {...selectionProps} onKeyDown={onKeyDown} placeholder={placeholder} />
|
<Editor
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
{...selectionProps}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder={placeholder || t('document.textBlock.placeholder')}
|
||||||
|
/>
|
||||||
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
|
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -17,15 +17,18 @@ export function useBlockPopover({
|
|||||||
onAfterOpen?: () => void;
|
onAfterOpen?: () => void;
|
||||||
renderContent: ({ onClose }: { onClose: () => void }) => React.ReactNode;
|
renderContent: ({ onClose }: { onClose: () => void }) => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const anchorElRef = useRef<HTMLDivElement>(null);
|
const anchorElRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { docId } = useSubscribeDocument();
|
const { docId } = useSubscribeDocument();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
const [anchorPosition, setAnchorPosition] = useState<{
|
||||||
const open = Boolean(anchorEl);
|
top: number;
|
||||||
|
left: number;
|
||||||
|
}>();
|
||||||
|
const open = Boolean(anchorPosition);
|
||||||
const editing = useEditingState(id);
|
const editing = useEditingState(id);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const closePopover = useCallback(() => {
|
const closePopover = useCallback(() => {
|
||||||
setAnchorEl(null);
|
setAnchorPosition(undefined);
|
||||||
dispatch(
|
dispatch(
|
||||||
blockEditActions.setBlockEditState({
|
blockEditActions.setBlockEditState({
|
||||||
id: docId,
|
id: docId,
|
||||||
@ -48,7 +51,14 @@ export function useBlockPopover({
|
|||||||
}, [dispatch, docId, id]);
|
}, [dispatch, docId, id]);
|
||||||
|
|
||||||
const openPopover = useCallback(() => {
|
const openPopover = useCallback(() => {
|
||||||
setAnchorEl(anchorElRef.current);
|
if (!anchorElRef.current) return;
|
||||||
|
|
||||||
|
const rect = anchorElRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
setAnchorPosition({
|
||||||
|
top: rect.top + rect.height,
|
||||||
|
left: rect.left + rect.width / 2,
|
||||||
|
});
|
||||||
selectBlock();
|
selectBlock();
|
||||||
onAfterOpen?.();
|
onAfterOpen?.();
|
||||||
}, [onAfterOpen, selectBlock]);
|
}, [onAfterOpen, selectBlock]);
|
||||||
@ -68,21 +78,18 @@ export function useBlockPopover({
|
|||||||
vertical: 'top',
|
vertical: 'top',
|
||||||
horizontal: 'center',
|
horizontal: 'center',
|
||||||
}}
|
}}
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center',
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onClose={closePopover}
|
onClose={closePopover}
|
||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorEl}
|
anchorReference={'anchorPosition'}
|
||||||
|
anchorPosition={anchorPosition}
|
||||||
>
|
>
|
||||||
{renderContent({
|
{renderContent({
|
||||||
onClose: closePopover,
|
onClose: closePopover,
|
||||||
})}
|
})}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}, [anchorEl, closePopover, open, renderContent]);
|
}, [anchorPosition, closePopover, open, renderContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!anchorElRef.current) {
|
if (!anchorElRef.current) {
|
||||||
|
@ -132,6 +132,7 @@ function InlineContainer({
|
|||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
|
className={'inline-block-content'}
|
||||||
>
|
>
|
||||||
{renderNode()}
|
{renderNode()}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { Portal, Snackbar } from '@mui/material';
|
import { Alert, Portal, Snackbar } from '@mui/material';
|
||||||
import { TransitionProps } from '@mui/material/transitions';
|
|
||||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||||
|
|
||||||
function SlideTransition(props: SlideProps) {
|
function SlideTransition(props: SlideProps) {
|
||||||
@ -11,6 +10,7 @@ interface MessageProps {
|
|||||||
message?: string;
|
message?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
type?: 'success' | 'error';
|
||||||
}
|
}
|
||||||
export function useMessage() {
|
export function useMessage() {
|
||||||
const [state, setState] = useState<MessageProps>();
|
const [state, setState] = useState<MessageProps>();
|
||||||
@ -23,6 +23,7 @@ export function useMessage() {
|
|||||||
|
|
||||||
const contentHolder = useMemo(() => {
|
const contentHolder = useMemo(() => {
|
||||||
const open = !!state;
|
const open = !!state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
@ -31,10 +32,19 @@ export function useMessage() {
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={hide}
|
onClose={hide}
|
||||||
TransitionProps={{ onExited: hide }}
|
TransitionProps={{ onExited: hide }}
|
||||||
message={state?.message}
|
|
||||||
key={state?.key}
|
key={state?.key}
|
||||||
TransitionComponent={SlideTransition}
|
TransitionComponent={SlideTransition}
|
||||||
/>
|
>
|
||||||
|
<>
|
||||||
|
{state?.type ? (
|
||||||
|
<Alert severity={state.type} sx={{ width: '100%' }}>
|
||||||
|
{state.message}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<span>{state?.message}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Snackbar>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
}, [hide, state]);
|
}, [hide, state]);
|
||||||
|
@ -37,10 +37,10 @@ const TextLeaf = (props: TextLeafProps) => {
|
|||||||
};
|
};
|
||||||
let newChildren = children;
|
let newChildren = children;
|
||||||
|
|
||||||
if (leaf.code) {
|
if (leaf.code && !leaf.temporary) {
|
||||||
newChildren = (
|
newChildren = (
|
||||||
<span
|
<span
|
||||||
className={`bg-fill-selector text-text-title`}
|
className={`bg-content-blue-50 text-text-title`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '85%',
|
fontSize: '85%',
|
||||||
lineHeight: 'normal',
|
lineHeight: 'normal',
|
||||||
@ -97,9 +97,9 @@ const TextLeaf = (props: TextLeafProps) => {
|
|||||||
isCodeBlock && 'token',
|
isCodeBlock && 'token',
|
||||||
leaf.prism_token && leaf.prism_token,
|
leaf.prism_token && leaf.prism_token,
|
||||||
leaf.strikethrough && 'line-through',
|
leaf.strikethrough && 'line-through',
|
||||||
leaf.selection_high_lighted && 'bg-fill-selector',
|
leaf.selection_high_lighted && 'bg-content-blue-100',
|
||||||
leaf.link_selection_lighted && 'text-text-link-selector bg-fill-selector',
|
leaf.link_selection_lighted && 'text-text-link-selector bg-content-blue-100',
|
||||||
leaf.code && 'inline-code',
|
leaf.code && !leaf.temporary && 'inline-code',
|
||||||
leaf.bold && 'font-bold',
|
leaf.bold && 'font-bold',
|
||||||
leaf.italic && 'italic',
|
leaf.italic && 'italic',
|
||||||
leaf.underline && 'underline',
|
leaf.underline && 'underline',
|
||||||
@ -114,7 +114,11 @@ const TextLeaf = (props: TextLeafProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (leaf.temporary) {
|
if (leaf.temporary) {
|
||||||
newChildren = <TemporaryInput leaf={leaf}>{newChildren}</TemporaryInput>;
|
newChildren = (
|
||||||
|
<TemporaryInput getSelection={getSelection} leaf={leaf}>
|
||||||
|
{newChildren}
|
||||||
|
</TemporaryInput>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { CheckOutlined, FunctionsOutlined } from '@mui/icons-material';
|
import { CheckOutlined, FunctionsOutlined } from '@mui/icons-material';
|
||||||
import { Divider, IconButton, InputAdornment } from '@mui/material';
|
import { IconButton, InputAdornment } from '@mui/material';
|
||||||
|
|
||||||
function EquationEditContent({
|
function EquationEditContent({
|
||||||
value,
|
value,
|
||||||
|
@ -4,7 +4,7 @@ import KatexMath from '$app/components/document/_shared/KatexMath';
|
|||||||
|
|
||||||
function TemporaryEquation({ latex }: { latex: string }) {
|
function TemporaryEquation({ latex }: { latex: string }) {
|
||||||
return (
|
return (
|
||||||
<span className={'rounded bg-fill-selector px-1 py-0.5'} contentEditable={false}>
|
<span className={'rounded bg-content-blue-50 px-1 py-0.5'} contentEditable={false}>
|
||||||
{latex ? (
|
{latex ? (
|
||||||
<KatexMath latex={latex} isInline />
|
<KatexMath latex={latex} isInline />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,33 +1,36 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { TemporaryType } from '$app/interfaces/document';
|
import { RangeStaticNoId, TemporaryType } from '$app/interfaces/document';
|
||||||
import TemporaryEquation from '$app/components/document/_shared/TemporaryInput/TemporaryEquation';
|
import TemporaryEquation from '$app/components/document/_shared/TemporaryInput/TemporaryEquation';
|
||||||
import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks';
|
import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks';
|
||||||
import { isOverlappingPrefix } from '$app/utils/document/temporary';
|
|
||||||
import { PopoverPosition } from '@mui/material';
|
import { PopoverPosition } from '@mui/material';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { temporaryActions } from '$app_reducers/document/temporary_slice';
|
import { temporaryActions } from '$app_reducers/document/temporary_slice';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
|
|
||||||
function TemporaryInput({ leaf, children }: { leaf: { text: string }; children: React.ReactNode }) {
|
function TemporaryInput({
|
||||||
|
leaf,
|
||||||
|
children,
|
||||||
|
getSelection,
|
||||||
|
}: {
|
||||||
|
leaf: { text: string };
|
||||||
|
children: React.ReactNode;
|
||||||
|
getSelection: (node: Element) => RangeStaticNoId | null;
|
||||||
|
}) {
|
||||||
const temporaryState = useSubscribeTemporary();
|
const temporaryState = useSubscribeTemporary();
|
||||||
const id = temporaryState?.id;
|
const id = temporaryState?.id;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const ref = useRef<HTMLSpanElement>(null);
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
const { docId } = useSubscribeDocument();
|
const { docId } = useSubscribeDocument();
|
||||||
const match = useMemo(() => {
|
const match = useMemo(() => {
|
||||||
|
if (!ref.current) return false;
|
||||||
if (!leaf.text) return false;
|
if (!leaf.text) return false;
|
||||||
if (!temporaryState) return false;
|
if (!temporaryState) return false;
|
||||||
const { selectedText, type } = temporaryState;
|
const { selectedText } = temporaryState;
|
||||||
|
const selection = getSelection(ref.current);
|
||||||
|
|
||||||
switch (type) {
|
if (!selection) return false;
|
||||||
case TemporaryType.Equation:
|
return leaf.text === selectedText || selection.index <= temporaryState.selection.index;
|
||||||
// when the leaf is split, the placeholder is not the same as the leaf text,
|
}, [leaf.text, temporaryState, getSelection]);
|
||||||
// so we can only check for overlapping prefix and hidden other leafs
|
|
||||||
return leaf.text === selectedText || isOverlappingPrefix(leaf.text, selectedText);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [temporaryState, leaf.text]);
|
|
||||||
|
|
||||||
const renderPlaceholder = useCallback(() => {
|
const renderPlaceholder = useCallback(() => {
|
||||||
if (!temporaryState) return null;
|
if (!temporaryState) return null;
|
||||||
@ -69,7 +72,7 @@ function TemporaryInput({ leaf, children }: { leaf: { text: string }; children:
|
|||||||
return (
|
return (
|
||||||
<span ref={ref}>
|
<span ref={ref}>
|
||||||
{match ? renderPlaceholder() : null}
|
{match ? renderPlaceholder() : null}
|
||||||
<span className={'absolute opacity-0'}>{children}</span>
|
<span className={`absolute opacity-0 ${match ? 'w-0' : ''}`}>{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import LanguageIcon from '@mui/icons-material/Language';
|
|||||||
import CopyIcon from '@mui/icons-material/CopyAll';
|
import CopyIcon from '@mui/icons-material/CopyAll';
|
||||||
import { copyText } from '$app/utils/document/copy_paste';
|
import { copyText } from '$app/utils/document/copy_paste';
|
||||||
import { useMessage } from '$app/components/document/_shared/Message';
|
import { useMessage } from '$app/components/document/_shared/Message';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const iconSize = {
|
const iconSize = {
|
||||||
width: '1rem',
|
width: '1rem',
|
||||||
@ -28,6 +29,7 @@ function EditLinkToolbar({
|
|||||||
editing: boolean;
|
editing: boolean;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { show, contentHolder } = useMessage();
|
const { show, contentHolder } = useMessage();
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -70,9 +72,9 @@ function EditLinkToolbar({
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await copyText(href);
|
await copyText(href);
|
||||||
show({ message: 'Copied!', duration: 6000 });
|
show({ message: t('message.copy.success'), duration: 6000 });
|
||||||
} catch {
|
} catch {
|
||||||
show({ message: 'Copy failed!', duration: 6000 });
|
show({ message: t('message.copy.fail'), duration: 6000 });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={'mr-2 cursor-pointer'}
|
className={'mr-2 cursor-pointer'}
|
||||||
@ -80,7 +82,7 @@ function EditLinkToolbar({
|
|||||||
<CopyIcon sx={iconSize} />
|
<CopyIcon sx={iconSize} />
|
||||||
</div>
|
</div>
|
||||||
<div onClick={onEdit} className={'cursor-pointer'}>
|
<div onClick={onEdit} className={'cursor-pointer'}>
|
||||||
Edit
|
{t('button.edit')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,11 +8,12 @@ import { formatLinkThunk } from '$app_reducers/document/async-actions/link';
|
|||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { useSubscribeLinkPopover } from '$app/components/document/_shared/SubscribeLinkPopover.hooks';
|
import { useSubscribeLinkPopover } from '$app/components/document/_shared/SubscribeLinkPopover.hooks';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function LinkEditPopover() {
|
function LinkEditPopover() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { docId, controller } = useSubscribeDocument();
|
const { docId, controller } = useSubscribeDocument();
|
||||||
|
const { t } = useTranslation();
|
||||||
const popoverState = useSubscribeLinkPopover();
|
const popoverState = useSubscribeLinkPopover();
|
||||||
const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState;
|
const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState;
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ function LinkEditPopover() {
|
|||||||
>
|
>
|
||||||
<div className='flex flex-col p-3'>
|
<div className='flex flex-col p-3'>
|
||||||
<EditLink
|
<EditLink
|
||||||
text={'URL'}
|
text={t('document.inlineLink.url.label')}
|
||||||
value={href}
|
value={href}
|
||||||
onChange={(link) => {
|
onChange={(link) => {
|
||||||
onChange({
|
onChange({
|
||||||
@ -111,7 +112,7 @@ function LinkEditPopover() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EditLink
|
<EditLink
|
||||||
text={'Link title'}
|
text={t('document.inlineLink.title.label')}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(text) =>
|
onChange={(text) =>
|
||||||
onChange({
|
onChange({
|
||||||
@ -123,7 +124,7 @@ function LinkEditPopover() {
|
|||||||
<div className={'flex items-center justify-end'}>
|
<div className={'flex items-center justify-end'}>
|
||||||
<Button onClick={onDone}>
|
<Button onClick={onDone}>
|
||||||
<Done />
|
<Done />
|
||||||
Done
|
{t('button.done')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
||||||
|
function ToolbarTooltip({ title, children }: { children: JSX.Element; title?: string }) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
slotProps={{ tooltip: { style: { background: 'var(--bg-tips)', borderRadius: 8 } } }}
|
||||||
|
title={title}
|
||||||
|
placement='top-start'
|
||||||
|
>
|
||||||
|
<div>{children}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToolbarTooltip;
|
@ -1,30 +1,53 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { writeImage } from '$app/utils/document/image';
|
import { writeImage } from '$app/utils/document/image';
|
||||||
import { isTauri } from '$app/utils/env';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useMessage } from '$app/components/document/_shared/Message';
|
||||||
|
|
||||||
export interface UploadImageProps {
|
export interface UploadImageProps {
|
||||||
onChange: (filePath: string) => void;
|
onChange: (filePath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UploadImage({ onChange }: UploadImageProps) {
|
function UploadImage({ onChange }: UploadImageProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const beforeUpload = useCallback((file: File) => {
|
const beforeUpload = useCallback(
|
||||||
|
(file: File) => {
|
||||||
// check file size and type
|
// check file size and type
|
||||||
const sizeMatched = file.size / 1024 / 1024 < 5; // 5MB
|
const sizeMatched = file.size / 1024 / 1024 < 5; // 5MB
|
||||||
const typeMatched = /image\/(png|jpg|jpeg|gif)/.test(file.type); // png, jpg, jpeg, gif
|
const typeMatched = /image\/(png|jpg|jpeg|gif)/.test(file.type); // png, jpg, jpeg, gif
|
||||||
|
|
||||||
|
if (!sizeMatched) {
|
||||||
|
setError(t('document.imageBlock.error.invalidImageSize'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!typeMatched) {
|
||||||
|
setError(t('document.imageBlock.error.invalidImageFormat'));
|
||||||
|
}
|
||||||
|
|
||||||
return sizeMatched && typeMatched;
|
return sizeMatched && typeMatched;
|
||||||
}, []);
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!error) return;
|
||||||
|
message.show({
|
||||||
|
message: error,
|
||||||
|
duration: 3000,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
const handleUpload = useCallback(
|
const handleUpload = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
if (!beforeUpload(file)) {
|
if (!beforeUpload(file)) {
|
||||||
setError('Image should be less than 5MB and in png, jpg, jpeg, gif format');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,10 +61,10 @@ function UploadImage({ onChange }: UploadImageProps) {
|
|||||||
onChange(filePath);
|
onChange(filePath);
|
||||||
} catch {
|
} catch {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError('Upload failed');
|
setError(t('document.imageBlock.error.invalidImage'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[beforeUpload, onChange]
|
[beforeUpload, onChange, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
@ -88,7 +111,7 @@ function UploadImage({ onChange }: UploadImageProps) {
|
|||||||
<input onChange={handleChange} ref={inputRef} type='file' className={'hidden'} accept={'image/*'} />
|
<input onChange={handleChange} ref={inputRef} type='file' className={'hidden'} accept={'image/*'} />
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex flex-col items-center justify-center rounded-md border border-dashed border-content-hover py-10 text-content-hover'
|
'flex flex-col items-center justify-center rounded-md border border-dashed border-content-blue-300 bg-content-blue-50 py-10 text-content-blue-300'
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
borderColor: errorColor,
|
borderColor: errorColor,
|
||||||
@ -101,7 +124,7 @@ function UploadImage({ onChange }: UploadImageProps) {
|
|||||||
<div className={'h-8 w-8'}>
|
<div className={'h-8 w-8'}>
|
||||||
<ImageSvg />
|
<ImageSvg />
|
||||||
</div>
|
</div>
|
||||||
<div className={'my-2 p-2'}>{isTauri() ? 'Click space to chose image' : 'Chose image or drag to space'}</div>
|
<div className={'my-2 p-2'}>{t('document.imageBlock.upload.placeholder')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? <CircularProgress /> : null}
|
{loading ? <CircularProgress /> : null}
|
||||||
@ -112,8 +135,9 @@ function UploadImage({ onChange }: UploadImageProps) {
|
|||||||
}}
|
}}
|
||||||
className={`mt-5 text-sm text-text-caption`}
|
className={`mt-5 text-sm text-text-caption`}
|
||||||
>
|
>
|
||||||
The maximum file size is 5MB. Supported formats: JPG, PNG, GIF, SVG.
|
{t('document.imageBlock.support')}
|
||||||
</div>
|
</div>
|
||||||
|
{message.contentHolder}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import AddSvg from '../../_shared/svg/AddSvg';
|
|||||||
|
|
||||||
export const GridAddView = () => {
|
export const GridAddView = () => {
|
||||||
return (
|
return (
|
||||||
<button className='flex cursor-pointer items-center rounded-lg p-2 text-sm hover:bg-fill-hover'>
|
<button className='flex cursor-pointer items-center rounded-lg p-2 text-sm hover:bg-fill-list-hover'>
|
||||||
<i className='mr-2 h-5 w-5'>
|
<i className='mr-2 h-5 w-5'>
|
||||||
<AddSvg />
|
<AddSvg />
|
||||||
</i>
|
</i>
|
||||||
|
@ -18,15 +18,15 @@ export const GridTableHeader = ({ controller }: { controller: DatabaseController
|
|||||||
return <GridTableHeaderItem field={field} controller={controller} key={i} />;
|
return <GridTableHeaderItem field={field} controller={controller} key={i} />;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<th className='m-0 w-40 border border-r-0 border-line-border p-0'>
|
<th className='m-0 w-40 border border-r-0 border-line-divider p-0'>
|
||||||
<div
|
<div
|
||||||
className='flex cursor-pointer items-center px-4 py-2 text-text-caption hover:bg-fill-hover hover:text-text-title'
|
className='flex cursor-pointer items-center px-4 py-2 text-text-caption hover:bg-fill-list-hover hover:text-text-title'
|
||||||
onClick={onAddField}
|
onClick={onAddField}
|
||||||
>
|
>
|
||||||
<i className='mr-2 h-5 w-5'>
|
<i className='mr-2 h-5 w-5'>
|
||||||
<AddSvg />
|
<AddSvg />
|
||||||
</i>
|
</i>
|
||||||
<span>{t('grid.newCol')}</span>
|
<span>{t('grid.field.newProperty')}</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -61,9 +61,9 @@ export const GridTableHeaderItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th key={field.fieldId} className='m-0 border border-l-0 border-line-border p-0'>
|
<th key={field.fieldId} className='m-0 border border-l-0 border-line-divider p-0'>
|
||||||
<div
|
<div
|
||||||
className={'flex w-full cursor-pointer items-center px-4 py-2 hover:bg-fill-hover'}
|
className={'flex w-full cursor-pointer items-center px-4 py-2 hover:bg-fill-list-hover'}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
@ -21,7 +21,7 @@ export const GridTableRow = ({
|
|||||||
<tr className='group'>
|
<tr className='group'>
|
||||||
{cells.map((cell, cellIndex) => {
|
{cells.map((cell, cellIndex) => {
|
||||||
return (
|
return (
|
||||||
<td className='m-0 border border-l-0 border-line-border p-0 ' key={cellIndex}>
|
<td className='m-0 border border-l-0 border-line-divider p-0 ' key={cellIndex}>
|
||||||
<div className='flex w-full items-center justify-end'>
|
<div className='flex w-full items-center justify-end'>
|
||||||
<GridCell
|
<GridCell
|
||||||
cellIdentifier={cell.cellIdentifier}
|
cellIdentifier={cell.cellIdentifier}
|
||||||
@ -32,7 +32,7 @@ export const GridTableRow = ({
|
|||||||
{cellIndex === 0 && (
|
{cellIndex === 0 && (
|
||||||
<div
|
<div
|
||||||
onClick={() => onOpenRow(row)}
|
onClick={() => onOpenRow(row)}
|
||||||
className='mr-1 hidden h-8 w-8 cursor-pointer rounded p-1.5 text-text-caption hover:bg-fill-hover group-hover:block '
|
className='mr-1 hidden h-8 w-8 cursor-pointer rounded p-1.5 text-text-caption hover:bg-fill-list-hover group-hover:block '
|
||||||
>
|
>
|
||||||
<FullView />
|
<FullView />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@ export const FooterPanel = () => {
|
|||||||
© 2023 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
|
© 2023 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button className={'h-8 w-8 rounded bg-fill-selector text-text-title hover:bg-fill-hover'}>?</button>
|
<button className={'h-8 w-8 rounded bg-content-blue-50 text-text-title hover:bg-content-blue-100'}>?</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -38,10 +38,16 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-hover'} onClick={() => history.back()}>
|
<button
|
||||||
|
className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-list-hover'}
|
||||||
|
onClick={() => history.back()}
|
||||||
|
>
|
||||||
<ArrowLeftSvg />
|
<ArrowLeftSvg />
|
||||||
</button>
|
</button>
|
||||||
<button className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-hover'} onClick={() => history.forward()}>
|
<button
|
||||||
|
className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-list-hover'}
|
||||||
|
onClick={() => history.forward()}
|
||||||
|
>
|
||||||
<ArrowRightSvg />
|
<ArrowRightSvg />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import { PageOptions } from './PageOptions';
|
|||||||
|
|
||||||
export const HeaderPanel = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
|
export const HeaderPanel = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-[60px] items-center justify-between border-b border-line-border px-8'}>
|
<div className={'flex h-[60px] items-center justify-between border-b border-line-divider px-8'}>
|
||||||
<Breadcrumbs menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}></Breadcrumbs>
|
<Breadcrumbs menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}></Breadcrumbs>
|
||||||
<PageOptions></PageOptions>
|
<PageOptions></PageOptions>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { LanguageSelectPopup } from '$app/components/_shared/LanguageSelectPopup';
|
|
||||||
import { LanguageOutlined } from '@mui/icons-material';
|
|
||||||
|
|
||||||
export const LanguageButton = () => {
|
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button onClick={() => setShowPopup(!showPopup)} className={'h-8 w-8 rounded text-text-title hover:bg-fill-hover'}>
|
|
||||||
<LanguageOutlined />
|
|
||||||
</button>
|
|
||||||
{showPopup && <LanguageSelectPopup onClose={() => setShowPopup(false)}></LanguageSelectPopup>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { LogoutSvg } from '$app/components/_shared/svg/LogoutSvg';
|
||||||
|
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
function MoreMenu({ onClose }: { onClose: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { logout } = useAuth();
|
||||||
|
const onSignOutClick = useCallback(async () => {
|
||||||
|
await logout();
|
||||||
|
onClose();
|
||||||
|
}, [onClose, logout]);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: t('button.signOut'),
|
||||||
|
icon: (
|
||||||
|
<i className={'block h-5 w-5 flex-shrink-0'}>
|
||||||
|
<LogoutSvg></LogoutSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: onSignOutClick,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [onSignOutClick, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={item.onClick}>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
{item.icon}
|
||||||
|
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoreMenu;
|
@ -1,23 +0,0 @@
|
|||||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
|
||||||
import { LogoutSvg } from '../../_shared/svg/LogoutSvg';
|
|
||||||
|
|
||||||
export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => {
|
|
||||||
const items: IPopupItem[] = [
|
|
||||||
{
|
|
||||||
title: 'Sign out',
|
|
||||||
icon: (
|
|
||||||
<i className={'block h-5 w-5 flex-shrink-0'}>
|
|
||||||
<LogoutSvg></LogoutSvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onSignOutClick,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<PopupSelect
|
|
||||||
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
|
|
||||||
items={items}
|
|
||||||
onOutsideClick={onClose}
|
|
||||||
></PopupSelect>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,27 +1,20 @@
|
|||||||
import { useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useAuth } from '../../auth/auth.hooks';
|
import { useAuth } from '../../auth/auth.hooks';
|
||||||
|
|
||||||
export const usePageOptions = () => {
|
export const usePageOptions = () => {
|
||||||
const [showOptionsPopup, setShowOptionsPopup] = useState(false);
|
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | HTMLButtonElement>();
|
||||||
const { logout } = useAuth();
|
|
||||||
|
|
||||||
const onOptionsClick = () => {
|
const onOptionsClick = useCallback((el: HTMLDivElement | HTMLButtonElement) => {
|
||||||
setShowOptionsPopup(true);
|
setAnchorEl(el);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setShowOptionsPopup(false);
|
setAnchorEl(undefined);
|
||||||
};
|
|
||||||
|
|
||||||
const onSignOutClick = async () => {
|
|
||||||
await logout();
|
|
||||||
onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showOptionsPopup,
|
anchorEl,
|
||||||
onOptionsClick,
|
onOptionsClick,
|
||||||
onClose,
|
onClose,
|
||||||
onSignOutClick,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,30 +1,73 @@
|
|||||||
import { Button } from '../../_shared/Button';
|
|
||||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||||
import { usePageOptions } from './PageOptions.hooks';
|
import { usePageOptions } from './PageOptions.hooks';
|
||||||
import { OptionsPopup } from './OptionsPopup';
|
import { Button, IconButton, List } from '@mui/material';
|
||||||
import { LanguageButton } from '$app/components/layout/HeaderPanel/LanguageButton';
|
import Popover from '@mui/material/Popover';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import MoreMenu from '$app/components/layout/HeaderPanel/MoreMenu';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
enum PageOptionsEnum {
|
||||||
|
Share = 'Share',
|
||||||
|
More = 'More',
|
||||||
|
}
|
||||||
export const PageOptions = () => {
|
export const PageOptions = () => {
|
||||||
const { showOptionsPopup, onOptionsClick, onClose, onSignOutClick } = usePageOptions();
|
const { t } = useTranslation();
|
||||||
|
const { anchorEl, onOptionsClick, onClose } = usePageOptions();
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const [option, setOption] = useState<PageOptionsEnum>();
|
||||||
|
const renderMenu = useCallback(() => {
|
||||||
|
switch (option) {
|
||||||
|
case PageOptionsEnum.Share:
|
||||||
|
return <div>Share</div>;
|
||||||
|
default:
|
||||||
|
return <MoreMenu onClose={onClose} />;
|
||||||
|
}
|
||||||
|
}, [onClose, option]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'relative flex items-center gap-4'}>
|
<div className={'relative flex items-center gap-4'}>
|
||||||
<Button size={'small'} onClick={() => console.log('share click')}>
|
<Button
|
||||||
Share
|
variant={'contained'}
|
||||||
|
onClick={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
|
||||||
|
setOption(PageOptionsEnum.Share);
|
||||||
|
onOptionsClick(el);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('shareAction.buttonText')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<LanguageButton></LanguageButton>
|
<IconButton
|
||||||
|
|
||||||
<button
|
|
||||||
id='option-button'
|
id='option-button'
|
||||||
className={'relative h-8 w-8 rounded text-text-title hover:bg-fill-hover'}
|
size={'small'}
|
||||||
onClick={onOptionsClick}
|
className={'h-8 w-8 rounded text-text-title hover:bg-fill-list-hover'}
|
||||||
|
onClick={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
|
||||||
|
setOption(PageOptionsEnum.More);
|
||||||
|
onOptionsClick(el);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Details2Svg></Details2Svg>
|
<Details2Svg></Details2Svg>
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
{showOptionsPopup && <OptionsPopup onSignOutClick={onSignOutClick} onClose={onClose}></OptionsPopup>}
|
<Popover
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={onClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List>{renderMenu()}</List>
|
||||||
|
</Popover>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { EditSvg } from '$app/components/_shared/svg/EditSvg';
|
||||||
|
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||||
|
import { CopySvg } from '$app/components/_shared/svg/CopySvg';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import RenameDialog from '$app/components/layout/NavigationPanel/RenameDialog';
|
||||||
|
import { IPage } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
|
function MoreMenu({
|
||||||
|
selectedPage,
|
||||||
|
onRename,
|
||||||
|
onDeleteClick,
|
||||||
|
onDuplicateClick,
|
||||||
|
}: {
|
||||||
|
selectedPage: IPage;
|
||||||
|
onRename: (name: string) => Promise<void>;
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
onDuplicateClick: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const items = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||||
|
<EditSvg></EditSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
setRenameDialogOpen(true);
|
||||||
|
},
|
||||||
|
title: t('disclosureAction.rename'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||||
|
<TrashSvg></TrashSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: onDeleteClick,
|
||||||
|
title: t('disclosureAction.delete'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||||
|
<CopySvg></CopySvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: onDuplicateClick,
|
||||||
|
title: t('disclosureAction.duplicate'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onDeleteClick, onDuplicateClick, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={item.onClick}>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
{item.icon}
|
||||||
|
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<RenameDialog
|
||||||
|
defaultValue={selectedPage.title}
|
||||||
|
open={renameDialogOpen}
|
||||||
|
onClose={() => setRenameDialogOpen(false)}
|
||||||
|
onOk={async (val: string) => {
|
||||||
|
await onRename(val);
|
||||||
|
setRenameDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoreMenu;
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
@ -10,23 +10,26 @@ import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants
|
|||||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||||
import { ViewObserver } from '$app/stores/effects/folder/view/view_observer';
|
import { ViewObserver } from '$app/stores/effects/folder/view/view_observer';
|
||||||
|
|
||||||
|
export enum NavItemOptions {
|
||||||
|
More = 'More',
|
||||||
|
NewPage = 'NewPage',
|
||||||
|
}
|
||||||
export const useNavItem = (page: IPage) => {
|
export const useNavItem = (page: IPage) => {
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
const workspace = useAppSelector((state) => state.workspace);
|
const workspace = useAppSelector((state) => state.workspace);
|
||||||
const currentLocation = useLocation();
|
const currentLocation = useLocation();
|
||||||
const [activePageId, setActivePageId] = useState<string>('');
|
const [activePageId, setActivePageId] = useState<string>('');
|
||||||
const pages = useAppSelector((state) => state.pages);
|
const pages = useAppSelector((state) => state.pages);
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLElement>();
|
||||||
|
const menuOpen = Boolean(anchorEl);
|
||||||
|
const [menuOption, setMenuOption] = useState<NavItemOptions>();
|
||||||
|
const [selectedPage, setSelectedPage] = useState<IPage>();
|
||||||
|
const onClickMenuBtn = useCallback((page: IPage, option: NavItemOptions) => {
|
||||||
|
setSelectedPage(page);
|
||||||
|
setMenuOption(option);
|
||||||
|
}, []);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Actions
|
|
||||||
const [showPageOptions, setShowPageOptions] = useState(false);
|
|
||||||
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
|
|
||||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
|
||||||
|
|
||||||
// UI configurations
|
|
||||||
const [folderHeight, setFolderHeight] = useState(`${INITIAL_FOLDER_HEIGHT}px`);
|
|
||||||
|
|
||||||
// backend
|
// backend
|
||||||
const service = new ViewBackendService(page.id);
|
const service = new ViewBackendService(page.id);
|
||||||
const observer = new ViewObserver(page.id);
|
const observer = new ViewObserver(page.id);
|
||||||
@ -68,14 +71,6 @@ export const useNavItem = (page: IPage) => {
|
|||||||
setActivePageId(pageId);
|
setActivePageId(pageId);
|
||||||
}, [currentLocation]);
|
}, [currentLocation]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (page.showPagesInside) {
|
|
||||||
setFolderHeight(`${PAGE_ITEM_HEIGHT + getChildCount(page) * PAGE_ITEM_HEIGHT}px`);
|
|
||||||
} else {
|
|
||||||
setFolderHeight(`${PAGE_ITEM_HEIGHT}px`);
|
|
||||||
}
|
|
||||||
}, [page, pages]);
|
|
||||||
|
|
||||||
// recursively get all unfolded child pages
|
// recursively get all unfolded child pages
|
||||||
const getChildCount: (startPage: IPage) => number = (startPage: IPage) => {
|
const getChildCount: (startPage: IPage) => number = (startPage: IPage) => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@ -95,42 +90,21 @@ export const useNavItem = (page: IPage) => {
|
|||||||
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPageOptionsClick = () => {
|
|
||||||
setShowPageOptions((prevState) => !prevState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPageRename = () => {
|
|
||||||
setShowRenamePopup(true);
|
|
||||||
closePopup();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNewPageClick = () => {
|
|
||||||
setShowNewPageOptions(!showNewPageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePageTitle = async (newTitle: string) => {
|
const changePageTitle = async (newTitle: string) => {
|
||||||
await service.update({ name: newTitle });
|
await service.update({ name: newTitle });
|
||||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||||
};
|
setAnchorEl(undefined);
|
||||||
|
|
||||||
const closeRenamePopup = () => {
|
|
||||||
setShowRenamePopup(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePage = async () => {
|
const deletePage = async () => {
|
||||||
closePopup();
|
|
||||||
await service.delete();
|
await service.delete();
|
||||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||||
|
setAnchorEl(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicatePage = async () => {
|
const duplicatePage = async () => {
|
||||||
closePopup();
|
|
||||||
await service.duplicate();
|
await service.duplicate();
|
||||||
};
|
setAnchorEl(undefined);
|
||||||
|
|
||||||
const closePopup = () => {
|
|
||||||
setShowPageOptions(false);
|
|
||||||
setShowNewPageOptions(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPageClick = (eventPage: IPage) => {
|
const onPageClick = (eventPage: IPage) => {
|
||||||
@ -151,7 +125,6 @@ export const useNavItem = (page: IPage) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddNewPage = async (pageType: ViewLayoutPB) => {
|
const onAddNewPage = async (pageType: ViewLayoutPB) => {
|
||||||
closePopup();
|
|
||||||
if (!workspace?.id) return;
|
if (!workspace?.id) return;
|
||||||
|
|
||||||
let newPageName = '';
|
let newPageName = '';
|
||||||
@ -199,24 +172,15 @@ export const useNavItem = (page: IPage) => {
|
|||||||
showPagesInside: false,
|
showPagesInside: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
setAnchorEl(undefined);
|
||||||
navigate(`/page/${pageTypeRoute}/${newView.id}`);
|
navigate(`/page/${pageTypeRoute}/${newView.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onUnfoldClick,
|
onUnfoldClick,
|
||||||
onNewPageClick,
|
|
||||||
onPageOptionsClick,
|
|
||||||
startPageRename,
|
|
||||||
|
|
||||||
changePageTitle,
|
changePageTitle,
|
||||||
closeRenamePopup,
|
|
||||||
closePopup,
|
|
||||||
|
|
||||||
showNewPageOptions,
|
|
||||||
showPageOptions,
|
|
||||||
showRenamePopup,
|
|
||||||
|
|
||||||
deletePage,
|
deletePage,
|
||||||
duplicatePage,
|
duplicatePage,
|
||||||
@ -225,7 +189,12 @@ export const useNavItem = (page: IPage) => {
|
|||||||
|
|
||||||
onAddNewPage,
|
onAddNewPage,
|
||||||
|
|
||||||
folderHeight,
|
|
||||||
activePageId,
|
activePageId,
|
||||||
|
menuOpen,
|
||||||
|
anchorEl,
|
||||||
|
setAnchorEl,
|
||||||
|
menuOption,
|
||||||
|
selectedPage,
|
||||||
|
onClickMenuBtn,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,90 +1,91 @@
|
|||||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||||
import AddSvg from '../../_shared/svg/AddSvg';
|
import AddSvg from '../../_shared/svg/AddSvg';
|
||||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
|
||||||
import { NewPagePopup } from './NewPagePopup';
|
|
||||||
import { IPage } from '$app_reducers/pages/slice';
|
import { IPage } from '$app_reducers/pages/slice';
|
||||||
import { Button } from '../../_shared/Button';
|
import { useMemo, useRef } from 'react';
|
||||||
import { RenamePopup } from './RenamePopup';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
||||||
import { ANIMATION_DURATION, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
import { ANIMATION_DURATION } from '../../_shared/constants';
|
||||||
import { useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
|
import { NavItemOptions, useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
import Popover from '@mui/material/Popover';
|
||||||
|
import { IconButton, List } from '@mui/material';
|
||||||
|
import MoreMenu from '$app/components/layout/NavigationPanel/MoreMenu';
|
||||||
|
import NewPageMenu from '$app/components/layout/NavigationPanel/NewPageMenu';
|
||||||
|
|
||||||
export const NavItem = ({ page }: { page: IPage }) => {
|
export const NavItem = ({ page }: { page: IPage }) => {
|
||||||
const pages = useAppSelector((state) => state.pages);
|
const pages = useAppSelector((state) => state.pages);
|
||||||
const {
|
const {
|
||||||
onUnfoldClick,
|
onUnfoldClick,
|
||||||
onNewPageClick,
|
|
||||||
onPageOptionsClick,
|
|
||||||
startPageRename,
|
|
||||||
|
|
||||||
changePageTitle,
|
changePageTitle,
|
||||||
closeRenamePopup,
|
|
||||||
closePopup,
|
|
||||||
|
|
||||||
showNewPageOptions,
|
|
||||||
showPageOptions,
|
|
||||||
showRenamePopup,
|
|
||||||
|
|
||||||
deletePage,
|
deletePage,
|
||||||
duplicatePage,
|
duplicatePage,
|
||||||
|
|
||||||
onAddNewPage,
|
onAddNewPage,
|
||||||
|
|
||||||
folderHeight,
|
|
||||||
activePageId,
|
activePageId,
|
||||||
|
|
||||||
onPageClick,
|
onPageClick,
|
||||||
|
onClickMenuBtn,
|
||||||
|
menuOpen,
|
||||||
|
menuOption,
|
||||||
|
setAnchorEl,
|
||||||
|
selectedPage,
|
||||||
|
anchorEl,
|
||||||
} = useNavItem(page);
|
} = useNavItem(page);
|
||||||
|
|
||||||
const [popupY, setPopupY] = useState(0);
|
|
||||||
|
|
||||||
const el = useRef<HTMLDivElement>(null);
|
const el = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (el.current) {
|
|
||||||
const { top } = el.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
setPopupY(top);
|
|
||||||
}
|
|
||||||
}, [showPageOptions, showNewPageOptions, showRenamePopup]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div ref={el}>
|
<div ref={el}>
|
||||||
|
<div className={`transition-all`} style={{ transitionDuration: `${ANIMATION_DURATION}ms` }}>
|
||||||
|
<div className={`cursor-pointer px-1 py-1`}>
|
||||||
<div
|
<div
|
||||||
className={`overflow-hidden transition-all`}
|
className={`flex items-center justify-between rounded-lg px-2 py-1 hover:bg-fill-list-hover ${
|
||||||
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }}
|
activePageId === page.id ? 'bg-fill-list-hover' : ''
|
||||||
>
|
|
||||||
<div style={{ height: PAGE_ITEM_HEIGHT }} className={`cursor-pointer px-1 py-1`}>
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between rounded-lg px-2 py-1 hover:bg-fill-active ${
|
|
||||||
activePageId === page.id ? 'bg-fill-active' : ''
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={'flex h-full min-w-0 flex-1 items-center'}>
|
<div className={'flex h-full min-w-0 flex-1 items-center'}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onUnfoldClick()}
|
onClick={() => onUnfoldClick()}
|
||||||
className={`mr-2 h-5 w-5 transition-transform duration-200 ${page.showPagesInside && 'rotate-180'}`}
|
className={`mr-2 h-5 w-5 transition-transform duration-200 ${
|
||||||
|
page.showPagesInside ? 'rotate-180' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<DropDownShowSvg></DropDownShowSvg>
|
<DropDownShowSvg></DropDownShowSvg>
|
||||||
</button>
|
</button>
|
||||||
<div onClick={() => onPageClick(page)} className={'mr-1 flex h-full min-w-0 items-center text-left'}>
|
<div
|
||||||
|
onClick={() => onPageClick(page)}
|
||||||
|
className={'mr-1 flex h-full min-w-0 flex-1 items-center text-left'}
|
||||||
|
>
|
||||||
<span className={'w-[100%] overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
|
<span className={'w-[100%] overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
<IconButton
|
||||||
|
className={'h-6 w-6'}
|
||||||
|
size={'small'}
|
||||||
|
onClick={(e) => {
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
onClickMenuBtn(page, NavItemOptions.More);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Details2Svg></Details2Svg>
|
<Details2Svg></Details2Svg>
|
||||||
</Button>
|
</IconButton>
|
||||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
<IconButton
|
||||||
|
className={'h-6 w-6'}
|
||||||
|
size={'small'}
|
||||||
|
onClick={(e) => {
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
onClickMenuBtn(page, NavItemOptions.NewPage);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AddSvg></AddSvg>
|
<AddSvg></AddSvg>
|
||||||
</Button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'pl-4'}>
|
<div className={`${page.showPagesInside ? '' : 'hidden'} pl-4`}>
|
||||||
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
|
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
|
||||||
(insidePage, insideIndex) => (
|
(insidePage, insideIndex) => (
|
||||||
<NavItem key={insideIndex} page={insidePage}></NavItem>
|
<NavItem key={insideIndex} page={insidePage}></NavItem>
|
||||||
@ -92,32 +93,32 @@ export const NavItem = ({ page }: { page: IPage }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showPageOptions && (
|
</div>
|
||||||
<NavItemOptionsPopup
|
<Popover
|
||||||
onRenameClick={() => startPageRename()}
|
open={menuOpen}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={() => setAnchorEl(undefined)}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{menuOption === NavItemOptions.More && selectedPage && (
|
||||||
|
<MoreMenu
|
||||||
|
selectedPage={selectedPage}
|
||||||
|
onRename={changePageTitle}
|
||||||
onDeleteClick={() => deletePage()}
|
onDeleteClick={() => deletePage()}
|
||||||
onDuplicateClick={() => duplicatePage()}
|
onDuplicateClick={() => duplicatePage()}
|
||||||
onClose={() => closePopup()}
|
/>
|
||||||
top={popupY - 124 + 58}
|
|
||||||
></NavItemOptionsPopup>
|
|
||||||
)}
|
)}
|
||||||
{showNewPageOptions && (
|
{menuOption === NavItemOptions.NewPage && (
|
||||||
<NewPagePopup
|
<NewPageMenu
|
||||||
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
|
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
|
||||||
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
|
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
|
||||||
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
|
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
|
||||||
onClose={() => closePopup()}
|
/>
|
||||||
top={popupY - 124 + 58}
|
|
||||||
></NewPagePopup>
|
|
||||||
)}
|
)}
|
||||||
{showRenamePopup && (
|
</List>
|
||||||
<RenamePopup
|
</Popover>
|
||||||
value={page.title}
|
</>
|
||||||
onChange={(newTitle) => changePageTitle(newTitle)}
|
|
||||||
onClose={closeRenamePopup}
|
|
||||||
top={popupY - 124 + 40}
|
|
||||||
></RenamePopup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
|
||||||
import { EditSvg } from '../../_shared/svg/EditSvg';
|
|
||||||
import { TrashSvg } from '../../_shared/svg/TrashSvg';
|
|
||||||
import { CopySvg } from '../../_shared/svg/CopySvg';
|
|
||||||
|
|
||||||
export const NavItemOptionsPopup = ({
|
|
||||||
onRenameClick,
|
|
||||||
onDeleteClick,
|
|
||||||
onDuplicateClick,
|
|
||||||
onClose,
|
|
||||||
top,
|
|
||||||
}: {
|
|
||||||
onRenameClick: () => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
onDuplicateClick: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
top: number;
|
|
||||||
}) => {
|
|
||||||
const items: IPopupItem[] = [
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
|
||||||
<EditSvg></EditSvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onRenameClick,
|
|
||||||
title: 'Rename',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
|
||||||
<TrashSvg></TrashSvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onDeleteClick,
|
|
||||||
title: 'Delete',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
|
||||||
<CopySvg></CopySvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onDuplicateClick,
|
|
||||||
title: 'Duplicate',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopupSelect
|
|
||||||
onOutsideClick={() => onClose && onClose()}
|
|
||||||
items={items}
|
|
||||||
className={`absolute right-0`}
|
|
||||||
style={{ top: `${top}px` }}
|
|
||||||
></PopupSelect>
|
|
||||||
);
|
|
||||||
};
|
|
@ -54,24 +54,16 @@ export const NavigationPanel = ({
|
|||||||
left: `${menuHidden ? -width : 0}px`,
|
left: `${menuHidden ? -width : 0}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col'}>
|
|
||||||
<AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
|
<AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
|
||||||
<WorkspaceUser></WorkspaceUser>
|
<WorkspaceUser></WorkspaceUser>
|
||||||
<div className={'relative flex flex-1 flex-col'}>
|
<div className={'relative flex flex-1 flex-col'}>
|
||||||
<div
|
<div className={'flex h-[100%] flex-col overflow-auto px-2'} ref={el}>
|
||||||
className={'flex flex-col overflow-auto px-2'}
|
|
||||||
style={{
|
|
||||||
maxHeight: 'calc(100vh - 350px)',
|
|
||||||
}}
|
|
||||||
ref={el}
|
|
||||||
>
|
|
||||||
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
|
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex max-h-[215px] flex-col'}>
|
<div className={'flex max-h-[240px] flex-col'}>
|
||||||
<div className={'border-b border-line-border px-2 pb-4'}>
|
<div className={'border-b border-line-divider px-2 pb-4'}>
|
||||||
{/*<PluginsButton></PluginsButton>*/}
|
{/*<PluginsButton></PluginsButton>*/}
|
||||||
|
|
||||||
{/*<DesignSpec></DesignSpec>*/}
|
{/*<DesignSpec></DesignSpec>*/}
|
||||||
@ -105,7 +97,7 @@ export const TestBackendButton = () => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/page/api-test')}
|
onClick={() => navigate('/page/api-test')}
|
||||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-fill-active'}
|
className={'hover:bg-fill-active flex w-full items-center rounded-lg px-4 py-2'}
|
||||||
>
|
>
|
||||||
API Test
|
API Test
|
||||||
</button>
|
</button>
|
||||||
@ -118,7 +110,7 @@ export const DesignSpec = () => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('page/colors')}
|
onClick={() => navigate('page/colors')}
|
||||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-fill-active'}
|
className={'hover:bg-fill-active flex w-full items-center rounded-lg px-4 py-2'}
|
||||||
>
|
>
|
||||||
Color Palette
|
Color Palette
|
||||||
</button>
|
</button>
|
||||||
@ -131,7 +123,7 @@ export const AllIcons = () => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('page/all-icons')}
|
onClick={() => navigate('page/all-icons')}
|
||||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-fill-active'}
|
className={'hover:bg-fill-active flex w-full items-center rounded-lg px-4 py-2'}
|
||||||
>
|
>
|
||||||
All Icons
|
All Icons
|
||||||
</button>
|
</button>
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
|
||||||
|
import { BoardSvg } from '$app/components/_shared/svg/BoardSvg';
|
||||||
|
import { GridSvg } from '$app/components/_shared/svg/GridSvg';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
function NewPageMenu({
|
||||||
|
onDocumentClick,
|
||||||
|
onGridClick,
|
||||||
|
onBoardClick,
|
||||||
|
}: {
|
||||||
|
onDocumentClick: () => void;
|
||||||
|
onGridClick: () => void;
|
||||||
|
onBoardClick: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const items = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||||
|
<DocumentSvg></DocumentSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: onDocumentClick,
|
||||||
|
title: t('document.menuName'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||||
|
<BoardSvg></BoardSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: onBoardClick,
|
||||||
|
title: t('board.menuName'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||||
|
<GridSvg></GridSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: onGridClick,
|
||||||
|
title: t('grid.menuName'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onBoardClick, onDocumentClick, onGridClick, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={item.onClick}>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
{item.icon}
|
||||||
|
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewPageMenu;
|
@ -1,57 +0,0 @@
|
|||||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
|
||||||
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
|
||||||
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
|
||||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
|
||||||
|
|
||||||
export const NewPagePopup = ({
|
|
||||||
onDocumentClick,
|
|
||||||
onGridClick,
|
|
||||||
onBoardClick,
|
|
||||||
onClose,
|
|
||||||
top,
|
|
||||||
}: {
|
|
||||||
onDocumentClick: () => void;
|
|
||||||
onGridClick: () => void;
|
|
||||||
onBoardClick: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
top: number;
|
|
||||||
}) => {
|
|
||||||
const items: IPopupItem[] = [
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
|
||||||
<DocumentSvg></DocumentSvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onDocumentClick,
|
|
||||||
title: 'Document',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
|
||||||
<BoardSvg></BoardSvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onBoardClick,
|
|
||||||
title: 'Board',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
|
||||||
<GridSvg></GridSvg>
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
onClick: onGridClick,
|
|
||||||
title: 'Grid',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopupSelect
|
|
||||||
onOutsideClick={() => onClose && onClose()}
|
|
||||||
items={items}
|
|
||||||
className={'absolute right-0'}
|
|
||||||
style={{ top: `${top}px` }}
|
|
||||||
></PopupSelect>
|
|
||||||
);
|
|
||||||
};
|
|
@ -10,10 +10,10 @@ export const NewViewButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
|||||||
void onNewRootView();
|
void onNewRootView();
|
||||||
scrollDown();
|
scrollDown();
|
||||||
}}
|
}}
|
||||||
className={'flex h-[50px] w-full items-center px-6 hover:bg-fill-active'}
|
className={'flex h-[50px] w-full items-center px-6 hover:bg-fill-list-active'}
|
||||||
>
|
>
|
||||||
<div className={'mr-2 rounded-full bg-fill-default'}>
|
<div className={'mr-2 rounded-full bg-fill-default'}>
|
||||||
<div className={'h-[24px] w-[24px] text-content-onfill'}>
|
<div className={'h-[24px] w-[24px] text-content-on-fill'}>
|
||||||
<AddSvg></AddSvg>
|
<AddSvg></AddSvg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { Button, DialogActions } from '@mui/material';
|
||||||
|
|
||||||
|
function RenameDialog({
|
||||||
|
defaultValue,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onOk,
|
||||||
|
}: {
|
||||||
|
defaultValue: string;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onOk: (val: string) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>{t('menuAppHeader.renameDialog')}</DialogTitle>
|
||||||
|
<DialogContent className={'flex w-[540px]'}>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
}}
|
||||||
|
margin='dense'
|
||||||
|
fullWidth
|
||||||
|
variant='standard'
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>{t('button.Cancel')}</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onOk(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('button.OK')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RenameDialog;
|
@ -1,47 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import useOutsideClick from '../../_shared/useOutsideClick';
|
|
||||||
|
|
||||||
export const RenamePopup = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onClose,
|
|
||||||
className = '',
|
|
||||||
top,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (newTitle: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
className?: string;
|
|
||||||
top?: number;
|
|
||||||
}) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
useOutsideClick(ref, () => onClose && onClose());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!inputRef || !inputRef.current) return;
|
|
||||||
|
|
||||||
const { current: el } = inputRef;
|
|
||||||
|
|
||||||
el.focus();
|
|
||||||
el.selectionStart = 0;
|
|
||||||
el.selectionEnd = el.value.length;
|
|
||||||
}, [inputRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={
|
|
||||||
'absolute left-[50px] top-[40px] z-10 flex w-[300px] rounded bg-white py-1 px-1.5 shadow-md ' + className
|
|
||||||
}
|
|
||||||
style={{ top: `${top}px` }}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
className={'border-shades-3 flex-1 rounded border bg-main-selector p-1'}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -3,7 +3,7 @@ import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
|||||||
|
|
||||||
export const TrashButton = () => {
|
export const TrashButton = () => {
|
||||||
return (
|
return (
|
||||||
<button className={'flex w-full items-center rounded-lg px-4 py-2 text-text-title hover:bg-fill-active'}>
|
<button className={'flex w-full items-center rounded-lg px-4 py-2 text-text-title hover:bg-fill-list-active'}>
|
||||||
<span className={'h-[23px] w-[23px]'}>
|
<span className={'h-[23px] w-[23px]'}>
|
||||||
<TrashSvg />
|
<TrashSvg />
|
||||||
</span>
|
</span>
|
||||||
|
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
|
|||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
import { Theme, ThemeMode, UserSetting } from '$app/interfaces';
|
import { Theme, ThemeMode, UserSetting } from '$app/interfaces';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function AppearanceSetting({
|
function AppearanceSetting({
|
||||||
theme = Theme.Default,
|
theme = Theme.Default,
|
||||||
@ -12,6 +13,8 @@ function AppearanceSetting({
|
|||||||
themeMode?: ThemeMode;
|
themeMode?: ThemeMode;
|
||||||
onChange: (setting: UserSetting) => void;
|
onChange: (setting: UserSetting) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
|
|
||||||
@ -23,14 +26,14 @@ function AppearanceSetting({
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
value: ThemeMode.Light,
|
value: ThemeMode.Light,
|
||||||
content: 'Light',
|
content: t('settings.appearance.themeMode.light'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ThemeMode.Dark,
|
value: ThemeMode.Dark,
|
||||||
content: 'Dark',
|
content: t('settings.appearance.themeMode.dark'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const themeOptions = useMemo(
|
const themeOptions = useMemo(
|
||||||
@ -96,7 +99,7 @@ function AppearanceSetting({
|
|||||||
{renderSelect([
|
{renderSelect([
|
||||||
{
|
{
|
||||||
options: themeModeOptions,
|
options: themeModeOptions,
|
||||||
label: 'Theme Mode',
|
label: t('settings.appearance.themeMode.label'),
|
||||||
value: themeMode,
|
value: themeMode,
|
||||||
onChange: (newValue) => {
|
onChange: (newValue) => {
|
||||||
onChange({
|
onChange({
|
||||||
@ -106,7 +109,7 @@ function AppearanceSetting({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
options: themeOptions,
|
options: themeOptions,
|
||||||
label: 'Theme',
|
label: t('settings.appearance.theme'),
|
||||||
value: theme,
|
value: theme,
|
||||||
onChange: (newValue) => {
|
onChange: (newValue) => {
|
||||||
onChange({
|
onChange({
|
||||||
|
@ -1,7 +1,74 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import { UserSetting } from '$app/interfaces';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
function LanguageSetting() {
|
const languages = [
|
||||||
return <div></div>;
|
{
|
||||||
|
key: 'ar-SA',
|
||||||
|
title: 'العربية',
|
||||||
|
},
|
||||||
|
{ key: 'ca-ES', title: 'Català' },
|
||||||
|
{ key: 'de-DE', title: 'Deutsch' },
|
||||||
|
{ key: 'en', title: 'English' },
|
||||||
|
{ key: 'es-VE', title: 'Español (Venezuela)' },
|
||||||
|
{ key: 'eu-ES', title: 'Español' },
|
||||||
|
{ key: 'fr-FR', title: 'Français' },
|
||||||
|
{ key: 'hu-HU', title: 'Magyar' },
|
||||||
|
{ key: 'id-ID', title: 'Bahasa Indonesia' },
|
||||||
|
{ key: 'it-IT', title: 'Italiano' },
|
||||||
|
{ key: 'ja-JP', title: '日本語' },
|
||||||
|
{ key: 'ko-KR', title: '한국어' },
|
||||||
|
{ key: 'pl-PL', title: 'Polski' },
|
||||||
|
{ key: 'pt-BR', title: 'Português' },
|
||||||
|
{ key: 'pt-PT', title: 'Português' },
|
||||||
|
{ key: 'ru-RU', title: 'Русский' },
|
||||||
|
{ key: 'sv', title: 'Svenska' },
|
||||||
|
{ key: 'tr-TR', title: 'Türkçe' },
|
||||||
|
{ key: 'zh-CN', title: '简体中文' },
|
||||||
|
{ key: 'zh-TW', title: '繁體中文' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function LanguageSetting({
|
||||||
|
language = 'en',
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
language?: string;
|
||||||
|
onChange: (setting: UserSetting) => void;
|
||||||
|
}) {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col'}>
|
||||||
|
<div className={'mb-2 flex items-center justify-between text-sm'}>
|
||||||
|
<div className={'flex-1 text-text-title'}>{t('settings.menu.language')}</div>
|
||||||
|
<div className={'flex items-center'}>
|
||||||
|
<Select
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
}}
|
||||||
|
variant={'standard'}
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => {
|
||||||
|
const language = e.target.value;
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
i18n.changeLanguage(language);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{languages.map((option) => (
|
||||||
|
<MenuItem key={option.key} value={option.key}>
|
||||||
|
{option.title}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LanguageSetting;
|
export default LanguageSetting;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import LanguageIcon from '@mui/icons-material/Language';
|
import LanguageIcon from '@mui/icons-material/Language';
|
||||||
import PaletteOutlined from '@mui/icons-material/PaletteOutlined';
|
import PaletteOutlined from '@mui/icons-material/PaletteOutlined';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export enum MenuItem {
|
export enum MenuItem {
|
||||||
Appearance = 'Appearance',
|
Appearance = 'Appearance',
|
||||||
@ -8,23 +9,25 @@ export enum MenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem) => void; selected: MenuItem }) {
|
function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem) => void; selected: MenuItem }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Appearance',
|
label: t('settings.menu.appearance'),
|
||||||
value: MenuItem.Appearance,
|
value: MenuItem.Appearance,
|
||||||
icon: <PaletteOutlined />,
|
icon: <PaletteOutlined />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Language',
|
label: t('settings.menu.language'),
|
||||||
value: MenuItem.Language,
|
value: MenuItem.Language,
|
||||||
icon: <LanguageIcon />,
|
icon: <LanguageIcon />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'h-[300px] w-[200px] border-r border-solid border-r-line-border pr-2 text-sm'}>
|
<div className={'h-[300px] w-[200px] border-r border-solid border-r-line-border pr-4 text-sm'}>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -33,7 +36,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem
|
|||||||
onSelect(option.value);
|
onSelect(option.value);
|
||||||
}}
|
}}
|
||||||
className={`my-1 flex h-10 w-full cursor-pointer items-center justify-start rounded-md px-4 py-2 text-text-title ${
|
className={`my-1 flex h-10 w-full cursor-pointer items-center justify-start rounded-md px-4 py-2 text-text-title ${
|
||||||
selected === option.value ? 'bg-fill-hover' : 'hover:bg-fill-hover'
|
selected === option.value ? 'bg-fill-list-hover' : 'hover:text-content-blue-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={'mr-2'}>{option.icon}</div>
|
<div className={'mr-2'}>{option.icon}</div>
|
||||||
|
@ -14,7 +14,7 @@ function UserSettingPanel({
|
|||||||
userSettingState?: UserSetting;
|
userSettingState?: UserSetting;
|
||||||
onChange: (setting: Partial<UserSetting>) => void;
|
onChange: (setting: Partial<UserSetting>) => void;
|
||||||
}) {
|
}) {
|
||||||
const { theme, themeMode } = userSettingState;
|
const { theme, themeMode, language } = userSettingState;
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -24,14 +24,14 @@ function UserSettingPanel({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: MenuItem.Language,
|
value: MenuItem.Language,
|
||||||
icon: <LanguageSetting />,
|
content: <LanguageSetting onChange={onChange} language={language} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [onChange, theme, themeMode]);
|
}, [language, onChange, theme, themeMode]);
|
||||||
|
|
||||||
const option = options.find((option) => option.value === selected);
|
const option = options.find((option) => option.value === selected);
|
||||||
|
|
||||||
return <div className={'flex-1 pl-2'}>{option?.content}</div>;
|
return <div className={'flex-1 pl-4'}>{option?.content}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserSettingPanel;
|
export default UserSettingPanel;
|
||||||
|
@ -10,6 +10,7 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
|||||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||||
import { useUserSettingControllerContext } from '$app/components/_shared/app-hooks/useUserSettingControllerContext';
|
import { useUserSettingControllerContext } from '$app/components/_shared/app-hooks/useUserSettingControllerContext';
|
||||||
import { ThemeModePB } from '@/services/backend';
|
import { ThemeModePB } from '@/services/backend';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const SlideTransition = React.forwardRef((props: SlideProps, ref) => {
|
const SlideTransition = React.forwardRef((props: SlideProps, ref) => {
|
||||||
return <Slide {...props} direction='up' ref={ref} />;
|
return <Slide {...props} direction='up' ref={ref} />;
|
||||||
@ -19,7 +20,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
|||||||
const userSettingState = useAppSelector((state) => state.currentUser.userSetting);
|
const userSettingState = useAppSelector((state) => state.currentUser.userSetting);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const userSettingController = useUserSettingControllerContext();
|
const userSettingController = useUserSettingControllerContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [selected, setSelected] = useState<MenuItem>(MenuItem.Appearance);
|
const [selected, setSelected] = useState<MenuItem>(MenuItem.Appearance);
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(setting: Partial<UserSetting>) => {
|
(setting: Partial<UserSetting>) => {
|
||||||
@ -27,9 +28,15 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
|||||||
|
|
||||||
dispatch(currentUserActions.setUserSetting(newSetting));
|
dispatch(currentUserActions.setUserSetting(newSetting));
|
||||||
if (userSettingController) {
|
if (userSettingController) {
|
||||||
|
const language = newSetting.language || 'en';
|
||||||
|
|
||||||
userSettingController.setAppearanceSetting({
|
userSettingController.setAppearanceSetting({
|
||||||
theme: newSetting.theme || Theme.Default,
|
theme: newSetting.theme || Theme.Default,
|
||||||
mode: newSetting.themeMode || ThemeModePB.Light,
|
theme_mode: newSetting.themeMode || ThemeModePB.Light,
|
||||||
|
locale: {
|
||||||
|
language_code: language.split('-')[0],
|
||||||
|
country_code: language.split('-')[1],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -44,7 +51,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
|||||||
keepMounted
|
keepMounted
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<DialogTitle>{'Settings'}</DialogTitle>
|
<DialogTitle>{t('settings.title')}</DialogTitle>
|
||||||
<DialogContent className={'flex w-[540px]'}>
|
<DialogContent className={'flex w-[540px]'}>
|
||||||
<UserSettingMenu
|
<UserSettingMenu
|
||||||
onSelect={(selected) => {
|
onSelect={(selected) => {
|
||||||
|
@ -29,7 +29,7 @@ export const WorkspaceUser = () => {
|
|||||||
<PersonOutline />
|
<PersonOutline />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span className={'ml-2'}>{currentUser.displayName}</span>
|
<span className={'ml-2'}>{currentUser.displayName}</span>
|
||||||
<button className={'ml-1 rounded hover:bg-fill-hover'}>
|
<button className={'ml-1 rounded hover:bg-fill-list-hover'}>
|
||||||
<ArrowDropDown />
|
<ArrowDropDown />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,24 +5,24 @@ export const ColorPalette = () => {
|
|||||||
<h2 className={'mb-4'}>Main</h2>
|
<h2 className={'mb-4'}>Main</h2>
|
||||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||||
<div title={'main-accent'} className={'m-2 h-[100px] w-[100px] bg-fill-default'}></div>
|
<div title={'main-accent'} className={'m-2 h-[100px] w-[100px] bg-fill-default'}></div>
|
||||||
<div title={'main-hovered'} className={'m-2 h-[100px] w-[100px] bg-fill-hover'}></div>
|
<div title={'main-hovered'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||||
<div title={'main-secondary'} className={'m-2 h-[100px] w-[100px] bg-fill-hover'}></div>
|
<div title={'main-secondary'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||||
<div title={'main-selector'} className={'m-2 h-[100px] w-[100px] bg-fill-selector'}></div>
|
<div title={'main-selector'} className={'m-2 h-[100px] w-[100px] bg-fill-selector'}></div>
|
||||||
<div title={'main-alert'} className={'m-2 h-[100px] w-[100px] bg-function-info'}></div>
|
<div title={'main-alert'} className={'m-2 h-[100px] w-[100px] bg-function-info'}></div>
|
||||||
<div title={'main-warning'} className={'m-2 h-[100px] w-[100px] bg-function-warning'}></div>
|
<div title={'main-warning'} className={'m-2 h-[100px] w-[100px] bg-function-warning'}></div>
|
||||||
<div title={'main-success'} className={'m-2 h-[100px] w-[100px] bg-function-success'}></div>
|
<div title={'main-success'} className={'m-2 h-[100px] w-[100px] bg-function-success'}></div>
|
||||||
</div>
|
</div>
|
||||||
<h2 className={'mb-4'}>Tint</h2>
|
<h2 className={'mb-4'}>Tint</h2>
|
||||||
<div className={'mb-8 flex flex-wrap items-center text-content-onfill'}>
|
<div className={'mb-8 flex flex-wrap items-center text-text-title'}>
|
||||||
<div title={'tint-1'} className={'m-2 h-[100px] w-[100px] bg-tint-1'}></div>
|
<div title={'tint-1'} className={'m-2 h-[100px] w-[100px] bg-tint-pink'}></div>
|
||||||
<div title={'tint-2'} className={'m-2 h-[100px] w-[100px] bg-tint-2'}></div>
|
<div title={'tint-2'} className={'m-2 h-[100px] w-[100px] bg-tint-purple'}></div>
|
||||||
<div title={'tint-3'} className={'m-2 h-[100px] w-[100px] bg-tint-3'}></div>
|
<div title={'tint-3'} className={'m-2 h-[100px] w-[100px] bg-tint-red'}></div>
|
||||||
<div title={'tint-4'} className={'m-2 h-[100px] w-[100px] bg-tint-4'}></div>
|
<div title={'tint-4'} className={'m-2 h-[100px] w-[100px] bg-tint-green'}></div>
|
||||||
<div title={'tint-5'} className={'m-2 h-[100px] w-[100px] bg-tint-5'}></div>
|
<div title={'tint-5'} className={'m-2 h-[100px] w-[100px] bg-tint-blue'}></div>
|
||||||
<div title={'tint-6'} className={'m-2 h-[100px] w-[100px] bg-tint-6'}></div>
|
<div title={'tint-6'} className={'m-2 h-[100px] w-[100px] bg-tint-yellow'}></div>
|
||||||
<div title={'tint-7'} className={'m-2 h-[100px] w-[100px] bg-tint-7'}></div>
|
<div title={'tint-7'} className={'m-2 h-[100px] w-[100px] bg-tint-aqua'}></div>
|
||||||
<div title={'tint-8'} className={'m-2 h-[100px] w-[100px] bg-tint-8'}></div>
|
<div title={'tint-8'} className={'m-2 h-[100px] w-[100px] bg-tint-lime'}></div>
|
||||||
<div title={'tint-9'} className={'m-2 h-[100px] w-[100px] bg-tint-9'}></div>
|
<div title={'tint-9'} className={'m-2 h-[100px] w-[100px] bg-tint-pink'}></div>
|
||||||
</div>
|
</div>
|
||||||
<h2 className={'mb-4'}>Shades</h2>
|
<h2 className={'mb-4'}>Shades</h2>
|
||||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user