feat: support ai writer (#69)
* feat: support ai writer * feat: support duplicate page * feat: support upgrade AI Max
@@ -1,7 +1,4 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
import registerCodeCoverageTasks from '@cypress/code-coverage/task';
|
||||
|
||||
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin';
|
||||
|
||||
export default defineConfig({
|
||||
env: {
|
||||
@@ -16,8 +13,6 @@ export default defineConfig({
|
||||
bundler: 'vite',
|
||||
},
|
||||
setupNodeEvents(on, config) {
|
||||
registerCodeCoverageTasks(on, config);
|
||||
addMatchImageSnapshotPlugin(on, config);
|
||||
return config;
|
||||
},
|
||||
supportFile: 'cypress/support/component.ts',
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"coverage": "pnpm run test:unit && pnpm run test:components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appflowyinc/ai-chat": "0.0.15",
|
||||
"@appflowyinc/editor": "^0.1.6",
|
||||
"@appflowyinc/ai-chat": "0.1.24",
|
||||
"@appflowyinc/editor": "^0.1.10",
|
||||
"@atlaskit/primitives": "^5.5.3",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
|
||||
143
pnpm-lock.yaml
generated
@@ -2,11 +2,11 @@ lockfileVersion: '6.0'
|
||||
|
||||
dependencies:
|
||||
'@appflowyinc/ai-chat':
|
||||
specifier: 0.0.15
|
||||
version: 0.0.15(@appflowyinc/editor@0.1.6)(@types/react-dom@18.2.22)(@types/react@18.2.66)(axios@1.7.2)(dompurify@3.1.7)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)
|
||||
specifier: 0.1.24
|
||||
version: 0.1.24(@appflowyinc/editor@0.1.10)(@types/react-dom@18.2.22)(@types/react@18.2.66)(axios@1.7.2)(dompurify@3.1.7)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)
|
||||
'@appflowyinc/editor':
|
||||
specifier: ^0.1.6
|
||||
version: 0.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5)
|
||||
specifier: ^0.1.10
|
||||
version: 0.1.10(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5)
|
||||
'@atlaskit/primitives':
|
||||
specifier: ^5.5.3
|
||||
version: 5.7.0(@types/react@18.2.66)(react@18.2.0)
|
||||
@@ -535,10 +535,10 @@ packages:
|
||||
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
||||
dev: false
|
||||
|
||||
/@appflowyinc/ai-chat@0.0.15(@appflowyinc/editor@0.1.6)(@types/react-dom@18.2.22)(@types/react@18.2.66)(axios@1.7.2)(dompurify@3.1.7)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0):
|
||||
resolution: {integrity: sha512-vpF73487ARBFrxO7BywRsCWDEKSTyhT5SgPyZetBRsyVwLju6iYtFTyf7N7FdLsl1mWZnJUBcvi7YWdWEqno6A==}
|
||||
/@appflowyinc/ai-chat@0.1.24(@appflowyinc/editor@0.1.10)(@types/react-dom@18.2.22)(@types/react@18.2.66)(axios@1.7.2)(dompurify@3.1.7)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0):
|
||||
resolution: {integrity: sha512-A8NtOFMlI0jzJ+vC0GfLdRoD8WMVGUV3ypLNBm+8fnG5r8Z8aSe0Zu9Y3BRowyV+IvT3vZFbH/K1jUiIUxgFLw==}
|
||||
peerDependencies:
|
||||
'@appflowyinc/editor': ^0.1.6
|
||||
'@appflowyinc/editor': ^0.1.7
|
||||
axios: ^1.7.9
|
||||
dompurify: ^3.2.4
|
||||
i18next: ^22.4.10
|
||||
@@ -547,11 +547,13 @@ packages:
|
||||
react-dom: ^18.2.0
|
||||
react-i18next: ^14.1.0
|
||||
dependencies:
|
||||
'@appflowyinc/editor': 0.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5)
|
||||
'@appflowyinc/editor': 0.1.10(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5)
|
||||
'@jest/globals': 29.7.0
|
||||
'@radix-ui/react-avatar': 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-dialog': 1.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-label': 2.1.1(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-popover': 1.1.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-radio-group': 1.2.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-select': 2.1.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-separator': 1.1.1(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.1.2(@types/react@18.2.66)(react@18.2.0)
|
||||
@@ -573,6 +575,7 @@ packages:
|
||||
react-i18next: 14.1.2(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
react-infinite-scroll-component: 6.1.0(react@18.2.0)
|
||||
sass: 1.83.0
|
||||
smooth-scroll-into-view-if-needed: 2.0.2
|
||||
tailwind-merge: 2.5.5
|
||||
tailwindcss: 3.4.17
|
||||
tailwindcss-animate: 1.0.7(tailwindcss@3.4.17)
|
||||
@@ -586,8 +589,8 @@ packages:
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
/@appflowyinc/editor@0.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5):
|
||||
resolution: {integrity: sha512-IR+SfRM1E5QWTl4Q83GZ+XXSEYH/27nxj5Dy5Zfv697D7qirHENggU4lJBUXqRBXHSoKWB5wDJunF/t9HY+E9w==}
|
||||
/@appflowyinc/editor@0.1.10(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5):
|
||||
resolution: {integrity: sha512-2/IcZ1FTIEhv9DzVdCu+9WvOnjSLENU3j08Uu2Pep52EsoYQpEHpdeKaYKqBpwESFGwI0vc2sOXFyKOel8UgTg==}
|
||||
peerDependencies:
|
||||
i18next: ^22.4.10
|
||||
i18next-resources-to-backend: ^1.2.1
|
||||
@@ -4056,6 +4059,39 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dialog@1.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-portal': 1.1.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.1.2(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@types/react': 18.2.66
|
||||
'@types/react-dom': 18.2.22
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.6.3(@types/react@18.2.66)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-direction@1.1.0(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
||||
peerDependencies:
|
||||
@@ -4152,6 +4188,28 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-scope@1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@types/react': 18.2.66
|
||||
'@types/react-dom': 18.2.22
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-id@1.1.0(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
|
||||
peerDependencies:
|
||||
@@ -4217,7 +4275,7 @@ packages:
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.6.2(@types/react@18.2.66)(react@18.2.0)
|
||||
react-remove-scroll: 2.6.3(@types/react@18.2.66)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-popper@1.2.1(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
@@ -4352,6 +4410,63 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@types/react': 18.2.66
|
||||
'@types/react-dom': 18.2.22
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-roving-focus@1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-collection': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.66)(react@18.2.0)
|
||||
'@types/react': 18.2.66
|
||||
'@types/react-dom': 18.2.22
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-select@2.1.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==}
|
||||
peerDependencies:
|
||||
@@ -4389,7 +4504,7 @@ packages:
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.6.2(@types/react@18.2.66)(react@18.2.0)
|
||||
react-remove-scroll: 2.6.3(@types/react@18.2.66)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-separator@1.1.1(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0):
|
||||
@@ -12742,8 +12857,8 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/react-remove-scroll@2.6.2(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==}
|
||||
/react-remove-scroll@2.6.3(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
|
||||
@@ -264,7 +264,8 @@
|
||||
"questionBubble": {
|
||||
"shortcuts": "Shortcuts",
|
||||
"whatsNew": "What's new?",
|
||||
"help": "Help & Support",
|
||||
"documentation": "Help & documentation",
|
||||
"help": "Get support",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "Debug Info",
|
||||
@@ -1687,7 +1688,9 @@
|
||||
"aiWriter": "AI Writer",
|
||||
"dateOrReminder": "Date or Reminder",
|
||||
"photoGallery": "Photo Gallery",
|
||||
"file": "File"
|
||||
"file": "File",
|
||||
"continueWriting": "Continue Writing",
|
||||
"askAIAnything": "Ask AI Anything"
|
||||
},
|
||||
"subPage": {
|
||||
"name": "Document",
|
||||
@@ -2446,7 +2449,9 @@
|
||||
"toggleListShortForm": "Toggle",
|
||||
"quoteListShortForm": "Quote",
|
||||
"mathEquationShortForm": "Formula",
|
||||
"codeBlockShortForm": "Code"
|
||||
"codeBlockShortForm": "Code",
|
||||
"improveWriting": "Improve writing",
|
||||
"askAI": "Ask AI Anything"
|
||||
},
|
||||
"favorite": {
|
||||
"noFavorite": "No favorite page",
|
||||
@@ -2978,6 +2983,21 @@
|
||||
"priceIn": "Price in ",
|
||||
"free": "Free",
|
||||
"pro": "Pro",
|
||||
"getAIMax": "Get AI Max",
|
||||
"upgradeAIMax": "Upgrade to AI Max",
|
||||
"unlock": "Unlock",
|
||||
"AIMax": {
|
||||
"label": "AI Max",
|
||||
"removeTitle": "Remove AI Max",
|
||||
"removeDescription": "Are you sure you want to remove AI Max? You will lose access to the features and benefits of AI Max immediately.",
|
||||
"description": "Access the most advanced AI models including GPT-4o, GPT-o3-mini, DeepSeek R1, and Claude 3.5 Sonnet",
|
||||
"pricing": "per member per month\nbilled annually",
|
||||
"points": {
|
||||
"first": "Unlimited AI responses",
|
||||
"second": "Unlimited file uploads",
|
||||
"third": "50 AI images per month"
|
||||
}
|
||||
},
|
||||
"freeDescription": "For individuals up to 2 members to organize everything",
|
||||
"proDescription": "For small teams to manage projects and team knowledge",
|
||||
"proDuration": {
|
||||
@@ -2995,15 +3015,17 @@
|
||||
"three": "5 GB storage",
|
||||
"four": "Intelligent search",
|
||||
"five": "10 AI responses",
|
||||
"six": "Mobile app",
|
||||
"seven": "Real-time collaboration"
|
||||
"six": "2 AI images",
|
||||
"seven": "Mobile app",
|
||||
"eight": "Real-time collaboration"
|
||||
},
|
||||
"proPoints": {
|
||||
"first": "Unlimited storage",
|
||||
"second": "Up to 10 workspace members",
|
||||
"three": "Unlimited AI responses",
|
||||
"four": "Unlimited file uploads",
|
||||
"five": "Custom namespace"
|
||||
"four": "10 AI images per month",
|
||||
"five": "Unlimited file uploads",
|
||||
"six": "Custom namespace"
|
||||
},
|
||||
"cancelPlan": {
|
||||
"title": "Sorry to see you go",
|
||||
|
||||
@@ -35,6 +35,7 @@ const wrapperCreator =
|
||||
databaseDoc={doc}
|
||||
rowDocMap={rowDocMap}
|
||||
readOnly={true}
|
||||
workspaceId={''}
|
||||
>
|
||||
{children}
|
||||
</DatabaseContextProvider>
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface DatabaseContextState {
|
||||
navigateToView?: (viewId: string, blockId?: string) => Promise<void>;
|
||||
onRendered?: (height: number) => void;
|
||||
showActions?: boolean;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export const DatabaseContext = createContext<DatabaseContextState | null>(null);
|
||||
@@ -33,7 +34,7 @@ export const DatabaseContext = createContext<DatabaseContextState | null>(null);
|
||||
export const useDatabaseContext = () => {
|
||||
const context = useContext(DatabaseContext);
|
||||
|
||||
if (!context) {
|
||||
if(!context) {
|
||||
throw new Error('DatabaseContext is not provided');
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ export const useDatabaseView = () => {
|
||||
return viewId ? database?.get(YjsDatabaseKey.views)?.get(viewId) : undefined;
|
||||
};
|
||||
|
||||
export function useDatabaseFields () {
|
||||
export function useDatabaseFields() {
|
||||
const database = useDatabase();
|
||||
|
||||
return database.get(YjsDatabaseKey.fields);
|
||||
|
||||
@@ -1855,7 +1855,7 @@ export async function getChatMessages(workspaceId: string, chatId: string, limit
|
||||
data?: RepeatedChatMessage;
|
||||
message: string;
|
||||
}>(url, {
|
||||
params: { limit: limit},
|
||||
params: { limit: limit },
|
||||
});
|
||||
|
||||
const data = response?.data;
|
||||
@@ -1866,3 +1866,17 @@ export async function getChatMessages(workspaceId: string, chatId: string, limit
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function duplicatePage(workspaceId: string, viewId: string) {
|
||||
const url = `/api/workspace/${workspaceId}/page-view/${viewId}/duplicate`;
|
||||
const response = await axiosInstance?.post<{
|
||||
code: number;
|
||||
message: string;
|
||||
}>(url, {});
|
||||
|
||||
if(response?.data.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(response?.data);
|
||||
}
|
||||
@@ -581,6 +581,10 @@ export class AFClientService implements AFService {
|
||||
return APIService.updatePage(workspaceId, viewId, data);
|
||||
}
|
||||
|
||||
async duplicateAppPage(workspaceId: string, viewId: string) {
|
||||
return APIService.duplicatePage(workspaceId, viewId);
|
||||
}
|
||||
|
||||
async deleteTrash(workspaceId: string, viewId?: string) {
|
||||
return APIService.deleteTrash(workspaceId, viewId);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,14 @@ import {
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { RepeatedChatMessage } from '@appflowyinc/ai-chat/dist/types';
|
||||
|
||||
export type AFService = PublishService & AppService & WorkspaceService & TemplateService & QuickNoteService & AIChatService & {
|
||||
export type AFService =
|
||||
PublishService
|
||||
& AppService
|
||||
& WorkspaceService
|
||||
& TemplateService
|
||||
& QuickNoteService
|
||||
& AIChatService
|
||||
& {
|
||||
getClientId: () => string;
|
||||
getAxiosInstance: () => AxiosInstance | null;
|
||||
};
|
||||
@@ -110,7 +117,7 @@ export interface AppService {
|
||||
restoreFromTrash: (workspaceId: string, viewId?: string) => Promise<void>;
|
||||
movePage: (workspaceId: string, viewId: string, parentId: string, prevViewId?: string) => Promise<void>;
|
||||
uploadFile: (workspaceId: string, viewId: string, file: File, onProgress?: (progress: number) => void) => Promise<string>;
|
||||
|
||||
duplicateAppPage: (workspaceId: string, viewId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface QuickNoteService {
|
||||
@@ -179,7 +186,7 @@ export interface PublishService {
|
||||
|
||||
}
|
||||
|
||||
export interface AIChatService{
|
||||
export interface AIChatService {
|
||||
getChatMessages: (
|
||||
workspaceId: string,
|
||||
chatId: string,
|
||||
|
||||
@@ -45,6 +45,21 @@ import {
|
||||
} from '@/application/slate-yjs/utils/yjs';
|
||||
|
||||
export const CustomEditor = {
|
||||
getEditorContent(editor: YjsEditor) {
|
||||
const allNodes = editor.children ?? [];
|
||||
|
||||
return allNodes.map((node) => {
|
||||
return CustomEditor.getBlockTextContent(node);
|
||||
}).join('\n');
|
||||
},
|
||||
|
||||
getSelectionContent(editor: YjsEditor, range?: Range) {
|
||||
const at = range || editor.selection;
|
||||
|
||||
if(!at) return '';
|
||||
|
||||
return editor.string(at);
|
||||
},
|
||||
// Get the text content of a block node, including the text content of its children and formula nodes
|
||||
getBlockTextContent(node: Node, depth: number = Infinity): string {
|
||||
if(Text.isText(node)) {
|
||||
@@ -78,7 +93,7 @@ export const CustomEditor = {
|
||||
},
|
||||
|
||||
setBlockData<T = BlockData>(editor: YjsEditor, blockId: string, updateData: T, select?: boolean) {
|
||||
|
||||
|
||||
const block = getBlock(blockId, editor.sharedRoot);
|
||||
const oldData = dataStringTOJson(block.get(YjsEditorKey.block_data));
|
||||
const newData = {
|
||||
|
||||
@@ -17,17 +17,17 @@ export function translateYEvents(editor: YjsEditor, events: Array<YEvent>) {
|
||||
|
||||
events.forEach((event) => {
|
||||
console.log(event.path);
|
||||
if (isEqual(event.path, ['document', 'blocks'])) {
|
||||
if(isEqual(event.path, ['document', 'blocks'])) {
|
||||
applyBlocksYEvent(editor, event as BlockMapEvent);
|
||||
}
|
||||
|
||||
if (isEqual((event.path), ['document', 'blocks', event.path[2]])) {
|
||||
if(isEqual((event.path), ['document', 'blocks', event.path[2]])) {
|
||||
const blockId = event.path[2] as string;
|
||||
|
||||
applyUpdateBlockYEvent(editor, blockId, event as YMapEvent<unknown>);
|
||||
}
|
||||
|
||||
if (isEqual(event.path, ['document', 'meta', 'text_map', event.path[3]])) {
|
||||
if(isEqual(event.path, ['document', 'meta', 'text_map', event.path[3]])) {
|
||||
const textId = event.path[3] as string;
|
||||
|
||||
applyTextYEvent(editor, textId, event as YTextEvent);
|
||||
@@ -42,7 +42,7 @@ function applyUpdateBlockYEvent(editor: YjsEditor, blockId: string, event: YMapE
|
||||
const newData = dataStringTOJson(block.get(YjsEditorKey.block_data));
|
||||
const entry = findSlateEntryByBlockId(editor, blockId);
|
||||
|
||||
if (!entry) {
|
||||
if(!entry) {
|
||||
console.error('Block node not found', blockId);
|
||||
return [];
|
||||
}
|
||||
@@ -75,7 +75,7 @@ function applyTextYEvent(editor: YjsEditor, textId: string, event: YTextEvent) {
|
||||
});
|
||||
|
||||
console.log('=== Applying text Yjs event ===', entry);
|
||||
if (!entry) {
|
||||
if(!entry) {
|
||||
console.error('Text node not found', textId);
|
||||
return [];
|
||||
}
|
||||
@@ -107,14 +107,14 @@ function applyBlocksYEvent(editor: YjsEditor, event: BlockMapEvent) {
|
||||
keysChanged.forEach((key: string) => {
|
||||
const value = keys.get(key);
|
||||
|
||||
if (!value) return;
|
||||
if(!value) return;
|
||||
|
||||
if (value.action === 'add') {
|
||||
if(value.action === 'add') {
|
||||
handleNewBlock(editor, key, keyPath);
|
||||
|
||||
} else if (value.action === 'delete') {
|
||||
} else if(value.action === 'delete') {
|
||||
handleDeleteNode(editor, key);
|
||||
} else if (value.action === 'update') {
|
||||
} else if(value.action === 'update') {
|
||||
console.log('=== Applying block update Yjs event ===', key);
|
||||
}
|
||||
});
|
||||
@@ -127,7 +127,7 @@ function handleNewBlock(editor: YjsEditor, key: string, keyPath: Record<string,
|
||||
const pageId = getPageId(editor.sharedRoot);
|
||||
const parent = getBlock(parentId, editor.sharedRoot);
|
||||
|
||||
if (!parent) {
|
||||
if(!parent) {
|
||||
console.error('Parent block not found', parentId, block.toJSON());
|
||||
return;
|
||||
}
|
||||
@@ -139,11 +139,11 @@ function handleNewBlock(editor: YjsEditor, key: string, keyPath: Record<string,
|
||||
const yText = getText(textId, editor.sharedRoot);
|
||||
let textNode: Element | undefined;
|
||||
|
||||
if (yText) {
|
||||
if(yText) {
|
||||
const delta = yText?.toDelta();
|
||||
const slateDelta = delta.flatMap(deltaInsertToSlateNode);
|
||||
|
||||
if (slateDelta.length === 0) {
|
||||
if(slateDelta.length === 0) {
|
||||
slateDelta.push({
|
||||
text: '',
|
||||
});
|
||||
@@ -158,15 +158,15 @@ function handleNewBlock(editor: YjsEditor, key: string, keyPath: Record<string,
|
||||
|
||||
let path = [index];
|
||||
|
||||
if (parentId !== pageId) {
|
||||
if(parentId !== pageId) {
|
||||
const [parentEntry] = editor.nodes({
|
||||
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId === parentId,
|
||||
mode: 'all',
|
||||
at: [],
|
||||
});
|
||||
|
||||
if (!parentEntry) {
|
||||
if (keyPath[parentId]) {
|
||||
if(!parentEntry) {
|
||||
if(keyPath[parentId]) {
|
||||
path = [...keyPath[parentId], index + 1];
|
||||
} else {
|
||||
console.error('Parent block not found', parentId);
|
||||
@@ -198,7 +198,7 @@ function handleDeleteNode(editor: YjsEditor, key: string) {
|
||||
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId === key,
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
if(!entry) {
|
||||
console.error('Block not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import { getBlock, getText } from '@/application/slate-yjs/utils/yjs';
|
||||
|
||||
// transform slate op to yjs op and apply it to ydoc
|
||||
export function applyToYjs(ydoc: Y.Doc, editor: Editor, op: Operation, slateContent: Descendant[]) {
|
||||
if (op.type === 'set_selection') return;
|
||||
if(op.type === 'set_selection') return;
|
||||
console.log('applySlateOp', op, slateContent);
|
||||
|
||||
switch (op.type) {
|
||||
switch(op.type) {
|
||||
case 'insert_text':
|
||||
return applyInsertText(ydoc, editor, op, slateContent);
|
||||
case 'remove_text':
|
||||
@@ -38,11 +38,11 @@ function getAttributesAtOffset(ytext: Y.Text, offset: number): object | null {
|
||||
const delta = ytext.toDelta();
|
||||
let currentOffset = 0;
|
||||
|
||||
for (const op of delta) {
|
||||
if ('insert' in op) {
|
||||
for(const op of delta) {
|
||||
if('insert' in op) {
|
||||
const length = op.insert.length;
|
||||
|
||||
if (currentOffset <= offset && offset < currentOffset + length) {
|
||||
if(currentOffset <= offset && offset < currentOffset + length) {
|
||||
return op.attributes || null;
|
||||
}
|
||||
|
||||
@@ -63,25 +63,25 @@ function insertText(ydoc: Y.Doc, editor: Editor, { path, offset, text, attribute
|
||||
const sharedRoot = ydoc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||
const yText = getText(textId, sharedRoot);
|
||||
|
||||
if (!yText) return;
|
||||
if(!yText) return;
|
||||
const point = { path, offset };
|
||||
|
||||
const relativeOffset = Math.min(calculateOffsetRelativeToParent(node, point), yText.toJSON().length);
|
||||
|
||||
const beforeAttributes = getAttributesAtOffset(yText, relativeOffset - 1);
|
||||
|
||||
if (beforeAttributes && ('formula' in beforeAttributes || 'mention' in beforeAttributes)) {
|
||||
if(beforeAttributes && ('formula' in beforeAttributes || 'mention' in beforeAttributes)) {
|
||||
const newAttributes = {
|
||||
...attributes,
|
||||
};
|
||||
|
||||
if ('formula' in beforeAttributes) {
|
||||
if('formula' in beforeAttributes) {
|
||||
Object.assign({
|
||||
formula: null,
|
||||
});
|
||||
}
|
||||
|
||||
if ('mention' in beforeAttributes) {
|
||||
if('mention' in beforeAttributes) {
|
||||
Object.assign({
|
||||
mention: null,
|
||||
});
|
||||
@@ -105,7 +105,7 @@ function applyInsertText(ydoc: Y.Doc, editor: Editor, op: InsertTextOperation, s
|
||||
function applyInsertNode(ydoc: Y.Doc, editor: Editor, op: InsertNodeOperation, slateContent: Descendant[]) {
|
||||
const { path, node } = op;
|
||||
|
||||
if (!Text.isText(node)) return;
|
||||
if(!Text.isText(node)) return;
|
||||
const { text, ...attributes } = node;
|
||||
const offset = 0;
|
||||
|
||||
@@ -121,7 +121,7 @@ function applyRemoveText(ydoc: Y.Doc, editor: Editor, op: RemoveTextOperation, s
|
||||
|
||||
const textId = node.textId;
|
||||
|
||||
if (!textId) {
|
||||
if(!textId) {
|
||||
console.error('textId not found', node);
|
||||
return;
|
||||
}
|
||||
@@ -129,8 +129,8 @@ function applyRemoveText(ydoc: Y.Doc, editor: Editor, op: RemoveTextOperation, s
|
||||
const sharedRoot = ydoc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||
const yText = getText(textId, sharedRoot);
|
||||
|
||||
if (!yText) {
|
||||
console.error('yText not found', textId, sharedRoot.toJSON());
|
||||
if(!yText) {
|
||||
console.error('yText not found', textId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,11 +152,11 @@ function applySetNode(ydoc: Y.Doc, editor: Editor, op: SetNodeOperation, slateCo
|
||||
const sharedRoot = ydoc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||
|
||||
console.log('applySetNode isLeaf', isLeaf, op);
|
||||
if (isLeaf) {
|
||||
if(isLeaf) {
|
||||
const node = getNodeAtPath(slateContent, path.slice(0, -1)) as Element;
|
||||
const textId = node.textId;
|
||||
|
||||
if (!textId) return;
|
||||
if(!textId) return;
|
||||
|
||||
const yText = getText(textId, sharedRoot);
|
||||
const start = {
|
||||
@@ -179,7 +179,7 @@ function applySetNode(ydoc: Y.Doc, editor: Editor, op: SetNodeOperation, slateCo
|
||||
};
|
||||
|
||||
Object.entries(properties).forEach(([key, val]) => {
|
||||
if (val && !(key in newProperties)) {
|
||||
if(val && !(key in newProperties)) {
|
||||
formats[key] = null;
|
||||
}
|
||||
});
|
||||
@@ -188,18 +188,18 @@ function applySetNode(ydoc: Y.Doc, editor: Editor, op: SetNodeOperation, slateCo
|
||||
return;
|
||||
}
|
||||
|
||||
if (isData) {
|
||||
if(isData) {
|
||||
const node = getNodeAtPath(slateContent, path) as Element;
|
||||
const blockId = node.blockId as string;
|
||||
|
||||
if (!blockId) {
|
||||
if(!blockId) {
|
||||
console.error('blockId is not found in node', node, newProperties);
|
||||
return;
|
||||
}
|
||||
|
||||
const block = getBlock(blockId, sharedRoot);
|
||||
|
||||
if (
|
||||
if(
|
||||
'data' in newProperties
|
||||
) {
|
||||
block.set(YjsEditorKey.block_data, JSON.stringify(newProperties.data));
|
||||
|
||||
@@ -26,7 +26,13 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un
|
||||
|
||||
if(!block) {
|
||||
console.error('Block not found', id);
|
||||
return;
|
||||
return {
|
||||
blockId: id,
|
||||
type: 'block_not_found',
|
||||
data: {},
|
||||
relationId: id,
|
||||
children: [{ type: 'text', textId: id, children: [{ text: '' }] }],
|
||||
};
|
||||
}
|
||||
|
||||
const childrenId = block.children as string;
|
||||
|
||||
@@ -990,4 +990,3 @@ export function addBlock(editor: YjsEditor, {
|
||||
|
||||
return newBlockId;
|
||||
}
|
||||
|
||||
|
||||
@@ -236,15 +236,10 @@ export function deleteBlock(sharedRoot: YSharedRoot, blockId: string) {
|
||||
deleteBlock(sharedRoot, id);
|
||||
});
|
||||
|
||||
blocks.delete(blockId);
|
||||
|
||||
const meta = document.get(YjsEditorKey.meta) as YMeta;
|
||||
const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap;
|
||||
const textMap = meta.get(YjsEditorKey.text_map) as YTextMap;
|
||||
|
||||
childrenMap.delete(blockId);
|
||||
textMap.delete(blockId);
|
||||
|
||||
const parent = getBlock(parentId, sharedRoot);
|
||||
|
||||
if(!parent) return;
|
||||
@@ -256,8 +251,14 @@ export function deleteBlock(sharedRoot: YSharedRoot, blockId: string) {
|
||||
|
||||
if(index !== -1) {
|
||||
parentChildren.delete(index, 1);
|
||||
} else {
|
||||
console.info('Block not found in parent\'s children');
|
||||
}
|
||||
|
||||
blocks.delete(blockId);
|
||||
childrenMap.delete(blockId);
|
||||
textMap.delete(blockId);
|
||||
|
||||
// delete parent if it's empty column block
|
||||
if(parentType === BlockType.ColumnBlock && afterDeletedLength === 0) {
|
||||
deleteBlock(sharedRoot, parentId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export type BlockId = string;
|
||||
@@ -956,6 +957,7 @@ export enum SubscriptionPlan {
|
||||
Free = 'free',
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
AIMax = 'ai_max'
|
||||
}
|
||||
|
||||
export enum SubscriptionInterval {
|
||||
@@ -991,6 +993,7 @@ export interface ViewMetaProps {
|
||||
cover?: ViewMetaCover;
|
||||
name?: string;
|
||||
viewId?: string;
|
||||
workspaceId?: string;
|
||||
layout?: ViewLayout;
|
||||
visibleViewIds?: string[];
|
||||
extra?: ViewExtra | null;
|
||||
@@ -1008,6 +1011,7 @@ export interface TextCount {
|
||||
|
||||
export interface ViewComponentProps {
|
||||
doc: YDoc;
|
||||
workspaceId: string;
|
||||
readOnly: boolean;
|
||||
navigateToView?: (viewId: string, blockId?: string) => Promise<void>;
|
||||
loadViewMeta?: LoadViewMeta;
|
||||
@@ -1025,6 +1029,7 @@ export interface ViewComponentProps {
|
||||
loadViews?: (variant?: UIVariant) => Promise<View[] | undefined>;
|
||||
onWordCountChange?: (viewId: string, props: TextCount) => void;
|
||||
uploadFile?: (file: File) => Promise<string>;
|
||||
requestInstance?: AxiosInstance | null;
|
||||
}
|
||||
|
||||
export interface CreatePagePayload {
|
||||
|
||||
@@ -13,11 +13,11 @@ export function applyYDoc(doc: Y.Doc, state: Uint8Array) {
|
||||
() => {
|
||||
try {
|
||||
Y.applyUpdate(doc, state, CollabOrigin.Remote);
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
console.error('Error applying', doc, e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
CollabOrigin.Remote
|
||||
CollabOrigin.Remote,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M10.3542 4.33333C10.3542 2.49233 8.86084 1 7.02084 1C5.18084 1 3.68751 2.49233 3.68751 4.33333C3.68751 6.17433 5.18084 7.66667 7.02084 7.66667C8.86084 7.66667 10.3542 6.17433 10.3542 4.33333ZM7.02084 6.33333C5.9175 6.33333 5.02084 5.438 5.02084 4.33333C5.02084 3.22867 5.9175 2.33333 7.02084 2.33333C8.12417 2.33333 9.02084 3.22867 9.02084 4.33333C9.02084 5.438 8.12417 6.33333 7.02084 6.33333Z"
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.3333 5.21932C10.3333 3.37832 8.83996 1.88599 6.99996 1.88599C5.15996 1.88599 3.66663 3.37832 3.66663 5.21932C3.66663 7.06032 5.15996 8.55265 6.99996 8.55265C8.83996 8.55265 10.3333 7.06032 10.3333 5.21932ZM6.99996 7.21932C5.89663 7.21932 4.99997 6.32399 4.99997 5.21932C4.99997 4.11465 5.89663 3.21932 6.99996 3.21932C8.1033 3.21932 8.99996 4.11465 8.99996 5.21932C8.99996 6.32399 8.1033 7.21932 6.99996 7.21932Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M2.02083 12.6667C2.02083 12.4363 2.05083 12.213 2.10417 12C2.40083 10.85 3.44417 10 4.6875 10H8.67366V8.66667H4.6875C2.4775 8.66667 0.6875 10.4577 0.6875 12.6667V13.3333C0.6875 14.0667 1.2875 14.6667 2.02083 14.6667H8.65702V13.3333H2.02083V12.6667Z"
|
||||
<path d="M1.99996 13.5527C1.99996 13.3223 2.02996 13.099 2.08329 12.886C2.37996 11.736 3.42329 10.886 4.66663 10.886H8.65279V9.55265H4.66663C2.45663 9.55265 0.666626 11.3437 0.666626 13.5527V14.2193C0.666626 14.9527 1.26663 15.5527 1.99996 15.5527H8.63615V14.2193H1.99996V13.5527Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M12.3542 14.6667C11.986 14.6667 11.6875 14.3682 11.6875 14V12.6667H10.3542C9.98598 12.6667 9.6875 12.3682 9.6875 12C9.6875 11.6318 9.98598 11.3333 10.3542 11.3333H11.6875V10C11.6875 9.63181 11.986 9.33333 12.3542 9.33333C12.7224 9.33333 13.0208 9.63181 13.0208 10V11.3333H14.3542C14.7224 11.3333 15.0208 11.6318 15.0208 12C15.0208 12.3682 14.7224 12.6667 14.3542 12.6667H13.0208V14C13.0208 14.3682 12.7224 14.6667 12.3542 14.6667Z"
|
||||
<path d="M12.3333 15.5527C11.9651 15.5527 11.6666 15.2542 11.6666 14.886V13.5527H10.3333C9.9651 13.5527 9.66663 13.2542 9.66663 12.886C9.66663 12.5178 9.9651 12.2193 10.3333 12.2193H11.6666V10.886C11.6666 10.5178 11.9651 10.2193 12.3333 10.2193C12.7015 10.2193 13 10.5178 13 10.886V12.2193H14.3333C14.7015 12.2193 15 12.5178 15 12.886C15 13.2542 14.7015 13.5527 14.3333 13.5527H13V14.886C13 15.2542 12.7015 15.5527 12.3333 15.5527Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M10.3542 4.33333C10.3542 2.49233 8.86084 1 7.02084 1C5.18084 1 3.68751 2.49233 3.68751 4.33333C3.68751 6.17433 5.18084 7.66667 7.02084 7.66667C8.86084 7.66667 10.3542 6.17433 10.3542 4.33333ZM7.02084 6.33333C5.9175 6.33333 5.02084 5.438 5.02084 4.33333C5.02084 3.22867 5.9175 2.33333 7.02084 2.33333C8.12417 2.33333 9.02084 3.22867 9.02084 4.33333C9.02084 5.438 8.12417 6.33333 7.02084 6.33333Z"
|
||||
stroke="#F7F8FC" stroke-width="0.177778"/>
|
||||
<path d="M2.02083 12.6667C2.02083 12.4363 2.05083 12.213 2.10417 12C2.40083 10.85 3.44417 10 4.6875 10H8.67366V8.66667H4.6875C2.4775 8.66667 0.6875 10.4577 0.6875 12.6667V13.3333C0.6875 14.0667 1.2875 14.6667 2.02083 14.6667H8.65702V13.3333H2.02083V12.6667Z"
|
||||
stroke="#F7F8FC" stroke-width="0.177778"/>
|
||||
<path d="M12.3542 14.6667C11.986 14.6667 11.6875 14.3682 11.6875 14V12.6667H10.3542C9.98598 12.6667 9.6875 12.3682 9.6875 12C9.6875 11.6318 9.98598 11.3333 10.3542 11.3333H11.6875V10C11.6875 9.63181 11.986 9.33333 12.3542 9.33333C12.7224 9.33333 13.0208 9.63181 13.0208 10V11.3333H14.3542C14.7224 11.3333 15.0208 11.6318 15.0208 12C15.0208 12.3682 14.7224 12.6667 14.3542 12.6667H13.0208V14C13.0208 14.3682 12.7224 14.6667 12.3542 14.6667Z"
|
||||
stroke="#F7F8FC" stroke-width="0.177778"/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
7
src/assets/ai.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.03338 4.65784C8.37931 3.78072 9.62067 3.78072 9.9666 4.65785L11.0386 7.37598C11.1442 7.64377 11.3562 7.85575 11.624 7.96136L14.3422 9.03338C15.2193 9.37931 15.2193 10.6207 14.3422 10.9666L11.624 12.0386C11.3562 12.1442 11.1442 12.3562 11.0386 12.624L9.9666 15.3422C9.62066 16.2193 8.37931 16.2193 8.03338 15.3422L6.96136 12.624C6.85574 12.3562 6.64377 12.1442 6.37598 12.0386L3.65784 10.9666C2.78072 10.6207 2.78072 9.37931 3.65785 9.03338L6.37598 7.96136C6.64377 7.85574 6.85575 7.64377 6.96136 7.37598L8.03338 4.65784Z"
|
||||
stroke="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M15.2788 2.18536C15.1765 1.93826 14.8264 1.9382 14.7239 2.18526L14.5912 2.5053C14.3879 2.99548 13.9983 3.3849 13.5079 3.58798L13.1854 3.72156C12.9382 3.82396 12.9382 4.17415 13.1855 4.27647L13.5049 4.40864C13.9962 4.61192 14.3865 5.00222 14.5896 5.49348L14.7224 5.81445C14.8247 6.0618 15.1751 6.06186 15.2774 5.81455L15.4093 5.49609C15.6125 5.00523 16.0025 4.61524 16.4934 4.41202L16.8158 4.27854C17.063 4.1762 17.0631 3.8261 16.8159 3.72367L16.4957 3.59096C16.0053 3.38775 15.6156 2.99814 15.4124 2.50778L15.2788 2.18536ZM15.2788 14.1854C15.1765 13.9383 14.8264 13.9382 14.7239 14.1853L14.5912 14.5053C14.3879 14.9955 13.9983 15.3849 13.5079 15.588L13.1854 15.7216C12.9382 15.824 12.9382 16.1742 13.1855 16.2765L13.5049 16.4086C13.9962 16.6119 14.3865 17.0022 14.5896 17.4935L14.7224 17.8145C14.8247 18.0618 15.1751 18.0619 15.2774 17.8145L15.4093 17.4961C15.6125 17.0052 16.0025 16.6152 16.4934 16.412L16.8158 16.2785C17.063 16.1762 17.0631 15.8261 16.8159 15.7237L16.4957 15.591C16.0053 15.3878 15.6156 14.9981 15.4124 14.5078L15.2788 14.1854Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
10
src/assets/continue_writing.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 4.83333C12.6999 6.191 13.7411 7.22893 15.0333 7.36667M13.1329 4.19355L9.02678 8.38156C8.87174 8.5406 8.7217 8.85386 8.69169 9.07073L8.50664 10.6322C8.44162 11.1961 8.86173 11.5816 9.44189 11.4852L11.0523 11.2202C11.2774 11.1816 11.5925 11.0226 11.7475 10.8587L15.8536 6.67069C16.5638 5.94779 16.8839 5.12368 15.7786 4.11644C14.6783 3.11883 13.8431 3.47064 13.1329 4.19355Z"
|
||||
stroke="currentColor" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 5H9" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M3 8.5H6" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M3 12H6" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M3 15.5H11" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M14.1549 11.2316C14.283 10.9228 14.7206 10.9228 14.8486 11.2317L15.0156 11.6347C15.2695 12.2477 15.7566 12.7347 16.3696 12.9887L16.7699 13.1546C17.0789 13.2826 17.0788 13.7202 16.7698 13.8482L16.3667 14.015C15.7531 14.2691 15.2656 14.7565 15.0116 15.3701L14.8468 15.7682C14.7188 16.0773 14.2808 16.0773 14.153 15.7681L13.9871 15.3669C13.7331 14.7528 13.2453 14.2649 12.6312 14.0108L12.2319 13.8456C11.9228 13.7177 11.9227 13.28 12.2317 13.152L12.6349 12.985C13.2478 12.7311 13.7349 12.2444 13.989 11.6316L14.1549 11.2316Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,22 +1,15 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_8)">
|
||||
<path d="M12.7861 10.0512V7.95725C12.7861 6.47044 11.5807 5.26513 10.0939 5.26513H5.90618C4.4193 5.26513 3.21399 6.47044 3.21399 7.95725V10.0512C3.21399 12.6944 5.3568 14.8372 8.00005 14.8372C10.6433 14.8372 12.7861 12.6944 12.7861 10.0512Z"
|
||||
stroke="currentColor"/>
|
||||
<path d="M11.0767 5.607V4.92325C11.0767 3.224 9.69922 1.8465 7.99997 1.8465C6.30072 1.8465 4.92322 3.224 4.92322 4.92325V5.607"
|
||||
stroke="currentColor"/>
|
||||
<path d="M12.786 9.36744H14.8371" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M3.21397 9.36744H1.16284" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M9.70935 2.18837L11.4187 1.16281" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M6.29073 2.18837L4.58142 1.16281" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M13.8117 13.4699L12.4442 12.9229" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M13.8117 5.265L12.4442 5.81194" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M2.18835 13.4699L3.55579 12.9229" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M2.18835 5.265L3.55579 5.81194" stroke="currentColor" stroke-linecap="round"/>
|
||||
<path d="M8 14.4954V10.0512" stroke="currentColor" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_8">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M15.6004 12.3998V9.9498C15.6004 8.21011 14.1901 6.7998 12.4504 6.7998H7.55039C5.81069 6.7998 4.40039 8.21011 4.40039 9.9498V12.3998C4.40039 15.4926 6.9076 17.9998 10.0004 17.9998C13.0932 17.9998 15.6004 15.4926 15.6004 12.3998Z"
|
||||
stroke="currentColor" stroke-width="1.25"/>
|
||||
<path d="M13.6004 7.1998V6.3998C13.6004 4.41158 11.9886 2.7998 10.0004 2.7998C8.01217 2.7998 6.40039 4.41158 6.40039 6.3998V7.1998"
|
||||
stroke="currentColor" stroke-width="1.25"/>
|
||||
<path d="M15.5996 11.5996H17.9996" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M4.4 11.5996H2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M12 3.2L14 2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M8 3.2L6 2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M16.8002 16.3998L15.2002 15.7598" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M16.8002 6.7998L15.2002 7.4398" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M3.2002 16.3998L4.8002 15.7598" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M3.2002 6.7998L4.8002 7.4398" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M10 18V13" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
8
src/assets/help_documentation.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 8.8V10C18 13.7712 18 15.6569 16.8284 16.8284C15.6569 18 13.7712 18 10 18C6.22876 18 4.34314 18 3.17158 16.8284C2 15.6569 2 13.7712 2 10C2 6.22876 2 4.34314 3.17158 3.17158C4.34314 2 6.22876 2 10 2H11.2"
|
||||
stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M13 4.5C13 5.16304 13.2634 5.79893 13.7322 6.26777C14.2011 6.73661 14.837 7 15.5 7C16.163 7 16.7989 6.73661 17.2678 6.26777C17.7366 5.79893 18 5.16304 18 4.5C18 3.83696 17.7366 3.20107 17.2678 2.73223C16.7989 2.26339 16.163 2 15.5 2C14.837 2 14.2011 2.26339 13.7322 2.73223C13.2634 3.20107 13 3.83696 13 4.5Z"
|
||||
stroke="currentColor" stroke-width="1.25"/>
|
||||
<path d="M6 11H13" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M6 14H10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 957 B |
@@ -1,6 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 10L13 12C13 12.2652 12.8829 12.5196 12.6746 12.7071C12.4662 12.8946 12.1836 13 11.8889 13L4.11111 13C3.81643 13 3.53381 12.8946 3.32544 12.7071C3.11706 12.5196 3 12.2652 3 12L3 10"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 6L8 9L11 6" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 3L8 9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.2701 6.81958C13.9185 7.03135 15 8.29514 15 11.0618V11.1506C15 14.2042 13.6831 15.427 10.3946 15.427H5.60536C2.31687 15.427 1 14.2042 1 11.1506V11.0618C1 8.31564 2.06674 7.05185 4.67105 6.82641"
|
||||
stroke="currentColor" stroke-width="1.0247" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 2.21933V10.806" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.2323 9.31958L7.99896 11.5529L5.76562 9.31958" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 665 B |
6
src/assets/improve-writing.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.55364 2.82613L5.01065 2.63676L4.55364 2.82613C4.85837 3.56156 5.44271 4.14587 6.17817 4.45062L6.30047 4.5013L6.17547 4.55305C5.43925 4.85782 4.85435 5.44268 4.5496 6.17887L5.01158 6.37011L4.5496 6.17887L4.49997 6.29877L4.4491 6.17577C4.1444 5.43898 3.55912 4.85364 2.82235 4.54879L2.70125 4.49869L2.82624 4.44692C3.56159 4.14236 4.14595 3.55834 4.45086 2.82318L4.50166 2.70069L4.55364 2.82613Z"
|
||||
fill="currentColor" stroke="currentColor"/>
|
||||
<path d="M11.0234 5.71096C11.2876 7.34532 12.6641 8.59477 14.3725 8.76058M11.8628 4.85217L4.64726 12.1683C4.45676 12.3637 4.2724 12.7487 4.23553 13.0151L4.00815 14.9337C3.92827 15.6265 4.44447 16.1003 5.15731 15.9818L7.13608 15.6562C7.41262 15.6088 7.79977 15.4134 7.99027 15.212L15.2058 7.89587C16.0784 7.00763 16.4717 5.99504 15.1136 4.75742C13.7617 3.53165 12.7354 3.96393 11.8628 4.85217Z"
|
||||
stroke="currentColor" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,14 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_4)">
|
||||
<path d="M5.47817 13.2508C9.79161 15.4635 14.8829 12.177 14.6424 7.33506C14.4019 2.49313 9.01011 -0.272812 4.93711 2.35644C2.21798 4.11169 1.27198 7.64219 2.74917 10.5218L1.34973 14.6503L5.47817 13.2508Z"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.31091 5.55369C6.84716 4.02931 8.83254 3.65706 9.88454 4.88369C10.2117 5.265 10.3911 5.75106 10.3904 6.25344C10.3904 7.65294 8.29116 8.35262 8.29116 8.35262"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.34705 11.1516H8.35405" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1869_10430)">
|
||||
<path d="M9.96301 15C9.73181 15 9.51008 14.9091 9.3466 14.7474C9.18312 14.5857 9.09128 14.3664 9.09128 14.1376C9.09128 13.9089 9.18312 13.6896 9.3466 13.5279C9.51008 13.3661 9.73181 13.2753 9.96301 13.2753C10.1942 13.2753 10.4159 13.3661 10.5794 13.5279C10.7429 13.6896 10.8348 13.9089 10.8348 14.1376C10.8348 14.3664 10.7429 14.5857 10.5794 14.7474C10.4159 14.9091 10.1942 15 9.96301 15ZM7.60638 8.20734C7.52021 8.20673 7.43515 8.18809 7.35679 8.15265C7.27842 8.1172 7.20852 8.06575 7.15169 8.00168C7.09485 7.93761 7.05237 7.86236 7.02702 7.78089C7.00167 7.69942 6.99404 7.61357 7.00462 7.52897C7.04422 7.19364 7.10722 6.91944 7.19361 6.70637C7.34006 6.35323 7.56791 6.03882 7.85896 5.78822C8.15447 5.52401 8.50073 5.32136 8.87709 5.19234C9.25858 5.06297 9.65933 4.99797 10.0626 5.00005C10.9025 5.00005 11.6015 5.24457 12.1607 5.73421C12.7198 6.22207 13 6.87374 13 7.68684C13 8.05244 12.928 8.38362 12.784 8.68037C12.6412 8.97771 12.3364 9.34806 11.8703 9.79081C11.4029 10.2342 11.0945 10.5493 10.9415 10.7369C10.783 10.9361 10.6656 11.1641 10.596 11.4081C10.554 11.5452 10.524 11.7072 10.5078 11.8936C10.479 12.2082 10.2234 12.4574 9.90422 12.4574C9.81932 12.4569 9.73545 12.4389 9.65799 12.4045C9.58052 12.3702 9.51114 12.3202 9.45429 12.2578C9.39744 12.1954 9.35436 12.122 9.32781 12.0422C9.30125 11.9625 9.2918 11.8781 9.30006 11.7945C9.32106 11.5784 9.35286 11.3903 9.39725 11.2301C9.47945 10.9244 9.60424 10.6573 9.77103 10.4288C9.93722 10.1997 10.2372 9.86382 10.671 9.42047C11.1053 8.97771 11.3849 8.65485 11.5109 8.45484C11.6345 8.25364 11.7647 7.92662 11.7647 7.52897C11.7647 7.13132 11.5529 6.80786 11.2679 6.50161C10.9811 6.19537 10.5654 6.04165 10.02 6.04165C8.95569 6.04165 8.35213 6.58352 8.21054 7.66726C8.17034 7.97232 7.91956 8.20734 7.60878 8.20734H7.60638Z"
|
||||
fill="currentColor"/>
|
||||
<circle cx="10" cy="10" r="8.5" stroke="currentColor" stroke-width="1.25"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_4">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
<clipPath id="clip0_1869_10430">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 2.2 KiB |
@@ -1,9 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="vuesax/linear/moon">
|
||||
<g id="moon">
|
||||
<path id="Vector"
|
||||
d="M1.35404 8.27997C1.59404 11.7133 4.50738 14.5066 7.99404 14.66C10.454 14.7666 12.654 13.62 13.974 11.8133C14.5207 11.0733 14.2274 10.58 13.314 10.7466C12.8674 10.8266 12.4074 10.86 11.9274 10.84C8.66738 10.7066 6.00071 7.97997 5.98738 4.75997C5.98071 3.89331 6.16071 3.07331 6.48738 2.32664C6.84738 1.49997 6.41404 1.10664 5.58071 1.45997C2.94071 2.57331 1.13404 5.23331 1.35404 8.27997Z"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M17.3747 10.0694C17.3374 14.1105 14.0499 17.375 10 17.375C5.9269 17.375 2.625 14.0731 2.625 10C2.625 5.95005 5.88947 2.66256 9.93062 2.62532C9.9383 2.66773 9.93722 2.72234 9.92269 2.77631C9.90799 2.83093 9.88441 2.86765 9.86373 2.88847C8.94421 3.81425 8.375 5.09126 8.375 6.5C8.375 9.33046 10.6695 11.625 13.5 11.625C14.9087 11.625 16.1858 11.0558 17.1115 10.1363C17.1324 10.1156 17.1691 10.092 17.2237 10.0773C17.2777 10.0628 17.3323 10.0617 17.3747 10.0694Z"
|
||||
stroke="currentColor" stroke-width="1.25" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 654 B |
@@ -1,18 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="message-programming">
|
||||
<g id="vuesax/linear/message-programming">
|
||||
<g id="message-programming_2">
|
||||
<path id="Vector"
|
||||
d="M8.5 18.9697H8C4 18.9697 2 17.9697 2 12.9697V7.96973C2 3.96973 4 1.96973 8 1.96973H16C20 1.96973 22 3.96973 22 7.96973V12.9697C22 16.9697 20 18.9697 16 18.9697H15.5C15.19 18.9697 14.89 19.1198 14.7 19.3698L13.2 21.3698C12.54 22.2498 11.46 22.2498 10.8 21.3698L9.29999 19.3698C9.13999 19.1498 8.78 18.9697 8.5 18.9697Z"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group 1686556182">
|
||||
<path id="Vector_2" d="M12 7V12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M11.9946 15H12.0036" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M4.66667 2C3.19391 2 2 3.16164 2 4.59459V12.3784C2 13.8113 3.19391 14.973 4.66667 14.973H6.33333C6.75301 14.973 7.14819 15.1652 7.4 15.4919L8.93333 17.4811C9.46667 18.173 10.5333 18.173 11.0667 17.4811L12.6 15.4919C12.8518 15.1652 13.247 14.973 13.6667 14.973H15.3333C16.8061 14.973 18 13.8113 18 12.3784V4.59459C18 3.16164 16.8061 2 15.3333 2H4.66667Z"
|
||||
stroke="currentColor" stroke-width="1.25"/>
|
||||
<rect x="9.25" y="11" width="1.5" height="1.5" rx="0.75" fill="currentColor"/>
|
||||
<path d="M9.25 5L9.37455 10H10.6255L10.75 5H9.25Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 683 B |
@@ -1,7 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M5.40039 5.67018C5.63289 2.97018 7.02039 1.86768 10.0579 1.86768H10.1554C13.5079 1.86768 14.8504 3.21018 14.8504 6.56268V11.4527C14.8504 14.8052 13.5079 16.1477 10.1554 16.1477H10.0579C7.04289 16.1477 5.65539 15.0602 5.40789 12.4052"
|
||||
stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 9H11.7" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.48633 6.48779L11.9988 9.00029L9.48633 11.5128" stroke="currentColor" stroke-width="1.125"
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.1083 10.7334L13.9999 8.84177L12.1083 6.9501" stroke="currentColor" stroke-miterlimit="10"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<path d="M6.43359 8.84177H13.9485" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
<path d="M7.91144 14.7089C4.64537 14.7089 2 12.4921 2 8.79743C2 5.10278 4.64537 2.88599 7.91144 2.88599"
|
||||
stroke="currentColor" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 732 B After Width: | Height: | Size: 625 B |
@@ -1,11 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_2)">
|
||||
<path d="M6.0536 3.49319C6.9196 1.93962 7.3526 1.16281 8.00004 1.16281C8.64748 1.16281 9.08048 1.93956 9.94648 3.49319L10.1705 3.89512C10.4167 4.33656 10.5397 4.55731 10.7315 4.703C10.9234 4.84862 11.1623 4.90269 11.6402 5.01081L12.0754 5.10925C13.757 5.48975 14.5979 5.68 14.798 6.32331C14.998 6.96662 14.4248 7.637 13.2783 8.97762L12.9817 9.3245C12.6559 9.7055 12.493 9.89594 12.4197 10.1316C12.3464 10.3673 12.371 10.6214 12.4203 11.1298L12.4651 11.5926C12.6385 13.3813 12.7252 14.2757 12.2014 14.6732C11.6776 15.0708 10.8903 14.7083 9.31573 13.9834L8.90835 13.7957C8.46092 13.5897 8.23723 13.4867 8.00004 13.4867C7.76285 13.4867 7.5391 13.5897 7.09173 13.7957L6.68435 13.9834C5.10973 14.7083 4.32242 15.0708 3.79867 14.6732C3.27492 14.2757 3.3616 13.3813 3.53492 11.5926L3.57973 11.1298C3.62904 10.6214 3.65367 10.3673 3.58035 10.1316C3.5071 9.89594 3.34417 9.7055 3.01835 9.3245L2.72179 8.97762C1.57529 7.637 1.00204 6.96662 1.2021 6.32331C1.4021 5.68 2.24298 5.48975 3.92473 5.10925L4.35979 5.01081C4.83773 4.90269 5.07667 4.84862 5.26854 4.703C5.46042 4.55737 5.58342 4.33656 5.82954 3.89512L6.0536 3.49319Z"
|
||||
stroke="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_2">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M7.72252 4.72671C8.73583 2.90891 9.24247 2 9.99999 2C10.7575 2 11.2642 2.9089 12.2774 4.7267L12.5396 5.19699C12.8276 5.71355 12.9715 5.97184 13.1961 6.14225C13.4205 6.31267 13.7001 6.37593 14.2593 6.50245L14.7684 6.61763C16.7361 7.06286 17.72 7.28546 17.9541 8.03819C18.1881 8.79089 17.5174 9.57529 16.1759 11.1439L15.8289 11.5498C15.4477 11.9955 15.257 12.2184 15.1713 12.4942C15.0856 12.7699 15.1144 13.0673 15.172 13.6621L15.2245 14.2035C15.4273 16.2965 15.5287 17.343 14.9159 17.8082C14.303 18.2734 13.3818 17.8492 11.5394 17.001L11.0628 16.7815C10.5393 16.5404 10.2775 16.4199 9.99999 16.4199C9.72247 16.4199 9.46071 16.5404 8.93719 16.7815L8.46055 17.001C6.61814 17.8492 5.69694 18.2734 5.08412 17.8082C4.47129 17.343 4.5727 16.2965 4.77552 14.2035L4.82798 13.6621C4.88562 13.0673 4.91444 12.7699 4.82869 12.4942C4.74294 12.2184 4.55234 11.9955 4.17113 11.5498L3.82408 11.1439C2.4826 9.57529 1.81186 8.79089 2.04594 8.03819C2.28002 7.28546 3.26389 7.06286 5.23163 6.61763L5.74071 6.50245C6.29988 6.37593 6.57946 6.31267 6.80395 6.14225C7.02844 5.97184 7.17242 5.71356 7.46037 5.19699L7.72252 4.72671Z"
|
||||
stroke="currentColor" stroke-width="1.25"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,14 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="sun">
|
||||
<g id="vuesax/linear/sun">
|
||||
<g id="sun_2">
|
||||
<path id="Vector"
|
||||
d="M12 18.5C15.5899 18.5 18.5 15.5899 18.5 12C18.5 8.41015 15.5899 5.5 12 5.5C8.41015 5.5 5.5 8.41015 5.5 12C5.5 15.5899 8.41015 18.5 12 18.5Z"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2"
|
||||
d="M19.14 19.14L19.01 19.01M19.01 4.99L19.14 4.86L19.01 4.99ZM4.86 19.14L4.99 19.01L4.86 19.14ZM12 2.08V2V2.08ZM12 22V21.92V22ZM2.08 12H2H2.08ZM22 12H21.92H22ZM4.99 4.99L4.86 4.86L4.99 4.99Z"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1875 2.1875V3.7875M10.1875 16.5875V18.1875M3.7875 10.1875H2.1875M18.1879 10.1875H16.5879M16.4104 3.9655L14.6328 5.59076M3.96582 3.9655L5.74343 5.59076M5.7436 14.6325L3.96582 16.4103M16.4104 16.4093L14.6328 14.6315M6.1875 10.1875C6.1875 10.7128 6.29096 11.2329 6.49198 11.7182C6.693 12.2035 6.98764 12.6445 7.35907 13.0159C7.73051 13.3874 8.17146 13.682 8.65677 13.883C9.14207 14.084 9.66221 14.1875 10.1875 14.1875C10.7128 14.1875 11.2329 14.084 11.7182 13.883C12.2035 13.682 12.6445 13.3874 13.0159 13.0159C13.3874 12.6445 13.682 12.2035 13.883 11.7182C14.084 11.2329 14.1875 10.7128 14.1875 10.1875C14.1875 9.66221 14.084 9.14207 13.883 8.65677C13.682 8.17146 13.3874 7.73051 13.0159 7.35907C12.6445 6.98764 12.2035 6.693 11.7182 6.49198C11.2329 6.29096 10.7128 6.1875 10.1875 6.1875C9.66221 6.1875 9.14207 6.29096 8.65677 6.49198C8.17146 6.693 7.73051 6.98764 7.35907 7.35907C6.98764 7.73051 6.693 8.17146 6.49198 8.65677C6.29096 9.14207 6.1875 9.66221 6.1875 10.1875Z"
|
||||
stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 895 B After Width: | Height: | Size: 1.1 KiB |
13
src/assets/upgrade_ai_max.svg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
@@ -20,8 +20,8 @@ function BreadcrumbItem({ crumb, disableClick = false, toView, variant }: {
|
||||
const className = useMemo(() => {
|
||||
const classList = ['flex', 'items-center', 'gap-1.5', 'text-sm', 'overflow-hidden', 'max-sm:text-base'];
|
||||
|
||||
if (!disableClick && !extra?.is_space) {
|
||||
if ((is_published && variant === 'publish') || variant === 'app') {
|
||||
if(!disableClick && !extra?.is_space) {
|
||||
if((is_published && variant === 'publish') || variant === 'app') {
|
||||
classList.push('cursor-pointer hover:text-text-title hover:underline');
|
||||
} else {
|
||||
classList.push('flex-1');
|
||||
@@ -35,12 +35,12 @@ function BreadcrumbItem({ crumb, disableClick = false, toView, variant }: {
|
||||
|
||||
<div
|
||||
className={className}
|
||||
onClick={async () => {
|
||||
if (disableClick || extra?.is_space || (!is_published && variant === 'publish')) return;
|
||||
onClick={async() => {
|
||||
if(disableClick || extra?.is_space || (!is_published && variant === 'publish')) return;
|
||||
try {
|
||||
await toView?.(view_id);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
}
|
||||
}}
|
||||
@@ -53,7 +53,10 @@ function BreadcrumbItem({ crumb, disableClick = false, toView, variant }: {
|
||||
char={extra.space_icon ? undefined : name.slice(0, 1)}
|
||||
/>
|
||||
) : (
|
||||
<PageIcon view={crumb} className={'flex h-5 w-5 min-w-5 items-center justify-center'}/>
|
||||
<PageIcon
|
||||
view={crumb}
|
||||
className={'flex h-4 w-4 min-w-4 items-center justify-center'}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
title={name}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ReactComponent as DebugIcon } from '@/assets/debug.svg';
|
||||
import { ReactComponent as FeedbackIcon } from '@/assets/report.svg';
|
||||
import { ReactComponent as MoonIcon } from '@/assets/moon.svg';
|
||||
import { ReactComponent as SunIcon } from '@/assets/sun.svg';
|
||||
import { ReactComponent as DocumentationIcon } from '@/assets/help_documentation.svg';
|
||||
|
||||
const popoverProps: Partial<PopoverProps> = {
|
||||
anchorOrigin: {
|
||||
@@ -27,7 +28,7 @@ const popoverProps: Partial<PopoverProps> = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function Help () {
|
||||
export default function Help() {
|
||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
@@ -79,6 +80,16 @@ export default function Help () {
|
||||
variant={'text'}
|
||||
>{t('questionBubble.whatsNew')}
|
||||
</Button>
|
||||
<Button
|
||||
component={'a'}
|
||||
href={'https://appflowy.com/guide/getting-started-with-appflowy'}
|
||||
className={'justify-start'}
|
||||
target="_blank"
|
||||
color={'inherit'}
|
||||
startIcon={<DocumentationIcon />}
|
||||
variant={'text'}
|
||||
>{t('questionBubble.documentation')}
|
||||
</Button>
|
||||
<Button
|
||||
component={'a'}
|
||||
href={'https://discord.gg/9Q2xaN37tV'}
|
||||
@@ -110,7 +121,7 @@ export default function Help () {
|
||||
<Button
|
||||
component={'a'}
|
||||
target="_blank"
|
||||
href={'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose'}
|
||||
href={'https://github.com/AppFlowy-IO/AppFlowy-Web/issues/new/choose'}
|
||||
className={'justify-start'}
|
||||
color={'inherit'}
|
||||
startIcon={<FeedbackIcon />}
|
||||
|
||||
@@ -36,7 +36,7 @@ const InfoSnackbar = forwardRef<HTMLDivElement, InfoSnackbarProps>(
|
||||
className={'flex items-center justify-center'}
|
||||
>
|
||||
<Paper className={`relative flex flex-col gap-4 border p-5 ${getBorderColor(type)}`}>
|
||||
<div className={'flex w-full items-center justify-between text-base font-medium'}>
|
||||
<div className={'flex w-full items-center justify-between text-title font-medium'}>
|
||||
<div className={'flex flex-1 items-center gap-2 text-left font-semibold'}>
|
||||
{getIcon(type)}
|
||||
<div>{title}</div>
|
||||
@@ -77,8 +77,8 @@ const InfoSnackbar = forwardRef<HTMLDivElement, InfoSnackbarProps>(
|
||||
|
||||
export default InfoSnackbar;
|
||||
|
||||
function getIcon (type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch (type) {
|
||||
function getIcon(type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch(type) {
|
||||
case 'success':
|
||||
return <CheckCircle className={'h-6 w-6 text-[var(--function-success)]'} />;
|
||||
case 'info':
|
||||
@@ -90,8 +90,8 @@ function getIcon (type: 'success' | 'info' | 'warning' | 'error') {
|
||||
}
|
||||
}
|
||||
|
||||
function getButtonBgColor (type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch (type) {
|
||||
function getButtonBgColor(type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch(type) {
|
||||
case 'success':
|
||||
return 'bg-[var(--function-success)]';
|
||||
case 'info':
|
||||
@@ -103,8 +103,8 @@ function getButtonBgColor (type: 'success' | 'info' | 'warning' | 'error') {
|
||||
}
|
||||
}
|
||||
|
||||
function getButtonHoverBgColor (type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch (type) {
|
||||
function getButtonHoverBgColor(type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch(type) {
|
||||
case 'success':
|
||||
return 'hover:bg-[var(--function-success-hover)]';
|
||||
case 'info':
|
||||
@@ -116,8 +116,8 @@ function getButtonHoverBgColor (type: 'success' | 'info' | 'warning' | 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function getBorderColor (type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch (type) {
|
||||
function getBorderColor(type: 'success' | 'info' | 'warning' | 'error') {
|
||||
switch(type) {
|
||||
case 'success':
|
||||
return 'border-[var(--function-success)]';
|
||||
case 'info':
|
||||
|
||||
@@ -2,9 +2,10 @@ import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { UIVariant, ViewMetaProps, YDoc } from '@/application/types';
|
||||
import { useAIChatContext } from '@/components/ai-chat/AIChatProvider';
|
||||
import { insertDataToDoc } from '@/components/ai-chat/utils';
|
||||
import { useAppHandlers, useAppView } from '@/components/app/app.hooks';
|
||||
import { useAppHandlers, useAppView, useCurrentWorkspaceId } from '@/components/app/app.hooks';
|
||||
import { Document } from '@/components/document';
|
||||
import RecordNotFound from '@/components/error/RecordNotFound';
|
||||
import { useService } from '@/components/main/app.hooks';
|
||||
import { debounce } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import smoothScrollIntoViewIfNeeded from 'smooth-scroll-into-view-if-needed';
|
||||
@@ -37,7 +38,8 @@ function DrawerContent({
|
||||
id: string;
|
||||
doc: YDoc;
|
||||
} | undefined>(undefined);
|
||||
|
||||
const service = useService();
|
||||
const requestInstance = service?.getAxiosInstance();
|
||||
const initialScrolling = React.useRef(false);
|
||||
const [notFound, setNotFound] = React.useState(false);
|
||||
const [editor, setEditor] = useState<YjsEditor | undefined>(undefined);
|
||||
@@ -141,6 +143,7 @@ function DrawerContent({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const viewMeta: ViewMetaProps | null = useMemo(() => {
|
||||
return view ? {
|
||||
name: view.name,
|
||||
@@ -150,8 +153,9 @@ function DrawerContent({
|
||||
visibleViewIds: [],
|
||||
viewId: view.view_id,
|
||||
extra: view.extra,
|
||||
workspaceId,
|
||||
} : null;
|
||||
}, [view]);
|
||||
}, [view, workspaceId]);
|
||||
|
||||
const handleUploadFile = useCallback((file: File) => {
|
||||
if(view && uploadFile) {
|
||||
@@ -172,6 +176,8 @@ function DrawerContent({
|
||||
{
|
||||
doc && viewMeta && (
|
||||
<Document
|
||||
requestInstance={requestInstance}
|
||||
workspaceId={workspaceId || ''}
|
||||
doc={doc.doc}
|
||||
readOnly={false}
|
||||
viewMeta={viewMeta}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
createBlock, executeOperations,
|
||||
createBlock, executeOperations, getBlockIndex,
|
||||
getBlocks,
|
||||
getChildrenArray,
|
||||
getChildrenMap,
|
||||
getDocument, getPageId, getText, getTextMap, updateBlockParent,
|
||||
getDocument, getPageId, getParent, getText, getTextMap, updateBlockParent,
|
||||
} from '@/application/slate-yjs/utils/yjs';
|
||||
import {
|
||||
BlockType,
|
||||
@@ -60,3 +60,57 @@ export function insertDataToDoc(doc: YDoc, insertData: EditorData) {
|
||||
|
||||
}
|
||||
|
||||
export function insertDataAfterBlock(sharedRoot: YSharedRoot, insertData: EditorData, blockId: string) {
|
||||
let latestBlockId = '';
|
||||
|
||||
executeOperations(sharedRoot, [() => {
|
||||
const document = getDocument(sharedRoot);
|
||||
|
||||
if(!document) return;
|
||||
|
||||
const blocks = getBlocks(sharedRoot);
|
||||
const textMap = getTextMap(sharedRoot);
|
||||
const childrenMap = getChildrenMap(sharedRoot);
|
||||
|
||||
if(!blocks || !textMap || !childrenMap) return;
|
||||
const traverse = (node: EditorNode, parentId: string, index: number) => {
|
||||
const block = createBlock(sharedRoot, {
|
||||
ty: node.type as unknown as BlockType,
|
||||
data: node.data as object,
|
||||
});
|
||||
|
||||
const parent = blocks.get(parentId);
|
||||
|
||||
const blockText = getText(block.get(YjsEditorKey.block_external_id), sharedRoot);
|
||||
|
||||
if(blockText && node.delta && node.delta.length > 0) {
|
||||
blockText.applyDelta(node.delta);
|
||||
}
|
||||
|
||||
updateBlockParent(sharedRoot, block, parent, index);
|
||||
|
||||
latestBlockId = block.get(YjsEditorKey.block_id);
|
||||
if(node.children) {
|
||||
node.children.forEach((child, index) => {
|
||||
traverse(child, block.get(YjsEditorKey.block_id), index);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const parent = getParent(blockId, sharedRoot);
|
||||
const index = getBlockIndex(blockId, sharedRoot);
|
||||
const parentId = parent?.get(YjsEditorKey.block_id);
|
||||
|
||||
if(parentId) {
|
||||
insertData.forEach((node, i) => {
|
||||
traverse(node, parentId, index + i + 1);
|
||||
});
|
||||
}
|
||||
|
||||
}], 'AIInsertAfterContent');
|
||||
|
||||
return latestBlockId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
function Main () {
|
||||
function Main() {
|
||||
return (
|
||||
<main className={'flex-1'}>
|
||||
<Outlet />
|
||||
|
||||
@@ -57,7 +57,7 @@ function MainLayout() {
|
||||
width,
|
||||
transition: 'width 0.2s ease-in-out, transform 0.2s ease-in-out',
|
||||
}}
|
||||
className={'appflowy-layout flex flex-col appflowy-scroll-container h-full'}
|
||||
className={'appflowy-layout transform flex flex-col appflowy-scroll-container h-full'}
|
||||
>
|
||||
<AppHeader
|
||||
onOpenDrawer={() => {
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
} from '@/application/types';
|
||||
import SpaceIcon from '@/components/_shared/view-icon/SpaceIcon';
|
||||
import { findAncestors, findView } from '@/components/_shared/outline/utils';
|
||||
import { useAppHandlers, useAppOutline } from '@/components/app/app.hooks';
|
||||
import { useAppHandlers, useAppOutline, useCurrentWorkspaceId } from '@/components/app/app.hooks';
|
||||
import DatabaseView from '@/components/app/DatabaseView';
|
||||
import MoreActions from '@/components/app/header/MoreActions';
|
||||
import MovePagePopover from '@/components/app/view-actions/MovePagePopover';
|
||||
import { Document } from '@/components/document';
|
||||
import RecordNotFound from '@/components/error/RecordNotFound';
|
||||
import { useService } from '@/components/main/app.hooks';
|
||||
import { Button, Dialog, Divider, IconButton, Tooltip, Zoom } from '@mui/material';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -43,6 +44,8 @@ function ViewModal({
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
toView,
|
||||
@@ -63,6 +66,8 @@ function ViewModal({
|
||||
doc: YDoc;
|
||||
} | undefined>(undefined);
|
||||
const [notFound, setNotFound] = React.useState(false);
|
||||
const service = useService();
|
||||
const requestInstance = service?.getAxiosInstance();
|
||||
const loadPageDoc = useCallback(async(id: string) => {
|
||||
|
||||
setNotFound(false);
|
||||
@@ -104,8 +109,9 @@ function ViewModal({
|
||||
visibleViewIds: [],
|
||||
viewId: view.view_id,
|
||||
extra: view.extra,
|
||||
workspaceId,
|
||||
} : null;
|
||||
}, [view]);
|
||||
}, [view, workspaceId]);
|
||||
const handleUploadFile = useCallback((file: File) => {
|
||||
if(view && uploadFile) {
|
||||
return uploadFile(view.view_id, file);
|
||||
@@ -200,6 +206,8 @@ function ViewModal({
|
||||
const viewDom = useMemo(() => {
|
||||
if(!doc || !viewMeta || doc.id !== viewMeta.viewId) return null;
|
||||
return <View
|
||||
requestInstance={requestInstance}
|
||||
workspaceId={workspaceId || ''}
|
||||
doc={doc.doc}
|
||||
readOnly={false}
|
||||
viewMeta={viewMeta}
|
||||
@@ -216,7 +224,7 @@ function ViewModal({
|
||||
uploadFile={handleUploadFile}
|
||||
variant={UIVariant.App}
|
||||
/>;
|
||||
}, [openPageModal, handleUploadFile, setWordCount, loadViews, doc, viewMeta, View, toView, loadViewMeta, createRowDoc, loadView, updatePage, addPage, deletePage]);
|
||||
}, [requestInstance, openPageModal, workspaceId, handleUploadFile, setWordCount, loadViews, doc, viewMeta, View, toView, loadViewMeta, createRowDoc, loadView, updatePage, addPage, deletePage]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -229,7 +237,7 @@ function ViewModal({
|
||||
disableRestoreFocus={true}
|
||||
TransitionComponent={Transition}
|
||||
PaperProps={{
|
||||
className: `max-w-[70vw] w-[1188px] flex flex-col h-[80vh] appflowy-scroller`,
|
||||
className: `max-w-[70vw] appflowy-scroll-container transform relative w-[1188px] flex flex-col h-[80vh] appflowy-scroller`,
|
||||
}}
|
||||
>
|
||||
{modalTitle}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Origins } from '@/components/_shared/popover';
|
||||
import DeletePageConfirm from '@/components/app/view-actions/DeletePageConfirm';
|
||||
import MovePagePopover from '@/components/app/view-actions/MovePagePopover';
|
||||
import { Button } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DeleteIcon } from '@/assets/trash.svg';
|
||||
import { ViewLayout } from '@/application/types';
|
||||
import { ReactComponent as DuplicateIcon } from '@/assets/duplicate.svg';
|
||||
import { ReactComponent as MoveToIcon } from '@/assets/move_to.svg';
|
||||
import { ReactComponent as DeleteIcon } from '@/assets/trash.svg';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { Origins } from '@/components/_shared/popover';
|
||||
import { useAppHandlers, useAppView, useCurrentWorkspaceId } from '@/components/app/app.hooks';
|
||||
import DeletePageConfirm from '@/components/app/view-actions/DeletePageConfirm';
|
||||
import MovePagePopover from '@/components/app/view-actions/MovePagePopover';
|
||||
import { useService } from '@/components/main/app.hooks';
|
||||
import { Button, CircularProgress } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function MoreActionsContent({ itemClicked, viewId, movePopoverOrigins, onDeleted }: {
|
||||
itemClicked?: () => void;
|
||||
@@ -17,18 +21,40 @@ function MoreActionsContent({ itemClicked, viewId, movePopoverOrigins, onDeleted
|
||||
const { t } = useTranslation();
|
||||
const [movePopoverAnchorEl, setMovePopoverAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const service = useService();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const view = useAppView(viewId);
|
||||
const layout = view?.layout;
|
||||
const [duplicateLoading, setDuplicateLoading] = useState(false);
|
||||
const {
|
||||
refreshOutline,
|
||||
} = useAppHandlers();
|
||||
const handleDuplicateClick = async() => {
|
||||
if(!workspaceId || !service) return;
|
||||
setDuplicateLoading(true);
|
||||
try {
|
||||
await service.duplicateAppPage(workspaceId, viewId);
|
||||
|
||||
void refreshOutline?.();
|
||||
// eslint-disable-next-line
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
} finally {
|
||||
setDuplicateLoading(false);
|
||||
}
|
||||
|
||||
itemClicked?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col gap-2 more-actions'}>
|
||||
<Button
|
||||
size={'small'}
|
||||
className={'px-3 hidden py-1 justify-start '}
|
||||
className={`px-3 py-1 ${layout === ViewLayout.AIChat ? 'hidden' : ''} justify-start `}
|
||||
color={'inherit'}
|
||||
onClick={() => {
|
||||
itemClicked?.();
|
||||
}}
|
||||
|
||||
startIcon={<DuplicateIcon />}
|
||||
onClick={handleDuplicateClick}
|
||||
disabled={duplicateLoading}
|
||||
startIcon={duplicateLoading ? <CircularProgress size={14} /> : <DuplicateIcon />}
|
||||
>{t('button.duplicate')}</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { View } from '@/application/types';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { useAppHandlers, useCurrentWorkspaceId } from '@/components/app/app.hooks';
|
||||
import CreateSpaceModal from '@/components/app/view-actions/CreateSpaceModal';
|
||||
import DeleteSpaceConfirm from '@/components/app/view-actions/DeleteSpaceConfirm';
|
||||
import ManageSpace from '@/components/app/view-actions/ManageSpace';
|
||||
import { Button, Divider } from '@mui/material';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useService } from '@/components/main/app.hooks';
|
||||
import { Button, CircularProgress, Divider } from '@mui/material';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DeleteIcon } from '@/assets/trash.svg';
|
||||
import { ReactComponent as DuplicateIcon } from '@/assets/duplicate.svg';
|
||||
@@ -21,23 +24,47 @@ function MoreSpaceActions({
|
||||
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
|
||||
const [manageModalOpen, setManageModalOpen] = React.useState(false);
|
||||
const [createSpaceModalOpen, setCreateSpaceModalOpen] = React.useState(false);
|
||||
const service = useService();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const [duplicateLoading, setDuplicateLoading] = useState(false);
|
||||
const {
|
||||
refreshOutline,
|
||||
} = useAppHandlers();
|
||||
|
||||
const handleDuplicateClick = useCallback(async() => {
|
||||
if(!workspaceId || !service) return;
|
||||
setDuplicateLoading(true);
|
||||
try {
|
||||
await service.duplicateAppPage(workspaceId, view.view_id);
|
||||
|
||||
void refreshOutline?.();
|
||||
onClose();
|
||||
// eslint-disable-next-line
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
} finally {
|
||||
setDuplicateLoading(false);
|
||||
}
|
||||
}, [onClose, refreshOutline, service, view.view_id, workspaceId]);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
return [{
|
||||
label: t('space.manage'),
|
||||
icon: <SettingsIcon/>,
|
||||
icon: <SettingsIcon />,
|
||||
onClick: () => {
|
||||
setManageModalOpen(true);
|
||||
},
|
||||
}, {
|
||||
label: t('space.duplicate'),
|
||||
icon: <DuplicateIcon/>,
|
||||
hidden: true,
|
||||
onClick: () => {
|
||||
//
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('space.duplicate'),
|
||||
icon: duplicateLoading ? <CircularProgress size={14} /> : <DuplicateIcon />,
|
||||
disabled: duplicateLoading,
|
||||
onClick: () => {
|
||||
void handleDuplicateClick();
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [t]);
|
||||
}, [duplicateLoading, handleDuplicateClick, t]);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col gap-2 w-full p-1.5 min-w-[230px]'}>
|
||||
@@ -46,14 +73,14 @@ function MoreSpaceActions({
|
||||
key={action.label}
|
||||
size={'small'}
|
||||
onClick={action.onClick}
|
||||
className={`px-3 py-1 ${action.hidden ? 'hidden' : ''} justify-start `}
|
||||
className={`px-3 py-1 justify-start `}
|
||||
color={'inherit'}
|
||||
startIcon={action.icon}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
<Divider className={'w-full'}/>
|
||||
<Divider className={'w-full'} />
|
||||
<Button
|
||||
size={'small'}
|
||||
className={'px-3 py-1 justify-start'}
|
||||
@@ -61,11 +88,11 @@ function MoreSpaceActions({
|
||||
onClick={() => {
|
||||
setCreateSpaceModalOpen(true);
|
||||
}}
|
||||
startIcon={<AddIcon/>}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
{t('space.createNewSpace')}
|
||||
</Button>
|
||||
<Divider className={'w-full'}/>
|
||||
<Divider className={'w-full'} />
|
||||
<Button
|
||||
size={'small'}
|
||||
className={'px-3 py-1 hover:text-function-error justify-start'}
|
||||
@@ -73,7 +100,7 @@ function MoreSpaceActions({
|
||||
onClick={() => {
|
||||
setDeleteModalOpen(true);
|
||||
}}
|
||||
startIcon={<DeleteIcon/>}
|
||||
startIcon={<DeleteIcon />}
|
||||
>
|
||||
{t('button.delete')}
|
||||
</Button>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ReactComponent as SelectedIcon } from '@/assets/selected.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SpacePermission } from '@/application/types';
|
||||
|
||||
function SpacePermissionButton ({
|
||||
function SpacePermissionButton({
|
||||
onSelected,
|
||||
value,
|
||||
}: {
|
||||
@@ -30,7 +30,7 @@ function SpacePermissionButton ({
|
||||
onClick={e => setAnchorEl(e.currentTarget)}
|
||||
>
|
||||
<div className={'flex w-full items-start flex-col'}>
|
||||
<div className={'text-base font-normal'}>{
|
||||
<div className={'text-text-title font-normal'}>{
|
||||
SpacePermission.Private === value ? t('space.privatePermission') : t('space.publicPermission')
|
||||
}</div>
|
||||
<div className={'text-text-caption'}>{
|
||||
|
||||
@@ -17,16 +17,19 @@ import { ReactComponent as TipIcon } from '@/assets/warning.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as SignOutIcon } from '@/assets/sign_out.svg';
|
||||
import { ReactComponent as UpgradeIcon } from '@/assets/icon_upgrade.svg';
|
||||
import { ReactComponent as UpgradeAIMaxIcon } from '@/assets/upgrade_ai_max.svg';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import InviteMember from '@/components/app/workspaces/InviteMember';
|
||||
import UpgradePlan from '@/components/billing/UpgradePlan';
|
||||
import UpgradeAIMax from '@/components/billing/UpgradeAIMax';
|
||||
|
||||
export function Workspaces () {
|
||||
export function Workspaces() {
|
||||
const { t } = useTranslation();
|
||||
const userWorkspaceInfo = useUserWorkspaceInfo();
|
||||
const currentWorkspaceId = useCurrentWorkspaceId();
|
||||
const currentUser = useCurrentUser();
|
||||
const [openUpgradePlan, setOpenUpgradePlan] = React.useState(false);
|
||||
const [openUpgradeAIMax, setOpenUpgradeAIMax] = React.useState(false);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [hoveredHeader, setHoveredHeader] = React.useState<boolean>(false);
|
||||
const ref = React.useRef<HTMLButtonElement | null>(null);
|
||||
@@ -48,12 +51,12 @@ export function Workspaces () {
|
||||
setCurrentWorkspace(userWorkspaceInfo?.workspaces.find((workspace) => workspace.id === currentWorkspaceId));
|
||||
}, [currentWorkspaceId, userWorkspaceInfo]);
|
||||
|
||||
const handleChange = useCallback(async (selectedId: string) => {
|
||||
const handleChange = useCallback(async(selectedId: string) => {
|
||||
setChangeLoading(selectedId);
|
||||
try {
|
||||
await handleSelectedWorkspace?.(selectedId);
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
notify.error('Failed to change workspace');
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ export function Workspaces () {
|
||||
changeLoading={changeLoading || undefined}
|
||||
onUpdateCurrentWorkspace={(name) => {
|
||||
setCurrentWorkspace(prev => {
|
||||
if (!prev) return prev;
|
||||
if(!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
name,
|
||||
@@ -151,6 +154,7 @@ export function Workspaces () {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Divider className={'w-full'} />
|
||||
<Button
|
||||
size={'small'}
|
||||
className={'justify-start px-2'}
|
||||
@@ -174,20 +178,46 @@ export function Workspaces () {
|
||||
>
|
||||
{t('subscribe.changePlan')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
startIcon={<UpgradeAIMaxIcon />}
|
||||
color={'inherit'}
|
||||
onClick={() => {
|
||||
setOpenUpgradeAIMax(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={'justify-start px-2'}
|
||||
>
|
||||
{t('subscribe.getAIMax')}
|
||||
</Button>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
</Popover>
|
||||
{isOwner &&
|
||||
<UpgradePlan
|
||||
onOpen={
|
||||
() => {
|
||||
setOpenUpgradePlan(true);
|
||||
<>
|
||||
<UpgradePlan
|
||||
onOpen={
|
||||
() => {
|
||||
setOpenUpgradePlan(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
open={openUpgradePlan}
|
||||
onClose={() => setOpenUpgradePlan(false)}
|
||||
/>}
|
||||
open={openUpgradePlan}
|
||||
onClose={() => setOpenUpgradePlan(false)}
|
||||
/>
|
||||
<UpgradeAIMax
|
||||
onOpen={
|
||||
() => {
|
||||
setOpenUpgradeAIMax(true);
|
||||
}
|
||||
}
|
||||
open={openUpgradeAIMax}
|
||||
onClose={() => setOpenUpgradeAIMax(false)}
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
<Import />
|
||||
</>;
|
||||
}
|
||||
|
||||
211
src/components/billing/UpgradeAIMax.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { SubscriptionInterval, SubscriptionPlan } from '@/application/types';
|
||||
import { NormalModal } from '@/components/_shared/modal';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { useAppHandlers, useCurrentWorkspaceId } from '@/components/app/app.hooks';
|
||||
import { useService } from '@/components/main/app.hooks';
|
||||
import { Button, CircularProgress } from '@mui/material';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
function UpgradeAIMax({ open, onClose, onOpen }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const service = useService();
|
||||
const currentWorkspaceId = useCurrentWorkspaceId();
|
||||
const [cancelLoading, setCancelLoading] = React.useState(false);
|
||||
const [cancelOpen, setCancelOpen] = React.useState(false);
|
||||
const { getSubscriptions } = useAppHandlers();
|
||||
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const action = search.get('action');
|
||||
|
||||
useEffect(() => {
|
||||
if(!open && action === 'upgrade_ai_max') {
|
||||
onOpen();
|
||||
}
|
||||
|
||||
if(open) {
|
||||
setSearch(prev => {
|
||||
prev.set('action', 'upgrade_ai_max');
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [action, open, setSearch]);
|
||||
|
||||
const loadSubscription = useCallback(async() => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if(!subscriptions || subscriptions.length === 0) {
|
||||
setIsActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions.find(item => item.plan === SubscriptionPlan.AIMax);
|
||||
|
||||
setIsActive(!!subscription);
|
||||
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose();
|
||||
setSearch(prev => {
|
||||
prev.delete('action');
|
||||
return prev;
|
||||
});
|
||||
}, [onClose, setSearch]);
|
||||
|
||||
const handleUpgrade = useCallback(async() => {
|
||||
if(!service || !currentWorkspaceId) return;
|
||||
const plan = SubscriptionPlan.AIMax;
|
||||
|
||||
try {
|
||||
const link = await service.getSubscriptionLink(currentWorkspaceId, plan, SubscriptionInterval.Year);
|
||||
|
||||
window.open(link, '_current');
|
||||
// eslint-disable-next-line
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
}
|
||||
}, [currentWorkspaceId, service]);
|
||||
|
||||
const handleCancel = useCallback(async() => {
|
||||
if(!service || !currentWorkspaceId) return;
|
||||
setCancelLoading(true);
|
||||
const plan = SubscriptionPlan.AIMax;
|
||||
|
||||
try {
|
||||
await service.cancelSubscription(currentWorkspaceId, plan, '');
|
||||
notify.success(t('subscribe.cancelPlan.success'));
|
||||
setCancelOpen(false);
|
||||
handleClose();
|
||||
// eslint-disable-next-line
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
} finally {
|
||||
setCancelLoading(false);
|
||||
}
|
||||
|
||||
}, [currentWorkspaceId, service, t, handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if(open) {
|
||||
void loadSubscription();
|
||||
}
|
||||
}, [open, loadSubscription]);
|
||||
|
||||
return (
|
||||
<NormalModal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
title={t('subscribe.upgradeAIMax')}
|
||||
disableRestoreFocus={true}
|
||||
cancelButtonProps={{
|
||||
className: 'hidden',
|
||||
}}
|
||||
okButtonProps={{
|
||||
className: 'hidden',
|
||||
}}
|
||||
slotProps={{
|
||||
root: {
|
||||
className: 'min-w-[500px] max-w-full max-h-full',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className={'flex relative border border-billing-primary rounded-[16px] flex-col gap-4 p-4 w-full'}>
|
||||
|
||||
<div className="flex flex-col gap-[14px]">
|
||||
<div className="text-billing-primary">
|
||||
{t('subscribe.AIMax.description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div className="text-xl font-semibold">$8</div>
|
||||
<div className="text-text-caption whitespace-pre-wrap">
|
||||
{t('subscribe.AIMax.pricing')}
|
||||
</div>
|
||||
</div>
|
||||
{!isActive ?
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
<Button
|
||||
color={'secondary'}
|
||||
onClick={handleUpgrade}
|
||||
variant={'contained'}
|
||||
>
|
||||
{t('subscribe.unlock')}
|
||||
</Button>
|
||||
</div> :
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCancelOpen(true);
|
||||
}}
|
||||
variant={'outlined'}
|
||||
color={'inherit'}
|
||||
startIcon={cancelLoading ? <CircularProgress size={14} /> : null}
|
||||
>
|
||||
{t('subscribe.cancel')}
|
||||
</Button>
|
||||
}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={'flex h-6 items-center'}>
|
||||
<div className={'w-2 h-2 rounded-full bg-billing-primary'} />
|
||||
</div>
|
||||
<div className="flex-1 whitespace-pre-wrap break-words">{t('subscribe.AIMax.points.first')}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={'flex h-6 items-center'}>
|
||||
<div className={'w-2 h-2 rounded-full bg-billing-primary'} />
|
||||
</div>
|
||||
<div className="flex-1 whitespace-pre-wrap break-words">
|
||||
{t('subscribe.AIMax.points.second')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={'flex h-6 items-center'}>
|
||||
<div className={'w-2 h-2 rounded-full bg-billing-primary'} />
|
||||
</div>
|
||||
<div className="flex-1 whitespace-pre-wrap break-words">{
|
||||
t('subscribe.AIMax.points.third')
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NormalModal
|
||||
title={
|
||||
<div className={'w-full text-left'}>{t('subscribe.AIMax.removeTitle')}</div>
|
||||
}
|
||||
classes={{ paper: 'w-[420px]' }}
|
||||
|
||||
open={cancelOpen}
|
||||
onOk={handleCancel}
|
||||
danger
|
||||
onClose={() => {
|
||||
setCancelOpen(false);
|
||||
}}
|
||||
okLoading={cancelLoading}
|
||||
onCancel={() => {
|
||||
setCancelOpen(false);
|
||||
}}
|
||||
okText={t('button.confirm')}
|
||||
>
|
||||
<div className={'opacity-80'}>
|
||||
{t('subscribe.AIMax.removeDescription')}
|
||||
|
||||
</div>
|
||||
</NormalModal>
|
||||
</NormalModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpgradeAIMax;
|
||||
@@ -26,11 +26,11 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
const action = search.get('action');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open && action === 'change_plan') {
|
||||
if(!open && action === 'change_plan') {
|
||||
onOpen();
|
||||
}
|
||||
|
||||
if (open) {
|
||||
if(open) {
|
||||
setSearch(prev => {
|
||||
prev.set('action', 'change_plan');
|
||||
return prev;
|
||||
@@ -39,11 +39,11 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [action, open, setSearch]);
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
const loadSubscription = useCallback(async() => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
if(!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscription({
|
||||
plan: SubscriptionPlan.Free,
|
||||
currency: '',
|
||||
@@ -56,7 +56,7 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscription(subscription);
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
@@ -70,8 +70,8 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
}, [onClose, setSearch]);
|
||||
const [interval, setInterval] = React.useState<SubscriptionInterval>(SubscriptionInterval.Year);
|
||||
|
||||
const handleUpgrade = useCallback(async () => {
|
||||
if (!service || !currentWorkspaceId) return;
|
||||
const handleUpgrade = useCallback(async() => {
|
||||
if(!service || !currentWorkspaceId) return;
|
||||
const plan = SubscriptionPlan.Pro;
|
||||
|
||||
try {
|
||||
@@ -79,13 +79,13 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
|
||||
window.open(link, '_current');
|
||||
// eslint-disable-next-line
|
||||
} catch (e: any) {
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
}
|
||||
}, [currentWorkspaceId, service, interval]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if(open) {
|
||||
void loadSubscription();
|
||||
}
|
||||
}, [open, loadSubscription]);
|
||||
@@ -105,6 +105,7 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
t('subscribe.freePoints.five'),
|
||||
t('subscribe.freePoints.six'),
|
||||
t('subscribe.freePoints.seven'),
|
||||
t('subscribe.freePoints.eight'),
|
||||
],
|
||||
}, {
|
||||
key: SubscriptionPlan.Pro,
|
||||
@@ -118,6 +119,7 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
t('subscribe.proPoints.three'),
|
||||
t('subscribe.proPoints.four'),
|
||||
t('subscribe.proPoints.five'),
|
||||
t('subscribe.proPoints.six'),
|
||||
],
|
||||
}];
|
||||
}, [t, interval]);
|
||||
@@ -142,13 +144,23 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
>
|
||||
<div className={'flex flex-col gap-4 p-4 w-full'}>
|
||||
<div className={'flex justify-between gap-4 items-center'}>
|
||||
<ViewTabs indicatorColor={'secondary'} value={interval} onChange={(_, v) => {
|
||||
setInterval(v);
|
||||
}}>
|
||||
<ViewTab label={`${t('subscribe.yearly')} ${t('subscribe.save', {
|
||||
discount: 20,
|
||||
})}`} value={SubscriptionInterval.Year}/>
|
||||
<ViewTab label={t('subscribe.monthly')} value={SubscriptionInterval.Month}/>
|
||||
<ViewTabs
|
||||
indicatorColor={'secondary'}
|
||||
value={interval}
|
||||
onChange={(_, v) => {
|
||||
setInterval(v);
|
||||
}}
|
||||
>
|
||||
<ViewTab
|
||||
label={`${t('subscribe.yearly')} ${t('subscribe.save', {
|
||||
discount: 20,
|
||||
})}`}
|
||||
value={SubscriptionInterval.Year}
|
||||
/>
|
||||
<ViewTab
|
||||
label={t('subscribe.monthly')}
|
||||
value={SubscriptionInterval.Month}
|
||||
/>
|
||||
</ViewTabs>
|
||||
<div className={'flex items-center justify-end'}>
|
||||
{t('subscribe.priceIn')}
|
||||
@@ -158,18 +170,24 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
|
||||
<div className={'flex gap-4 w-full overflow-auto'}>
|
||||
{plans.map((plan) => {
|
||||
return <div key={plan.key} style={{
|
||||
borderColor: activeSubscription?.plan === plan.key ? 'var(--billing-primary)' : undefined,
|
||||
}} className={'relative flex flex-col gap-2 p-4 border rounded-[16px] border-line-divider'}>
|
||||
return <div
|
||||
key={plan.key}
|
||||
style={{
|
||||
borderColor: activeSubscription?.plan === plan.key ? 'var(--billing-primary)' : undefined,
|
||||
}}
|
||||
className={'relative flex flex-col gap-2 p-4 border rounded-[16px] border-line-divider'}
|
||||
>
|
||||
{activeSubscription?.plan === plan.key &&
|
||||
<div
|
||||
className={'absolute bg-billing-primary text-content-on-fill right-0 top-0 rounded-[14px] text-xs rounded-br-none rounded-tl-none p-2'}>
|
||||
className={'absolute bg-billing-primary text-content-on-fill right-0 top-0 rounded-[14px] text-xs rounded-br-none rounded-tl-none p-2'}
|
||||
>
|
||||
{t('subscribe.currentPlan')}
|
||||
</div>}
|
||||
<div className={'font-medium'}>{plan.name}</div>
|
||||
<div className={'text-text-caption text-sm'}>{plan.description}</div>
|
||||
<div
|
||||
className={'text-lg'}>{plan.price}
|
||||
className={'text-lg'}
|
||||
>{plan.price}
|
||||
</div>
|
||||
<div className={'text-text-caption whitespace-pre-wrap'}>{plan.duration}</div>
|
||||
|
||||
@@ -179,23 +197,31 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
<Button
|
||||
color={'secondary'}
|
||||
onClick={handleUpgrade}
|
||||
variant={'contained'}>
|
||||
variant={'contained'}
|
||||
>
|
||||
{t('subscribe.changePlan')}
|
||||
</Button>}
|
||||
<span className={'font-medium'}>{t('subscribe.everythingInFree')}</span>
|
||||
</div> :
|
||||
activeSubscription?.plan !== plan.key &&
|
||||
<Button onClick={() => {
|
||||
setCancelOpen(true);
|
||||
}} variant={'outlined'} color={'inherit'}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCancelOpen(true);
|
||||
}}
|
||||
variant={'outlined'}
|
||||
color={'inherit'}
|
||||
>
|
||||
{t('subscribe.cancel')}
|
||||
</Button>
|
||||
}
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
{plan.points.map((point, index) => {
|
||||
return <div key={index} className={'flex gap-2 items-start'}>
|
||||
return <div
|
||||
key={index}
|
||||
className={'flex gap-2 items-start'}
|
||||
>
|
||||
<div className={'flex h-6 items-center'}>
|
||||
<div className={'w-2 h-2 rounded-full bg-billing-primary'}/>
|
||||
<div className={'w-2 h-2 rounded-full bg-billing-primary'} />
|
||||
</div>
|
||||
<div className={''}>{point}</div>
|
||||
</div>;
|
||||
@@ -205,9 +231,13 @@ function UpgradePlan({ open, onClose, onOpen }: {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<CancelSubscribe onCanceled={loadSubscription} open={cancelOpen} onClose={() => {
|
||||
setCancelOpen(false);
|
||||
}}/>
|
||||
<CancelSubscribe
|
||||
onCanceled={loadSubscription}
|
||||
open={cancelOpen}
|
||||
onClose={() => {
|
||||
setCancelOpen(false);
|
||||
}}
|
||||
/>
|
||||
</NormalModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { DatabaseContextProvider } from './DatabaseContext';
|
||||
|
||||
export interface Database2Props {
|
||||
workspaceId: string;
|
||||
doc: YDoc;
|
||||
readOnly?: boolean;
|
||||
createRowDoc?: CreateRowDoc;
|
||||
@@ -38,7 +39,8 @@ export interface Database2Props {
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
function Database ({
|
||||
function Database({
|
||||
workspaceId,
|
||||
doc,
|
||||
createRowDoc,
|
||||
navigateToView,
|
||||
@@ -65,19 +67,19 @@ function Database ({
|
||||
const rowOrders = view?.get(YjsDatabaseKey.row_orders);
|
||||
const [rowIds, setRowIds] = useState<RowId[]>([]);
|
||||
const [rowDocMap, setRowDocMap] = useState<Record<RowId, YDoc> | null>(null);
|
||||
const dbRows = useLiveQuery(async () => {
|
||||
const dbRows = useLiveQuery(async() => {
|
||||
const rows = await db.rows.bulkGet(rowIds.map(id => `${doc.guid}_rows_${id}`));
|
||||
|
||||
return rows;
|
||||
}, [rowIds, variant]);
|
||||
|
||||
const updateRowMap = useCallback(async () => {
|
||||
const updateRowMap = useCallback(async() => {
|
||||
const newRowMap: Record<RowId, YDoc> = {};
|
||||
|
||||
if (!dbRows || !createRowDoc) return;
|
||||
if(!dbRows || !createRowDoc) return;
|
||||
|
||||
for (const row of dbRows) {
|
||||
if (!row) {
|
||||
for(const row of dbRows) {
|
||||
if(!row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -100,7 +102,7 @@ function Database ({
|
||||
console.log('Database.tsx: rowDocMap', rowDocMap);
|
||||
}, [rowDocMap, database]);
|
||||
|
||||
const handleUpdateRowDocMap = useCallback(async () => {
|
||||
const handleUpdateRowDocMap = useCallback(async() => {
|
||||
setRowIds(rowOrders?.toJSON().map(({ id }: { id: string }) => id) || []);
|
||||
}, [rowOrders]);
|
||||
|
||||
@@ -113,13 +115,14 @@ function Database ({
|
||||
};
|
||||
}, [handleUpdateRowDocMap, rowOrders]);
|
||||
|
||||
if (!rowDocMap || !viewId) {
|
||||
if(!rowDocMap || !viewId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-1 justify-center'}>
|
||||
<DatabaseContextProvider
|
||||
workspaceId={workspaceId}
|
||||
isDatabaseRowPage={!!rowId}
|
||||
navigateToRow={onOpenRow}
|
||||
iidIndex={iidIndex}
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('<DatabaseRow />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function TestDatabaseRow ({
|
||||
function TestDatabaseRow({
|
||||
rowId,
|
||||
databaseDoc,
|
||||
rows,
|
||||
@@ -73,6 +73,7 @@ function TestDatabaseRow ({
|
||||
}) {
|
||||
return (
|
||||
<DatabaseContextProvider
|
||||
workspaceId={'test'}
|
||||
iidIndex={viewId}
|
||||
viewId={viewId}
|
||||
readOnly={true}
|
||||
|
||||
@@ -6,7 +6,7 @@ import DatabaseViews from '@/components/database/DatabaseViews';
|
||||
import { useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export function renderDatabase (
|
||||
export function renderDatabase(
|
||||
{
|
||||
databaseId,
|
||||
viewId,
|
||||
@@ -54,7 +54,7 @@ export function renderDatabase (
|
||||
});
|
||||
}
|
||||
|
||||
export function TestDatabase ({
|
||||
export function TestDatabase({
|
||||
databaseDoc,
|
||||
rows,
|
||||
iidIndex,
|
||||
@@ -82,6 +82,7 @@ export function TestDatabase ({
|
||||
readOnly={true}
|
||||
iidIndex={iidIndex}
|
||||
showActions={true}
|
||||
workspaceId={''}
|
||||
>
|
||||
<DatabaseViews
|
||||
iidIndex={iidIndex}
|
||||
|
||||
@@ -4,7 +4,7 @@ import EditorSkeleton from '@/components/_shared/skeleton/EditorSkeleton';
|
||||
import { Editor } from '@/components/editor';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export function DatabaseRowSubDocument ({ rowId }: { rowId: string }) {
|
||||
export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
||||
const meta = useRowMetaSelector(rowId);
|
||||
const readOnly = useReadOnly();
|
||||
const documentId = meta?.documentId;
|
||||
@@ -13,18 +13,19 @@ export function DatabaseRowSubDocument ({ rowId }: { rowId: string }) {
|
||||
const createRowDoc = context.createRowDoc;
|
||||
const navigateToView = context.navigateToView;
|
||||
const loadViewMeta = context.loadViewMeta;
|
||||
const workspaceId = context.workspaceId;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||
|
||||
const handleOpenDocument = useCallback(async () => {
|
||||
if (!loadView || !documentId) return;
|
||||
const handleOpenDocument = useCallback(async() => {
|
||||
if(!loadView || !documentId) return;
|
||||
try {
|
||||
setDoc(null);
|
||||
const doc = await loadView(documentId, true);
|
||||
|
||||
setDoc(doc);
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
// haven't created by client, ignore error and show empty
|
||||
}
|
||||
@@ -35,15 +36,16 @@ export function DatabaseRowSubDocument ({ rowId }: { rowId: string }) {
|
||||
void handleOpenDocument().then(() => setLoading(false));
|
||||
}, [handleOpenDocument]);
|
||||
|
||||
if (loading) {
|
||||
if(loading) {
|
||||
return (
|
||||
<EditorSkeleton />
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc || !documentId) return null;
|
||||
if(!doc || !documentId || !workspaceId) return null;
|
||||
return (
|
||||
<Editor
|
||||
workspaceId={workspaceId}
|
||||
viewId={documentId}
|
||||
doc={doc}
|
||||
loadViewMeta={loadViewMeta}
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, { useCallback, useEffect } from 'react';
|
||||
import { renderColor } from '@/utils/color';
|
||||
import ImageRender from '@/components/_shared/image-render/ImageRender';
|
||||
|
||||
function DatabaseRowHeader ({ rowId, appendBreadcrumb }: { rowId: string; appendBreadcrumb?: AppendBreadcrumb }) {
|
||||
function DatabaseRowHeader({ rowId, appendBreadcrumb }: { rowId: string; appendBreadcrumb?: AppendBreadcrumb }) {
|
||||
const fieldId = usePrimaryFieldId() || '';
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
@@ -16,9 +16,9 @@ function DatabaseRowHeader ({ rowId, appendBreadcrumb }: { rowId: string; append
|
||||
const cover = meta?.cover;
|
||||
|
||||
const renderCoverImage = useCallback((cover: RowMeta['cover']) => {
|
||||
if (!cover) return null;
|
||||
if(!cover) return null;
|
||||
|
||||
if (cover.cover_type === RowCoverType.GradientCover || cover.cover_type === RowCoverType.ColorCover) {
|
||||
if(cover.cover_type === RowCoverType.GradientCover || cover.cover_type === RowCoverType.ColorCover) {
|
||||
return <div
|
||||
style={{
|
||||
background: renderColor(cover.data),
|
||||
@@ -29,7 +29,7 @@ function DatabaseRowHeader ({ rowId, appendBreadcrumb }: { rowId: string; append
|
||||
|
||||
let url: string | undefined = cover.data;
|
||||
|
||||
if (cover.cover_type === RowCoverType.AssetCover) {
|
||||
if(cover.cover_type === RowCoverType.AssetCover) {
|
||||
url = {
|
||||
1: '/covers/m_cover_image_1.png',
|
||||
2: '/covers/m_cover_image_2.png',
|
||||
@@ -40,7 +40,7 @@ function DatabaseRowHeader ({ rowId, appendBreadcrumb }: { rowId: string; append
|
||||
}[Number(cover.data)];
|
||||
}
|
||||
|
||||
if (!url) return null;
|
||||
if(!url) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,11 +81,11 @@ function DatabaseRowHeader ({ rowId, appendBreadcrumb }: { rowId: string; append
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
|
||||
if (!el) return;
|
||||
if(!el) return;
|
||||
|
||||
const container = document.querySelector('.appflowy-scroll-container') || getScrollParent(el);
|
||||
const container = el.closest('.appflowy-scroll-container') || getScrollParent(el);
|
||||
|
||||
if (!container) return;
|
||||
if(!container) return;
|
||||
|
||||
const handleResize = () => {
|
||||
setOffsetLeft(container.getBoundingClientRect().left - el.getBoundingClientRect().left);
|
||||
|
||||
@@ -64,9 +64,10 @@ export const Document = (props: DocumentProps) => {
|
||||
el.style.minHeight = `${scrollElement?.clientHeight - 64}px`;
|
||||
}, [onRendered]);
|
||||
|
||||
if(!document || !viewMeta.viewId) return null;
|
||||
if(!document || !viewMeta.viewId || !viewMeta.workspaceId) return null;
|
||||
|
||||
return (
|
||||
|
||||
<div
|
||||
ref={ref}
|
||||
className={'flex h-full w-full flex-col items-center'}
|
||||
|
||||
@@ -21,7 +21,7 @@ import HrefPopover from '@/components/editor/components/leaf/href/HrefPopover';
|
||||
const EditorOverlay = lazy(() => import('@/components/editor/EditorOverlay'));
|
||||
|
||||
const EditorEditable = () => {
|
||||
const { readOnly, decorateState, onWordCountChange, viewId } = useEditorContext();
|
||||
const { readOnly, decorateState, onWordCountChange, viewId, workspaceId } = useEditorContext();
|
||||
const editor = useSlate();
|
||||
|
||||
const codeDecorate = useDecorate(editor);
|
||||
@@ -32,12 +32,12 @@ const EditorEditable = () => {
|
||||
class_name: string;
|
||||
})[] = [];
|
||||
|
||||
if (!decorateState) return [];
|
||||
if(!decorateState) return [];
|
||||
|
||||
Object.values(decorateState).forEach((state) => {
|
||||
const intersection = Range.intersection(state.range, Editor.range(editor, path));
|
||||
|
||||
if (intersection) {
|
||||
if(intersection) {
|
||||
highlightRanges.push({
|
||||
...intersection,
|
||||
class_name: state.class_name,
|
||||
@@ -69,8 +69,8 @@ const EditorEditable = () => {
|
||||
const onCompositionStart = useCallback(() => {
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
if (Range.isExpanded(selection)) {
|
||||
if(!selection) return;
|
||||
if(Range.isExpanded(selection)) {
|
||||
editor.delete();
|
||||
}
|
||||
}, [editor]);
|
||||
@@ -84,7 +84,7 @@ const EditorEditable = () => {
|
||||
}, [onWordCountChange, viewId, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (readOnly) return;
|
||||
if(readOnly) return;
|
||||
const { onChange } = editor;
|
||||
|
||||
editor.onChange = () => {
|
||||
@@ -104,13 +104,13 @@ const EditorEditable = () => {
|
||||
const currentTarget = e.currentTarget as HTMLElement;
|
||||
const bottomArea = currentTarget.getBoundingClientRect().bottom - 56 * 4;
|
||||
|
||||
if (e.clientY > bottomArea && e.clientY < (bottomArea + 56)) {
|
||||
if(e.clientY > bottomArea && e.clientY < (bottomArea + 56)) {
|
||||
const lastBlock = editor.children[editor.children.length - 1] as SlateElement;
|
||||
const isEmptyLine = CustomEditor.getBlockTextContent(lastBlock) === '';
|
||||
const type = lastBlock.type;
|
||||
|
||||
if (!lastBlock) return;
|
||||
if (isEmptyLine && type === BlockType.Paragraph) {
|
||||
if(!lastBlock) return;
|
||||
if(isEmptyLine && type === BlockType.Paragraph) {
|
||||
editor.select(editor.end([editor.children.length - 1]));
|
||||
return;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ const EditorEditable = () => {
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
const detail = e.detail;
|
||||
|
||||
if (detail >= 3) {
|
||||
if(detail >= 3) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -141,11 +141,13 @@ const EditorEditable = () => {
|
||||
return (
|
||||
<PanelProvider editor={editor}>
|
||||
<BlockPopoverProvider editor={editor}>
|
||||
<LeafContext.Provider value={{
|
||||
linkOpen,
|
||||
openLinkPopover: handleOpenLinkPopover,
|
||||
closeLinkPopover: handleCloseLinkPopover,
|
||||
}}>
|
||||
<LeafContext.Provider
|
||||
value={{
|
||||
linkOpen,
|
||||
openLinkPopover: handleOpenLinkPopover,
|
||||
closeLinkPopover: handleCloseLinkPopover,
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
role={'textbox'}
|
||||
decorate={(entry: NodeEntry) => {
|
||||
@@ -169,7 +171,10 @@ const EditorEditable = () => {
|
||||
/>
|
||||
{!readOnly &&
|
||||
<Suspense>
|
||||
<EditorOverlay/>
|
||||
<EditorOverlay
|
||||
workspaceId={workspaceId}
|
||||
viewId={viewId}
|
||||
/>
|
||||
<HrefPopover
|
||||
open={!!linkOpen}
|
||||
onClose={handleCloseLinkPopover}
|
||||
|
||||
@@ -31,13 +31,14 @@ describe('<Editor />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function renderEditor (doc: YDoc) {
|
||||
function renderEditor(doc: YDoc) {
|
||||
const AppWrapper = withAppWrapper(() => {
|
||||
return (
|
||||
<div className={'h-screen w-screen overflow-y-auto'}>
|
||||
<Editor
|
||||
doc={doc}
|
||||
readOnly
|
||||
workspaceId={''}
|
||||
viewId={''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
LoadView,
|
||||
LoadViewMeta, UIVariant, View, CreatePagePayload, TextCount,
|
||||
} from '@/application/types';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { createContext, useCallback, useContext, useState } from 'react';
|
||||
import { BaseRange, Range } from 'slate';
|
||||
|
||||
@@ -26,6 +27,7 @@ export interface Decorate {
|
||||
}
|
||||
|
||||
export interface EditorContextState {
|
||||
workspaceId: string;
|
||||
viewId: string;
|
||||
readOnly: boolean;
|
||||
layoutStyle?: EditorLayoutStyle;
|
||||
@@ -43,7 +45,6 @@ export interface EditorContextState {
|
||||
decorateState?: Record<string, Decorate>;
|
||||
addDecorate?: (range: BaseRange, class_name: string, type: string) => void;
|
||||
removeDecorate?: (type: string) => void;
|
||||
|
||||
selectedBlockIds?: string[];
|
||||
setSelectedBlockIds?: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
addPage?: (parentId: string, payload: CreatePagePayload) => Promise<string>;
|
||||
@@ -52,6 +53,7 @@ export interface EditorContextState {
|
||||
loadViews?: (variant?: UIVariant) => Promise<View[] | undefined>;
|
||||
onWordCountChange?: (viewId: string, props: TextCount) => void;
|
||||
uploadFile?: (file: File) => Promise<string>;
|
||||
requestInstance?: AxiosInstance | null;
|
||||
}
|
||||
|
||||
export const EditorContext = createContext<EditorContextState>({
|
||||
@@ -59,6 +61,7 @@ export const EditorContext = createContext<EditorContextState>({
|
||||
layoutStyle: defaultLayoutStyle,
|
||||
codeGrammars: {},
|
||||
viewId: '',
|
||||
workspaceId: '',
|
||||
});
|
||||
|
||||
export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => {
|
||||
@@ -69,7 +72,7 @@ export const EditorContextProvider = ({ children, ...props }: EditorContextState
|
||||
setDecorateState((prev) => {
|
||||
const oldValue = prev[type];
|
||||
|
||||
if (oldValue && Range.equals(oldValue.range, range) && oldValue.class_name === class_name) {
|
||||
if(oldValue && Range.equals(oldValue.range, range) && oldValue.class_name === class_name) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
@@ -85,7 +88,7 @@ export const EditorContextProvider = ({ children, ...props }: EditorContextState
|
||||
|
||||
const removeDecorate = useCallback((type: string) => {
|
||||
setDecorateState((prev) => {
|
||||
if (prev[type] === undefined) {
|
||||
if(prev[type] === undefined) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,208 @@
|
||||
import React from 'react';
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { isEmbedBlockTypes } from '@/application/slate-yjs/command/const';
|
||||
import { findSlateEntryByBlockId, getBlockEntry } from '@/application/slate-yjs/utils/editor';
|
||||
import '@appflowyinc/ai-chat/style';
|
||||
import { getBlock, getText } from '@/application/slate-yjs/utils/yjs';
|
||||
import { BlockType, YjsEditorKey } from '@/application/types';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { insertDataAfterBlock } from '@/components/ai-chat/utils';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { getScrollParent } from '@/components/global-comment/utils';
|
||||
import { AIAssistantProvider, ContextPlaceholder, WriterRequest } from '@appflowyinc/ai-chat';
|
||||
import { EditorData } from '@appflowyinc/editor';
|
||||
import { Portal } from '@mui/material';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import Toolbars from './components/toolbar';
|
||||
import Panels from './components/panels';
|
||||
import { Element, NodeEntry, Range, Text, Transforms } from 'slate';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import BlockPopover from './components/block-popover';
|
||||
import Panels from './components/panels';
|
||||
import Toolbars from './components/toolbar';
|
||||
|
||||
function EditorOverlay () {
|
||||
function EditorOverlay({
|
||||
viewId,
|
||||
workspaceId,
|
||||
}: {
|
||||
viewId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const { requestInstance } = useEditorContext();
|
||||
const editor = useSlate() as YjsEditor;
|
||||
const selection = editor.selection;
|
||||
const isRange = selection ? Range.isExpanded(selection) : false;
|
||||
const start = useMemo(() => selection ? editor.start(selection) : null, [editor, selection]);
|
||||
const end = useMemo(() => selection ? editor.end(selection) : null, [editor, selection]);
|
||||
const startBlock = useMemo(() => {
|
||||
if(!start) return null;
|
||||
try {
|
||||
return getBlockEntry(editor, start);
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}, [editor, start]);
|
||||
|
||||
const endBlock = useMemo(() => {
|
||||
if(!end) return null;
|
||||
try {
|
||||
return getBlockEntry(editor, end);
|
||||
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}, [editor, end]);
|
||||
|
||||
const writerRequest = useMemo(() => {
|
||||
return new WriterRequest(workspaceId, viewId, requestInstance || undefined);
|
||||
}, [workspaceId, viewId, requestInstance]);
|
||||
|
||||
const handleInsertBelow = useCallback((data: EditorData) => {
|
||||
if(!endBlock) return;
|
||||
try {
|
||||
const [node] = endBlock as NodeEntry<Element>;
|
||||
|
||||
if(!node) return;
|
||||
|
||||
const blockId = insertDataAfterBlock(editor.sharedRoot, data, node.blockId as string);
|
||||
|
||||
try {
|
||||
ReactEditor.focus(editor);
|
||||
const [, path] = findSlateEntryByBlockId(editor, blockId);
|
||||
|
||||
editor.select(editor.end(path));
|
||||
} catch(e) {
|
||||
//
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
}, [editor, endBlock]);
|
||||
|
||||
const handleReplaceSelection = useCallback((data: EditorData) => {
|
||||
try {
|
||||
if(data.length === 1 && !isEmbedBlockTypes(data[0].type as unknown as BlockType)) {
|
||||
ReactEditor.focus(editor);
|
||||
if(Range.isExpanded(editor.selection as Range)) {
|
||||
CustomEditor.deleteBlockForward(editor);
|
||||
}
|
||||
|
||||
const texts = data[0].delta?.map(op => {
|
||||
return {
|
||||
text: op.insert,
|
||||
...op.attributes,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
Transforms.insertNodes(editor, texts as Text[], {
|
||||
select: true,
|
||||
voids: false,
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
ReactEditor.focus(editor);
|
||||
if(Range.isExpanded(editor.selection as Range)) {
|
||||
CustomEditor.deleteBlockForward(editor);
|
||||
}
|
||||
|
||||
if(!startBlock) return;
|
||||
|
||||
const [node] = startBlock as NodeEntry<Element>;
|
||||
|
||||
if(!node) return;
|
||||
|
||||
const blockId = insertDataAfterBlock(editor.sharedRoot, data, node.blockId as string);
|
||||
const startYBlock = getBlock(node.blockId as string, editor.sharedRoot);
|
||||
const startYText = getText(startYBlock.get(YjsEditorKey.block_external_id), editor.sharedRoot);
|
||||
|
||||
if(startYText && startYText.length === 0) {
|
||||
CustomEditor.deleteBlock(editor, node.blockId as string);
|
||||
}
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
const [, path] = findSlateEntryByBlockId(editor, blockId);
|
||||
|
||||
editor.select(editor.end(path));
|
||||
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
} catch(e: any) {
|
||||
notify.error(e.message);
|
||||
}
|
||||
|
||||
}, [editor, startBlock]);
|
||||
const {
|
||||
removeDecorate,
|
||||
} = useEditorContext();
|
||||
|
||||
const handleExit = useCallback(() => {
|
||||
removeDecorate?.('ai-writer');
|
||||
if(!ReactEditor.isFocused(editor)) {
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
}, [removeDecorate, editor]);
|
||||
|
||||
const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
|
||||
const [scrollerContainer, setScrollerContainer] = React.useState<HTMLDivElement | null>(null);
|
||||
const [absoluteHeight, setAbsoluteHeight] = React.useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if(endBlock) {
|
||||
const [node] = endBlock;
|
||||
|
||||
try {
|
||||
const dom = ReactEditor.toDOMNode(editor, node);
|
||||
|
||||
const firstChild = dom.firstChild as HTMLElement;
|
||||
|
||||
if(firstChild && firstChild.innerText.trim() === '') {
|
||||
setAbsoluteHeight(firstChild.offsetHeight);
|
||||
} else {
|
||||
setAbsoluteHeight(0);
|
||||
}
|
||||
|
||||
setContainer(dom as HTMLDivElement);
|
||||
|
||||
const container = dom.closest('.appflowy-scroll-container') || getScrollParent(dom);
|
||||
|
||||
setScrollerContainer(container as HTMLDivElement);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}, [editor, endBlock]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallbackRender={() => null}>
|
||||
<Toolbars />
|
||||
<Panels />
|
||||
<BlockPopover />
|
||||
<AIAssistantProvider
|
||||
isGlobalDocument={!isRange}
|
||||
onInsertBelow={handleInsertBelow}
|
||||
onReplace={handleReplaceSelection}
|
||||
request={writerRequest}
|
||||
viewId={viewId}
|
||||
onExit={handleExit}
|
||||
scrollContainer={scrollerContainer || undefined}
|
||||
>
|
||||
<Toolbars />
|
||||
<Panels />
|
||||
<BlockPopover />
|
||||
<Portal
|
||||
container={container}
|
||||
>
|
||||
{absoluteHeight ? <div
|
||||
style={{
|
||||
transform: 'translateY(-' + absoluteHeight + 'px)',
|
||||
}}
|
||||
className={'w-full flex'}
|
||||
><ContextPlaceholder /></div> :
|
||||
<ContextPlaceholder />}
|
||||
|
||||
</Portal>
|
||||
|
||||
</AIAssistantProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function mountEditor(props: EditorProps) {
|
||||
);
|
||||
});
|
||||
|
||||
cy.mount(<AppWrapper/>);
|
||||
cy.mount(<AppWrapper />);
|
||||
}
|
||||
|
||||
export const moveToEnd = () => {
|
||||
@@ -35,7 +35,7 @@ export const moveToLineStart = (lineIndex: number) => {
|
||||
|
||||
cy.get(selector).as('targetBlock');
|
||||
|
||||
if (lineIndex === 0) {
|
||||
if(lineIndex === 0) {
|
||||
cy.get('@targetBlock').invoke('on', 'click', (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}).type('{movetostart}').wait(50);
|
||||
@@ -54,7 +54,7 @@ export const moveCursor = (lineIndex: number, charIndex: number) => {
|
||||
const batchSize = 1;
|
||||
const batches = Math.ceil(charIndex / batchSize);
|
||||
|
||||
for (let i = 0; i < batches; i++) {
|
||||
for(let i = 0; i < batches; i++) {
|
||||
const remainingMoves = Math.min(batchSize, charIndex - i * batchSize);
|
||||
|
||||
cy.get('@targetBlock').invoke('on', 'click', (e: MouseEvent) => {
|
||||
@@ -79,7 +79,7 @@ export const initialEditorTest = () => {
|
||||
const initializeEditor = (data: FromBlockJSON[]) => {
|
||||
documentTest = new DocumentTest();
|
||||
documentTest.fromJSON(data);
|
||||
mountEditor({ readOnly: false, doc: documentTest.doc, viewId: 'test' });
|
||||
mountEditor({ readOnly: false, doc: documentTest.doc, viewId: 'test', workspaceId: 'test' });
|
||||
cy.get('[role="textbox"]').should('exist');
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export const initialEditorTest = () => {
|
||||
};
|
||||
|
||||
export const getModKey = () => {
|
||||
if (Cypress.platform === 'darwin') {
|
||||
if(Cypress.platform === 'darwin') {
|
||||
return 'Meta';
|
||||
} else {
|
||||
return 'Control';
|
||||
|
||||
@@ -15,6 +15,7 @@ export const DatabaseBlock = memo(
|
||||
const { t } = useTranslation();
|
||||
const viewId = node.data.view_id;
|
||||
const context = useEditorContext();
|
||||
const workspaceId = context.workspaceId;
|
||||
const navigateToView = context?.navigateToView;
|
||||
const loadView = context?.loadView;
|
||||
const createRowDoc = context?.createRowDoc;
|
||||
@@ -175,6 +176,7 @@ export const DatabaseBlock = memo(
|
||||
{selectedViewId && doc ? (
|
||||
<>
|
||||
<Database
|
||||
workspaceId={workspaceId}
|
||||
doc={doc}
|
||||
iidIndex={viewId}
|
||||
viewId={selectedViewId}
|
||||
|
||||
@@ -12,6 +12,7 @@ function Img({ onLoad, imgRef, url, width }: {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [localUrl, setLocalUrl] = useState('');
|
||||
const [imgError, setImgError] = useState<{
|
||||
ok: boolean;
|
||||
status: number;
|
||||
@@ -31,13 +32,13 @@ function Img({ onLoad, imgRef, url, width }: {
|
||||
|
||||
const attemptCheck: () => Promise<boolean> = async() => {
|
||||
try {
|
||||
console.log(`Checking ${pollingInterval}ms`);
|
||||
const result = await checkImage(url);
|
||||
|
||||
// Success case
|
||||
if(result.ok) {
|
||||
setImgError(null);
|
||||
setLoading(false);
|
||||
setLocalUrl(result.validatedUrl || '');
|
||||
setTimeout(() => {
|
||||
if(onLoad) {
|
||||
onLoad();
|
||||
@@ -56,6 +57,7 @@ function Img({ onLoad, imgRef, url, width }: {
|
||||
|
||||
if(attempts >= maxAttempts || elapsedTime >= timeoutDuration) {
|
||||
setLoading(false); // Stop loading after max attempts or timeout
|
||||
setImgError({ ok: false, status: 404, statusText: 'Image Not Found' });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -91,7 +93,7 @@ function Img({ onLoad, imgRef, url, width }: {
|
||||
<>
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={url}
|
||||
src={localUrl}
|
||||
alt={''}
|
||||
onLoad={() => {
|
||||
setLoading(false);
|
||||
|
||||
@@ -7,7 +7,7 @@ import emptyImageSrc from '@/assets/images/empty.png';
|
||||
export const LinkPreview = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<LinkPreviewNode>>(({ node, children, ...attributes }, ref) => {
|
||||
const [data, setData] = useState<{
|
||||
image: { url: string };
|
||||
image?: { url: string };
|
||||
title: string;
|
||||
description: string;
|
||||
} | null>(null);
|
||||
@@ -15,15 +15,15 @@ export const LinkPreview = memo(
|
||||
const url = node.data.url;
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) return;
|
||||
if(!url) return;
|
||||
|
||||
setData(null);
|
||||
void (async () => {
|
||||
void (async() => {
|
||||
try {
|
||||
setNotFound(false);
|
||||
const response = await axios.get(`https://api.microlink.io/?url=${url}`);
|
||||
|
||||
if (response.data.statusCode !== 200) {
|
||||
if(response.data.statusCode !== 200) {
|
||||
setNotFound(true);
|
||||
return;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export const LinkPreview = memo(
|
||||
const data = response.data.data;
|
||||
|
||||
setData(data);
|
||||
} catch (_) {
|
||||
} catch(_) {
|
||||
setNotFound(true);
|
||||
}
|
||||
})();
|
||||
@@ -57,8 +57,13 @@ export const LinkPreview = memo(
|
||||
{notFound ? (
|
||||
<div className={'flex w-full items-center'}>
|
||||
<div
|
||||
className={'text-text-title min-w-[80px] w-[120px] flex items-center justify-center mr-2 h-[80px] border rounded'}>
|
||||
<img src={emptyImageSrc} alt={'Empty state'} className={'h-full object-center object-cover'}/>
|
||||
className={'text-text-title min-w-[80px] w-[120px] flex items-center justify-center mr-2 h-[80px] border rounded'}
|
||||
>
|
||||
<img
|
||||
src={emptyImageSrc}
|
||||
alt={'Empty state'}
|
||||
className={'h-full object-center object-cover'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex-1 flex flex-col'}>
|
||||
<div className={'text-function-error'}>
|
||||
@@ -73,7 +78,7 @@ export const LinkPreview = memo(
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={data?.image.url}
|
||||
src={data?.image?.url}
|
||||
alt={data?.title}
|
||||
className={'container h-full min-h-[48px] w-[25%] rounded bg-cover bg-center'}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Placeholder from '@/components/editor/components/blocks/text/Placeholder';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useSlateStatic } from 'slate-react';
|
||||
import { useStartIcon } from './StartIcon.hooks';
|
||||
import { EditorElementProps, TextNode } from '@/components/editor/editor.type';
|
||||
@@ -12,14 +13,16 @@ export const Text = forwardRef<HTMLSpanElement, EditorElementProps<TextNode>>(
|
||||
const className = useMemo(() => {
|
||||
const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-word'];
|
||||
|
||||
if (classNameProp) classList.push(classNameProp);
|
||||
if (hasStartIcon) classList.push('has-start-icon');
|
||||
if(classNameProp) classList.push(classNameProp);
|
||||
if(hasStartIcon) classList.push('has-start-icon');
|
||||
return classList.join(' ');
|
||||
}, [classNameProp, hasStartIcon]);
|
||||
|
||||
const placeholder = useMemo(() => {
|
||||
if (!isEmpty) return null;
|
||||
return <Placeholder node={node}/>;
|
||||
if(!isEmpty) return null;
|
||||
return <ErrorBoundary fallback={null}>
|
||||
<Placeholder node={node} />
|
||||
</ErrorBoundary>;
|
||||
}, [isEmpty, node]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
|
||||
31
src/components/editor/components/element/BlockNotFound.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { EditorElementProps } from '@/components/editor/editor.type';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
export const BlockNotFound = forwardRef<HTMLDivElement, EditorElementProps>(({ node }, ref) => {
|
||||
if(import.meta.env.DEV) {
|
||||
return (
|
||||
<div
|
||||
className={'w-full my-1 select-none'}
|
||||
ref={ref}
|
||||
contentEditable={false}
|
||||
>
|
||||
<Alert
|
||||
className={'h-fit w-full'}
|
||||
severity={'error'}
|
||||
>
|
||||
<div className={'text-base'}>{`Block not found, id is ${node.blockId}`}</div>
|
||||
<div>
|
||||
{'It might be deleted or moved to another place but the children map is still referencing it.'}
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div
|
||||
className={'w-full h-0 select-none'}
|
||||
ref={ref}
|
||||
contentEditable={false}
|
||||
/>;
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import SimpleTableRow from '@/components/editor/components/blocks/simple-table/S
|
||||
import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table';
|
||||
import { Text } from '@/components/editor/components/blocks/text';
|
||||
import { VideoBlock } from '@/components/editor/components/blocks/video';
|
||||
import { BlockNotFound } from '@/components/editor/components/element/BlockNotFound';
|
||||
import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock';
|
||||
import { EditorElementProps, TextNode } from '@/components/editor/editor.type';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
@@ -156,6 +157,8 @@ export const Element = ({
|
||||
return Columns;
|
||||
case BlockType.ColumnBlock:
|
||||
return Column;
|
||||
case 'block_not_found':
|
||||
return BlockNotFound;
|
||||
default:
|
||||
return UnSupportedBlock;
|
||||
}
|
||||
@@ -181,7 +184,7 @@ export const Element = ({
|
||||
const className = useMemo(() => {
|
||||
const data = (node.data as BlockData) || {};
|
||||
const align = data.align;
|
||||
const classList = ['block-element relative flex rounded-[4px]'];
|
||||
const classList = ['block-element flex-col relative flex rounded-[4px]'];
|
||||
|
||||
if(selected) {
|
||||
classList.push('selected');
|
||||
|
||||
@@ -41,7 +41,8 @@ import { ReactComponent as ToggleHeading2Icon } from '@/assets/toggle_heading2.s
|
||||
import { ReactComponent as ToggleHeading3Icon } from '@/assets/toggle_heading3.svg';
|
||||
import { ReactComponent as MathIcon } from '@/assets/slash_menu_icon_math_equation.svg';
|
||||
import { ReactComponent as VideoIcon } from '@/assets/video.svg';
|
||||
|
||||
import { ReactComponent as AskAIIcon } from '@/assets/ai.svg';
|
||||
import { ReactComponent as ContinueWritingIcon } from '@/assets/continue_writing.svg';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { calculateOptimalOrigins, Popover } from '@/components/_shared/popover';
|
||||
import { usePopoverContext } from '@/components/editor/components/block-popover/BlockPopoverContext';
|
||||
@@ -49,9 +50,11 @@ import { usePanelContext } from '@/components/editor/components/panels/Panels.ho
|
||||
import { PanelType } from '@/components/editor/components/panels/PanelsContext';
|
||||
import { getRangeRect } from '@/components/editor/components/toolbar/selection-toolbar/utils';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { getCharacters } from '@/utils/word';
|
||||
import { useAIWriter } from '@appflowyinc/ai-chat';
|
||||
import { Button } from '@mui/material';
|
||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
|
||||
@@ -67,9 +70,18 @@ export function SlashPanel({
|
||||
searchText,
|
||||
removeContent,
|
||||
} = usePanelContext();
|
||||
const {
|
||||
addPage,
|
||||
openPageModal,
|
||||
viewId,
|
||||
loadViewMeta,
|
||||
} = useEditorContext();
|
||||
const [viewName, setViewName] = useState('');
|
||||
|
||||
const editor = useSlateStatic() as YjsEditor;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const optionsRef = useRef<HTMLDivElement>(null);
|
||||
const editor = useSlateStatic() as YjsEditor;
|
||||
const [selectedOption, setSelectedOption] = React.useState<string | null>(null);
|
||||
const [transformOrigin, setTransformOrigin] = React.useState<PopoverOrigin | undefined>(undefined);
|
||||
const selectedOptionRef = React.useRef<string | null>(null);
|
||||
@@ -80,6 +92,38 @@ export function SlashPanel({
|
||||
return isPanelOpen(PanelType.Slash);
|
||||
}, [isPanelOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if(viewId && open) {
|
||||
void loadViewMeta?.(viewId).then((view) => {
|
||||
setViewName(view.name);
|
||||
});
|
||||
}
|
||||
}, [viewId, loadViewMeta, open]);
|
||||
|
||||
const getBeforeContent = useCallback(() => {
|
||||
const { selection } = editor;
|
||||
|
||||
if(!selection) return '';
|
||||
|
||||
const start = {
|
||||
path: [0],
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
const end = editor.end(selection);
|
||||
|
||||
return viewName + '\n' + CustomEditor.getSelectionContent(editor, {
|
||||
anchor: start,
|
||||
focus: end,
|
||||
});
|
||||
}, [editor, viewName]);
|
||||
|
||||
const chars = useMemo(() => {
|
||||
if(!open) return 0;
|
||||
|
||||
return getCharacters(getBeforeContent());
|
||||
}, [open, getBeforeContent]);
|
||||
|
||||
const handleSelectOption = useCallback((option: string) => {
|
||||
setSelectedOption(option);
|
||||
removeContent();
|
||||
@@ -121,14 +165,13 @@ export function SlashPanel({
|
||||
|
||||
}, [editor, openPopover]);
|
||||
|
||||
const {
|
||||
addPage,
|
||||
openPageModal,
|
||||
viewId,
|
||||
} = useEditorContext();
|
||||
|
||||
const { openPanel } = usePanelContext();
|
||||
|
||||
const {
|
||||
askAIAnything,
|
||||
continueWriting,
|
||||
} = useAIWriter();
|
||||
|
||||
const options: {
|
||||
label: string;
|
||||
key: string;
|
||||
@@ -137,12 +180,29 @@ export function SlashPanel({
|
||||
onClick?: () => void;
|
||||
}[] = useMemo(() => {
|
||||
return [
|
||||
// {
|
||||
// label: t('document.slashMenu.name.aiWriter'),
|
||||
// key: 'aiWriter',
|
||||
// icon: <AIWriterIcon />,
|
||||
// keywords: ['ai', 'writer'],
|
||||
// },
|
||||
{
|
||||
label: t('document.slashMenu.name.askAIAnything'),
|
||||
key: 'askAIAnything',
|
||||
icon: <AskAIIcon />,
|
||||
keywords: ['ai', 'writer', 'ask', 'anything', 'askAIAnything', 'askai'],
|
||||
onClick: () => {
|
||||
const content = getBeforeContent();
|
||||
|
||||
askAIAnything(content);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('document.slashMenu.name.continueWriting'),
|
||||
key: 'continueWriting',
|
||||
disabled: chars < 2,
|
||||
icon: <ContinueWritingIcon />,
|
||||
keywords: ['ai', 'writing', 'continue'],
|
||||
onClick: () => {
|
||||
const content = getBeforeContent();
|
||||
|
||||
void continueWriting(content);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('document.slashMenu.name.text'),
|
||||
key: 'text',
|
||||
@@ -465,12 +525,13 @@ export function SlashPanel({
|
||||
turnInto(BlockType.FileBlock, {});
|
||||
},
|
||||
}].filter((option) => {
|
||||
if(option.disabled) return false;
|
||||
if(!searchText) return true;
|
||||
return option.keywords.some((keyword: string) => {
|
||||
return keyword.toLowerCase().includes(searchText.toLowerCase());
|
||||
});
|
||||
});
|
||||
}, [t, turnInto, openPanel, viewId, addPage, openPageModal, setEmojiPosition, searchText]);
|
||||
}, [t, chars, getBeforeContent, askAIAnything, continueWriting, turnInto, openPanel, viewId, addPage, openPageModal, setEmojiPosition, searchText]);
|
||||
|
||||
const resultLength = options.length;
|
||||
|
||||
|
||||
@@ -67,19 +67,24 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {
|
||||
console.warn('No range and node found');
|
||||
return;
|
||||
} else if(range) {
|
||||
const match = editor.above({
|
||||
match: (n) => {
|
||||
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
|
||||
},
|
||||
at: range,
|
||||
});
|
||||
try {
|
||||
const match = editor.above({
|
||||
match: (n) => {
|
||||
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
|
||||
},
|
||||
at: range,
|
||||
});
|
||||
|
||||
if(!match) {
|
||||
close();
|
||||
return;
|
||||
if(!match) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
node = match[0];
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
node = match[0];
|
||||
}
|
||||
|
||||
if(!node) {
|
||||
|
||||
@@ -3,8 +3,9 @@ import { EditorMarkFormat } from '@/application/slate-yjs/types';
|
||||
import { getSelectionPosition } from '@/components/editor/components/toolbar/selection-toolbar/utils';
|
||||
import { Decorate, useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
|
||||
import { useAIWriter } from '@appflowyinc/ai-chat';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Range } from 'slate';
|
||||
import { ReactEditor, useFocused, useSlate, useSlateStatic } from 'slate-react';
|
||||
|
||||
@@ -20,24 +21,44 @@ export function useVisible() {
|
||||
const isExpanded = selection ? Range.isExpanded(selection) : false;
|
||||
|
||||
const selectedText = useMemo(() => {
|
||||
if (!selection) return 0;
|
||||
if(!selection) return 0;
|
||||
|
||||
return CustomEditor.getTextNodes(editor).length;
|
||||
}, [editor, selection]);
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
if (forceShow) return true;
|
||||
if (!focus) return false;
|
||||
const {
|
||||
assistantType,
|
||||
} = useAIWriter();
|
||||
|
||||
if (document.getSelection()?.isCollapsed) return false;
|
||||
|
||||
const show = Boolean(selectedText && isExpanded && !isDragging);
|
||||
|
||||
return show;
|
||||
}, [forceShow, focus, selectedText, isExpanded, isDragging]);
|
||||
const assistantTypeRef = useRef(assistantType);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
assistantTypeRef.current = assistantType;
|
||||
}, [assistantType]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelectionChange = () => {
|
||||
setVisible(() => {
|
||||
if(forceShow) return true;
|
||||
if(!focus) return false;
|
||||
|
||||
if(document.getSelection()?.isCollapsed || assistantTypeRef.current !== undefined) return false;
|
||||
|
||||
return Boolean(selectedText && isExpanded && !isDragging);
|
||||
});
|
||||
};
|
||||
|
||||
handleSelectionChange();
|
||||
document.addEventListener('selectionchange', handleSelectionChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', handleSelectionChange);
|
||||
};
|
||||
}, [focus, forceShow, isDragging, isExpanded, selectedText]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!visible) {
|
||||
removeDecorate?.('selection-toolbar');
|
||||
return;
|
||||
}
|
||||
@@ -47,9 +68,10 @@ export function useVisible() {
|
||||
useEffect(() => {
|
||||
|
||||
const handleMouseDown = () => {
|
||||
|
||||
const { selection } = editor;
|
||||
|
||||
if (selection && Range.isExpanded(selection)) {
|
||||
if(selection && Range.isExpanded(selection)) {
|
||||
window.getSelection()?.removeAllRanges();
|
||||
}
|
||||
|
||||
@@ -75,7 +97,7 @@ export function useVisible() {
|
||||
}, [editor, removeDecorate]);
|
||||
|
||||
const handleForceShow = useCallback((show: boolean) => {
|
||||
if (show && editor.selection) {
|
||||
if(show && editor.selection) {
|
||||
setForceShow(true);
|
||||
addDecorate?.(editor.selection, 'bg-content-blue-100', 'selection-toolbar');
|
||||
} else {
|
||||
@@ -88,12 +110,12 @@ export function useVisible() {
|
||||
}, [decorateState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
if(!visible) return;
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
switch (true) {
|
||||
switch(true) {
|
||||
case createHotkey(HOT_KEY_NAME.ESCAPE)(event): {
|
||||
if (!editor.selection) break;
|
||||
if(!editor.selection) break;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const start = editor.start(editor.selection);
|
||||
@@ -196,7 +218,7 @@ export function useToolbarPosition() {
|
||||
const left = position.left + slateEditorDom.offsetLeft;
|
||||
|
||||
// If toolbar is out of editor, move it to the left edge of the editor
|
||||
if (left <= 0) {
|
||||
if(left <= 0) {
|
||||
toolbarEl.style.left = '0px';
|
||||
return;
|
||||
}
|
||||
@@ -205,7 +227,7 @@ export function useToolbarPosition() {
|
||||
const rightBound = slateEditorDom.offsetWidth + slateEditorDom.offsetLeft;
|
||||
|
||||
// If toolbar is out of editor, move the right edge to the right edge of the editor
|
||||
if (right > rightBound) {
|
||||
if(right > rightBound) {
|
||||
toolbarEl.style.left = `${rightBound - toolbarEl.offsetWidth}px`;
|
||||
return;
|
||||
}
|
||||
@@ -216,7 +238,7 @@ export function useToolbarPosition() {
|
||||
const showToolbar = useCallback((toolbarEl: HTMLDivElement) => {
|
||||
const position = getSelectionPosition(editor);
|
||||
|
||||
if (position) {
|
||||
if(position) {
|
||||
toolbarEl.style.opacity = '1';
|
||||
toolbarEl.style.pointerEvents = 'auto';
|
||||
setPosition(toolbarEl, position);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { getBlockEntry } from '@/application/slate-yjs/utils/editor';
|
||||
import { BlockType } from '@/application/types';
|
||||
import AIAssistant from '@/components/editor/components/toolbar/selection-toolbar/actions/AIAssistant';
|
||||
import Align from '@/components/editor/components/toolbar/selection-toolbar/actions/Align';
|
||||
import Bold from '@/components/editor/components/toolbar/selection-toolbar/actions/Bold';
|
||||
import BulletedList from '@/components/editor/components/toolbar/selection-toolbar/actions/BulletedList';
|
||||
@@ -34,29 +35,29 @@ function ToolbarActions() {
|
||||
const end = useMemo(() => selection ? editor.end(selection) : null, [editor, selection]);
|
||||
|
||||
const startBlock = useMemo(() => {
|
||||
if (!start) return null;
|
||||
if(!start) return null;
|
||||
try {
|
||||
return getBlockEntry(editor, start);
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}, [editor, start]);
|
||||
const endBlock = useMemo(() => {
|
||||
if (!end) return null;
|
||||
if(!end) return null;
|
||||
try {
|
||||
return getBlockEntry(editor, end);
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}, [editor, end]);
|
||||
|
||||
const isAcrossBlock = useMemo(() => {
|
||||
if (startBlock && endBlock && Path.equals(startBlock[1], endBlock[1])) return false;
|
||||
if(startBlock && endBlock && Path.equals(startBlock[1], endBlock[1])) return false;
|
||||
return startBlock?.[0].blockId !== endBlock?.[0].blockId;
|
||||
}, [endBlock, startBlock]);
|
||||
|
||||
const isCodeBlock = useMemo(() => {
|
||||
if (!start || !end) return false;
|
||||
if(!start || !end) return false;
|
||||
const range = { anchor: start, focus: end };
|
||||
|
||||
const [codeBlock] = editor.nodes({
|
||||
@@ -67,57 +68,50 @@ function ToolbarActions() {
|
||||
return !!codeBlock;
|
||||
}, [editor, end, start]);
|
||||
|
||||
const groupTwo = <>
|
||||
<Underline/>
|
||||
<Bold/>
|
||||
<Italic/>
|
||||
<StrikeThrough/>
|
||||
</>;
|
||||
|
||||
const groupOne = <>
|
||||
<Paragraph/>
|
||||
<Heading/>
|
||||
<Divider
|
||||
className={'my-1.5 bg-line-on-toolbar'}
|
||||
orientation={'vertical'}
|
||||
flexItem={true}
|
||||
/>
|
||||
</>;
|
||||
|
||||
const groupThree = <>
|
||||
<Divider
|
||||
className={'my-1.5 bg-line-on-toolbar'}
|
||||
orientation={'vertical'}
|
||||
flexItem={true}
|
||||
/>
|
||||
<Quote/>
|
||||
<BulletedList/>
|
||||
<NumberedList/>
|
||||
<Divider
|
||||
className={'my-1.5 bg-line-on-toolbar'}
|
||||
orientation={'vertical'}
|
||||
flexItem={true}
|
||||
/>
|
||||
<Href/>
|
||||
</>;
|
||||
|
||||
const groupFour = <><Align enabled={toolbarVisible}/></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'flex w-fit flex-grow items-center gap-1'}
|
||||
>
|
||||
{!isCodeBlock && <AIAssistant />}
|
||||
{
|
||||
!isAcrossBlock && !isCodeBlock && groupOne
|
||||
!isAcrossBlock && !isCodeBlock && <>
|
||||
<Paragraph />
|
||||
<Heading />
|
||||
<Divider
|
||||
className={'my-1.5 bg-line-on-toolbar'}
|
||||
orientation={'vertical'}
|
||||
flexItem={true}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{groupTwo}
|
||||
{!isCodeBlock && <InlineCode/>}
|
||||
{!isCodeBlock && !isAcrossBlock && <Formula/>}
|
||||
<>
|
||||
<Underline />
|
||||
<Bold />
|
||||
<Italic />
|
||||
<StrikeThrough />
|
||||
</>
|
||||
{!isCodeBlock && <InlineCode />}
|
||||
{!isCodeBlock && !isAcrossBlock && <Formula />}
|
||||
{
|
||||
!isAcrossBlock && !isCodeBlock && groupThree
|
||||
!isAcrossBlock && !isCodeBlock && <>
|
||||
<Divider
|
||||
className={'my-1.5 bg-line-on-toolbar'}
|
||||
orientation={'vertical'}
|
||||
flexItem={true}
|
||||
/>
|
||||
<Quote />
|
||||
<BulletedList />
|
||||
<NumberedList />
|
||||
<Divider
|
||||
className={'my-1.5 bg-line-on-toolbar'}
|
||||
orientation={'vertical'}
|
||||
flexItem={true}
|
||||
/>
|
||||
<Href />
|
||||
</>
|
||||
}
|
||||
{!isCodeBlock && groupFour}
|
||||
<Color/>
|
||||
{!isCodeBlock && <Align enabled={toolbarVisible} />}
|
||||
<Color />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import ActionButton from '@/components/editor/components/toolbar/selection-toolbar/actions/ActionButton';
|
||||
import {
|
||||
useSelectionToolbarContext,
|
||||
} from '@/components/editor/components/toolbar/selection-toolbar/SelectionToolbar.hooks';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { useAIWriter, AIWriterMenu, AIAssistantType } from '@appflowyinc/ai-chat';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { ReactComponent as AskAIIcon } from '@/assets/ai.svg';
|
||||
import { ReactComponent as ImproveWritingIcon } from '@/assets/improve-writing.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
|
||||
|
||||
function AIAssistant() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlate() as YjsEditor;
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const {
|
||||
addDecorate,
|
||||
} = useEditorContext();
|
||||
const {
|
||||
visible: toolbarVisible,
|
||||
} = useSelectionToolbarContext();
|
||||
const {
|
||||
improveWriting,
|
||||
} = useAIWriter();
|
||||
const [content, setContent] = React.useState('');
|
||||
|
||||
const addReplaceStyle = useCallback(() => {
|
||||
const range = editor.selection;
|
||||
|
||||
if(!range) return;
|
||||
|
||||
addDecorate?.(range, 'line-through text-text-caption', 'ai-writer');
|
||||
}, [addDecorate, editor.selection]);
|
||||
|
||||
const addHighLightStyle = useCallback(() => {
|
||||
const range = editor.selection;
|
||||
|
||||
if(!range) return;
|
||||
|
||||
addDecorate?.(range, 'bg-content-blue-100', 'ai-writer');
|
||||
}, [addDecorate, editor.selection]);
|
||||
const onClickImproveWriting = useCallback(() => {
|
||||
addReplaceStyle();
|
||||
const content = CustomEditor.getSelectionContent(editor);
|
||||
|
||||
void improveWriting(content);
|
||||
}, [addReplaceStyle, editor, improveWriting]);
|
||||
|
||||
const isFilterOut = useCallback((type: AIAssistantType) => {
|
||||
return type === AIAssistantType.ContinueWriting;
|
||||
}, []);
|
||||
|
||||
const onItemClicked = useCallback((type: AIAssistantType) => {
|
||||
if([AIAssistantType.ImproveWriting, AIAssistantType.FixSpelling, AIAssistantType.MakeLonger, AIAssistantType.MakeShorter].includes(type)) {
|
||||
addReplaceStyle();
|
||||
} else {
|
||||
addHighLightStyle();
|
||||
}
|
||||
|
||||
ReactEditor.blur(editor);
|
||||
}, [addHighLightStyle, addReplaceStyle, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!toolbarVisible) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [toolbarVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
className={'!text-ai-primary !hover:text-billing-primary'}
|
||||
onClick={onClickImproveWriting}
|
||||
tooltip={t('editor.improveWriting')}
|
||||
>
|
||||
<ImproveWritingIcon />
|
||||
</ActionButton>
|
||||
<AIWriterMenu
|
||||
input={content}
|
||||
open={open}
|
||||
isFilterOut={isFilterOut}
|
||||
onItemClicked={onItemClicked}
|
||||
>
|
||||
<ActionButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setContent(CustomEditor.getSelectionContent(editor));
|
||||
setOpen(prev => !prev);
|
||||
}}
|
||||
className={'!text-ai-primary !hover:text-billing-primary'}
|
||||
tooltip={t('editor.askAI')}
|
||||
>
|
||||
<div className={'flex items-center justify-center'}>
|
||||
<AskAIIcon />
|
||||
|
||||
<RightIcon className={'transform h-3 w-3 rotate-90 text-icon-on-toolbar opacity-80'} />
|
||||
</div>
|
||||
|
||||
</ActionButton>
|
||||
</AIWriterMenu>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AIAssistant;
|
||||
@@ -1,9 +1,5 @@
|
||||
@use "src/styles/mixin.scss";
|
||||
|
||||
.block-element:not([data-block-type="table/cell"]) {
|
||||
@apply my-[4px];
|
||||
}
|
||||
|
||||
|
||||
.block-element {
|
||||
&:has(.embed-block) {
|
||||
@@ -11,7 +7,7 @@
|
||||
}
|
||||
|
||||
.embed-block {
|
||||
@apply z-[1] hover:bg-content-blue-50 flex relative w-full gap-4 overflow-hidden rounded-[8px] border border-line-divider bg-fill-list-active;
|
||||
@apply z-[1] my-1 hover:bg-content-blue-50 flex relative w-full gap-4 overflow-hidden rounded-[8px] border border-line-divider bg-fill-list-active;
|
||||
}
|
||||
|
||||
.math-equation-block, .gallery-block {
|
||||
@@ -506,3 +502,17 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#appflowy-ai-writer {
|
||||
@apply bg-bg-body;
|
||||
#appflowy-editor > div {
|
||||
padding: 0 !important;
|
||||
@apply underline text-writer-placeholder;
|
||||
|
||||
div[data-block-type="image"] {
|
||||
@apply border-[4px] border-content-blue-200 p-0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,11 +19,11 @@ export interface CollabViewProps {
|
||||
doc?: YDoc;
|
||||
}
|
||||
|
||||
function CollabView ({ doc }: CollabViewProps) {
|
||||
function CollabView({ doc }: CollabViewProps) {
|
||||
const visibleViewIds = usePublishContext()?.viewMeta?.visible_view_ids;
|
||||
const { viewId, layout, icon, cover, layoutClassName, style, name } = useViewMeta();
|
||||
const View = useMemo(() => {
|
||||
switch (layout) {
|
||||
switch(layout) {
|
||||
case ViewLayout.Document:
|
||||
return Document;
|
||||
case ViewLayout.Grid:
|
||||
@@ -47,11 +47,11 @@ function CollabView ({ doc }: CollabViewProps) {
|
||||
const className = useMemo(() => {
|
||||
const classList = ['relative w-full flex-1'];
|
||||
|
||||
if (isTemplateThumb && layout !== ViewLayout.Document) {
|
||||
if(isTemplateThumb && layout !== ViewLayout.Document) {
|
||||
classList.push('flex justify-center h-full');
|
||||
}
|
||||
|
||||
if (layoutClassName) {
|
||||
if(layoutClassName) {
|
||||
classList.push(layoutClassName);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ function CollabView ({ doc }: CollabViewProps) {
|
||||
}, [isTemplateThumb, layout, layoutClassName]);
|
||||
|
||||
const skeleton = useMemo(() => {
|
||||
switch (layout) {
|
||||
switch(layout) {
|
||||
case ViewLayout.Grid:
|
||||
return <GridSkeleton />;
|
||||
case ViewLayout.Board:
|
||||
@@ -73,9 +73,9 @@ function CollabView ({ doc }: CollabViewProps) {
|
||||
}
|
||||
}, [layout]);
|
||||
|
||||
if (!View) return null;
|
||||
if(!View) return null;
|
||||
|
||||
if (!doc) {
|
||||
if(!doc) {
|
||||
return skeleton;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ function CollabView ({ doc }: CollabViewProps) {
|
||||
className={className}
|
||||
>
|
||||
<View
|
||||
workspaceId={''}
|
||||
doc={doc}
|
||||
readOnly={true}
|
||||
loadViewMeta={loadViewMeta}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import ViewMetaPreview from 'src/components/view-meta/ViewMetaPreview';
|
||||
|
||||
export interface DatabaseProps {
|
||||
workspaceId: string;
|
||||
doc: YDoc;
|
||||
createRowDoc?: CreateRowDoc;
|
||||
loadView?: LoadView;
|
||||
@@ -31,7 +32,7 @@ export interface DatabaseProps {
|
||||
onRendered?: () => void;
|
||||
}
|
||||
|
||||
function DatabaseView ({ viewMeta, ...props }: DatabaseProps) {
|
||||
function DatabaseView({ viewMeta, ...props }: DatabaseProps) {
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const visibleViewIds = useMemo(() => viewMeta.visibleViewIds || [], [viewMeta]);
|
||||
|
||||
@@ -66,11 +67,11 @@ function DatabaseView ({ viewMeta, ...props }: DatabaseProps) {
|
||||
const database = doc?.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
|
||||
|
||||
const skeleton = useMemo(() => {
|
||||
if (rowId) {
|
||||
if(rowId) {
|
||||
return <DocumentSkeleton />;
|
||||
}
|
||||
|
||||
switch (viewMeta.layout) {
|
||||
switch(viewMeta.layout) {
|
||||
case ViewLayout.Grid:
|
||||
return <GridSkeleton includeTitle={false} />;
|
||||
case ViewLayout.Board:
|
||||
@@ -82,7 +83,7 @@ function DatabaseView ({ viewMeta, ...props }: DatabaseProps) {
|
||||
}
|
||||
}, [rowId, viewMeta.layout]);
|
||||
|
||||
if (!viewId || !database) return null;
|
||||
if(!viewId || !database) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -203,14 +203,14 @@ export function ViewMetaPreview({
|
||||
if(readOnly) return;
|
||||
setIconAnchorEl(e.currentTarget);
|
||||
}}
|
||||
className={`view-icon flex h-[1.25em] px-1.5 items-center justify-center ${readOnly ? 'cursor-default' : 'cursor-pointer hover:bg-fill-list-hover '}`}
|
||||
className={`view-icon flex h-[1.25em] w-[1.25em] px-1.5 items-center justify-center ${readOnly ? 'cursor-default' : 'cursor-pointer hover:bg-fill-list-hover '}`}
|
||||
>
|
||||
<PageIcon
|
||||
view={{
|
||||
icon,
|
||||
layout: ViewLayout.Document,
|
||||
}}
|
||||
className={'h-[90%] w-[80%] flex items-center justify-center'}
|
||||
className={'h-[90%] w-[90%] flex items-center justify-center'}
|
||||
/>
|
||||
</div>
|
||||
: null
|
||||
|
||||
@@ -22,34 +22,34 @@ function AcceptInvitationPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
if(!isAuthenticated) {
|
||||
navigate('/login?redirectTo=' + encodeURIComponent(window.location.href));
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const loadInvitation = useCallback(async (invitationId: string) => {
|
||||
if (!service) return;
|
||||
const loadInvitation = useCallback(async(invitationId: string) => {
|
||||
if(!service) return;
|
||||
try {
|
||||
const res = await service.getInvitation(invitationId);
|
||||
|
||||
if (res.status === 'Accepted') {
|
||||
if(res.status === 'Accepted') {
|
||||
notify.warning(t('invitation.alreadyAccepted'));
|
||||
}
|
||||
|
||||
setInvitation(res);
|
||||
// eslint-disable-next-line
|
||||
} catch (e: any) {
|
||||
} catch(e: any) {
|
||||
setModalOpened(true);
|
||||
}
|
||||
}, [service, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!invitationId) return;
|
||||
if(!invitationId) return;
|
||||
void loadInvitation(invitationId);
|
||||
}, [loadInvitation, invitationId]);
|
||||
|
||||
const workspaceIconProps = useMemo(() => {
|
||||
if (!invitation) return {};
|
||||
if(!invitation) return {};
|
||||
|
||||
return getAvatar({
|
||||
icon: invitation.workspace_icon,
|
||||
@@ -61,7 +61,7 @@ function AcceptInvitationPage() {
|
||||
}, []);
|
||||
|
||||
const inviterIconProps = useMemo(() => {
|
||||
if (!invitation) return {};
|
||||
if(!invitation) return {};
|
||||
|
||||
return getAvatar({
|
||||
icon: invitation.inviter_icon,
|
||||
@@ -79,7 +79,7 @@ function AcceptInvitationPage() {
|
||||
}}
|
||||
className={'flex w-full cursor-pointer max-md:justify-center max-md:h-32 h-20 items-center justify-between sticky'}
|
||||
>
|
||||
<AppflowyLogo className={'w-32 h-12 max-md:w-52'}/>
|
||||
<AppflowyLogo className={'w-32 h-12 max-md:w-52'} />
|
||||
</div>
|
||||
<div className={'flex w-full max-w-[560px] flex-col items-center gap-6 text-center'}>
|
||||
<Avatar
|
||||
@@ -98,15 +98,15 @@ function AcceptInvitationPage() {
|
||||
<span className={'whitespace-nowrap'}>AppFlowy</span>
|
||||
|
||||
</div>
|
||||
<Divider className={'max-w-full w-[400px]'}/>
|
||||
<Divider className={'max-w-full w-[400px]'} />
|
||||
<div className={'flex items-center justify-center py-1 gap-4'}>
|
||||
<Avatar
|
||||
className={'h-20 w-20 border border-line-divider text-[40px]'} {...inviterIconProps}
|
||||
variant="circular"
|
||||
/>
|
||||
<div className={'flex gap-1 flex-col items-start'}>
|
||||
<div className={'text-base'}>{t('invitation.invitedBy')}</div>
|
||||
<div className={'text-base font-semibold'}>{invitation?.inviter_name}</div>
|
||||
<div className={'text-text-title'}>{t('invitation.invitedBy')}</div>
|
||||
<div className={'text-text-title font-semibold'}>{invitation?.inviter_name}</div>
|
||||
<div className={'text-sm text-text-caption'}>{t('invitation.membersCount', {
|
||||
count: invitation?.member_count || 0,
|
||||
})}</div>
|
||||
@@ -118,7 +118,7 @@ function AcceptInvitationPage() {
|
||||
<div
|
||||
className={'border-b max-sm:border max-sm:rounded-[8px] border-line-border flex items-center gap-2 max-w-full py-2 px-4 w-[400px] bg-bg-body'}
|
||||
>
|
||||
<EmailOutlined/>
|
||||
<EmailOutlined />
|
||||
{currentUser?.email}
|
||||
</div>
|
||||
|
||||
@@ -127,9 +127,9 @@ function AcceptInvitationPage() {
|
||||
color={'primary'}
|
||||
size={'large'}
|
||||
className={'max-w-full w-[400px] rounded-[16px] text-[24px] py-5 px-10'}
|
||||
onClick={async () => {
|
||||
if (!invitationId) return;
|
||||
if (invitation?.status === 'Accepted') {
|
||||
onClick={async() => {
|
||||
if(!invitationId) return;
|
||||
if(invitation?.status === 'Accepted') {
|
||||
notify.warning(t('invitation.alreadyAccepted'));
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +149,7 @@ function AcceptInvitationPage() {
|
||||
},
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
notify.error('Failed to join workspace');
|
||||
}
|
||||
}}
|
||||
@@ -157,7 +157,11 @@ function AcceptInvitationPage() {
|
||||
{t('invitation.joinWorkspace')}
|
||||
</Button>
|
||||
</div>
|
||||
{isAuthenticated && <ChangeAccount redirectTo={url} setModalOpened={setModalOpened} modalOpened={modalOpened}/>}
|
||||
{isAuthenticated && <ChangeAccount
|
||||
redirectTo={url}
|
||||
setModalOpened={setModalOpened}
|
||||
modalOpened={modalOpened}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,17 @@ import { ReactComponent as TipIcon } from '@/assets/warning.svg';
|
||||
import Help from '@/components/_shared/help/Help';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { findView } from '@/components/_shared/outline/utils';
|
||||
import { AppContext, useAppHandlers, useAppOutline, useAppViewId } from '@/components/app/app.hooks';
|
||||
import {
|
||||
AppContext,
|
||||
useAppHandlers,
|
||||
useAppOutline,
|
||||
useAppViewId,
|
||||
useCurrentWorkspaceId,
|
||||
} from '@/components/app/app.hooks';
|
||||
import DatabaseView from '@/components/app/DatabaseView';
|
||||
import { Document } from '@/components/document';
|
||||
import RecordNotFound from '@/components/error/RecordNotFound';
|
||||
import { useService } from '@/components/main/app.hooks';
|
||||
import { getPlatform } from '@/utils/platform';
|
||||
import { desktopDownloadLink, openAppFlowySchema } from '@/utils/url';
|
||||
import { Button, Checkbox, FormControlLabel } from '@mui/material';
|
||||
@@ -19,7 +26,7 @@ function AppPage() {
|
||||
const viewId = useAppViewId();
|
||||
const outline = useAppOutline();
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const {
|
||||
toView,
|
||||
loadViewMeta,
|
||||
@@ -88,8 +95,9 @@ function AppPage() {
|
||||
visibleViewIds: [],
|
||||
viewId: view.view_id,
|
||||
extra: view.extra,
|
||||
workspaceId,
|
||||
} : null;
|
||||
}, [view]);
|
||||
}, [view, workspaceId]);
|
||||
|
||||
const handleUploadFile = useCallback((file: File) => {
|
||||
if(view && uploadFile) {
|
||||
@@ -99,6 +107,9 @@ function AppPage() {
|
||||
return Promise.reject();
|
||||
}, [uploadFile, view]);
|
||||
|
||||
const service = useService();
|
||||
const requestInstance = service?.getAxiosInstance();
|
||||
|
||||
const viewDom = useMemo(() => {
|
||||
const isMobile = getPlatform().isMobile;
|
||||
|
||||
@@ -111,8 +122,10 @@ function AppPage() {
|
||||
|
||||
const View = layout === ViewLayout.Document ? Document : DatabaseView;
|
||||
|
||||
return doc && viewMeta && View ? (
|
||||
return doc && viewMeta && workspaceId && View ? (
|
||||
<View
|
||||
requestInstance={requestInstance}
|
||||
workspaceId={workspaceId}
|
||||
doc={doc}
|
||||
readOnly={Boolean(isMobile)}
|
||||
viewMeta={viewMeta}
|
||||
@@ -132,7 +145,7 @@ function AppPage() {
|
||||
variant={UIVariant.App}
|
||||
/>
|
||||
) : null;
|
||||
}, [doc, layout, viewId, viewMeta, toView, loadViewMeta, createRowDoc, appendBreadcrumb, loadView, onRendered, updatePage, addPage, deletePage, openPageModal, loadViews, setWordCount, handleUploadFile]);
|
||||
}, [requestInstance, workspaceId, doc, layout, viewId, viewMeta, toView, loadViewMeta, createRowDoc, appendBreadcrumb, loadView, onRendered, updatePage, addPage, deletePage, openPageModal, loadViews, setWordCount, handleUploadFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!viewId) return;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.sketch-picker {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
--content-blue-300: #52d1f4;
|
||||
--content-blue-600: #009fd1;
|
||||
--content-blue-100: #005174;
|
||||
--content-blue-200: #49CFF480;
|
||||
--content-blue-50: #024562;
|
||||
--content-blue-700: #0079a5;
|
||||
--content-blue-800: #00597a;
|
||||
@@ -82,4 +83,7 @@
|
||||
--note-header: #232b38;
|
||||
--billing-primary: #601DAA;
|
||||
--billing-primary-hover: #7A2EBF;
|
||||
--ai-primary: #D08EED;
|
||||
--writer-placeholder: #49CFF4;
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
--content-blue-800: #00597a;
|
||||
--content-blue-900: #003d4d;
|
||||
--content-blue-100: #e0f8ff;
|
||||
--content-blue-200: #00729680;
|
||||
--content-blue-50: #f2fcff;
|
||||
--content-on-fill-hover: #00bcf0;
|
||||
--content-on-fill: #ffffff;
|
||||
@@ -84,4 +85,6 @@
|
||||
--note-header: #EDEFF3;
|
||||
--billing-primary: #8427e0;
|
||||
--billing-primary-hover: #9f3ae6;
|
||||
--ai-primary: #D08EED;
|
||||
--writer-placeholder: #007296;
|
||||
}
|
||||
@@ -22,13 +22,13 @@ export const checkImage = async(url: string) => {
|
||||
clearTimeout(timeoutId);
|
||||
// Add cache-busting parameter to prevent browser caching
|
||||
// which can sometimes hide image loading issues
|
||||
const cacheBuster = `?cb=${Date.now()}`;
|
||||
// const cacheBuster = `?cb=${Date.now()}`;
|
||||
|
||||
resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
validatedUrl: url + cacheBuster,
|
||||
validatedUrl: url,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Tue, 07 Jan 2025 10:54:45 GMT
|
||||
* Generated on Thu, 27 Mar 2025 11:44:33 GMT
|
||||
* Generated from $pnpm css:variables
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Tue, 07 Jan 2025 10:54:45 GMT
|
||||
* Generated on Thu, 27 Mar 2025 11:44:33 GMT
|
||||
* Generated from $pnpm css:variables
|
||||
*/
|
||||
|
||||
@@ -57,6 +57,7 @@ module.exports = {
|
||||
"blue-800": "var(--content-blue-800)",
|
||||
"blue-900": "var(--content-blue-900)",
|
||||
"blue-100": "var(--content-blue-100)",
|
||||
"blue-200": "var(--content-blue-200)",
|
||||
"blue-50": "var(--content-blue-50)",
|
||||
"on-fill-hover": "var(--content-on-fill-hover)",
|
||||
"on-fill": "var(--content-on-fill)",
|
||||
@@ -103,5 +104,11 @@ module.exports = {
|
||||
"billing": {
|
||||
"primary": "var(--billing-primary)",
|
||||
"primary-hover": "var(--billing-primary-hover)"
|
||||
},
|
||||
"ai": {
|
||||
"primary": "var(--ai-primary)"
|
||||
},
|
||||
"writer": {
|
||||
"placeholder": "var(--writer-placeholder)"
|
||||
}
|
||||
};
|
||||
|
||||