diff --git a/.github/workflows/web_ci.yaml b/.github/workflows/web_ci.yaml index cfaefc47..107a063c 100644 --- a/.github/workflows/web_ci.yaml +++ b/.github/workflows/web_ci.yaml @@ -42,19 +42,14 @@ jobs: uses: pnpm/action-setup@v2 with: version: ${{ env.PNPM_VERSION }} - - name: Node_modules cache - uses: actions/cache@v2 - with: - path: node_modules - key: node-modules-${{ runner.os }} - + - name: install frontend dependencies run: | pnpm install - name: Run lint check run: | pnpm run lint - + - name: build and analyze run: | pnpm run analyze >> analyze-size.txt diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 311a118f..ff59a24f 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -85,6 +85,9 @@ http { root /usr/share/nginx/html; expires 30d; access_log off; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET' always; } location /.well-known/apple-app-site-association { diff --git a/package.json b/package.json index b50eee92..053b9d6b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "coverage": "pnpm run test:unit && pnpm run test:components" }, "dependencies": { - "@appflowyinc/editor": "^0.0.40", + "@appflowyinc/ai-chat": "0.0.14", + "@appflowyinc/editor": "^0.1.5", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0693fd4..233f0910 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,12 @@ lockfileVersion: '6.0' dependencies: + '@appflowyinc/ai-chat': + specifier: 0.0.14 + version: 0.0.14(@appflowyinc/editor@0.1.5)(@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.0.40 - version: 0.0.40(@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.5 + version: 0.1.5(@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) @@ -532,8 +535,59 @@ packages: resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} dev: false - /@appflowyinc/editor@0.0.40(@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-JLKQfkNtzTlfTA8Unv/tBtVdnCp3lKSFjIRaEFyf6XpA/6JcK6mV0shzWjMRMX1AbmC7iCiTDlRbTkS3p+qpQA==} + /@appflowyinc/ai-chat@0.0.14(@appflowyinc/editor@0.1.5)(@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-5OqcuISj1MwWO5g+/Chy1kIeSjbbskmDlEqjHGgFeWl7W3IvgzEVIM7elU7u8LL8ZGxzGF6rZgHvgoTRn3A6cw==} + peerDependencies: + '@appflowyinc/editor': ^0.1.5 + axios: ^1.7.9 + dompurify: ^3.2.4 + i18next: ^22.4.10 + i18next-resources-to-backend: ^1.1.4 + react: ^18.2.0 + react-dom: ^18.2.0 + react-i18next: ^14.1.0 + dependencies: + '@appflowyinc/editor': 0.1.5(@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-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-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) + '@radix-ui/react-switch': 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-toast': 1.2.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-tooltip': 1.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + axios: 1.7.2 + class-variance-authority: 0.7.1 + clsx: 2.1.1 + dompurify: 3.1.7 + framer-motion: 12.4.7(react-dom@18.2.0)(react@18.2.0) + i18next: 22.5.1 + i18next-resources-to-backend: 1.2.1 + lodash-es: 4.17.21 + lucide-react: 0.468.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-error-boundary: 5.0.0(react@18.2.0) + 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 + tailwind-merge: 2.5.5 + tailwindcss: 3.4.17 + tailwindcss-animate: 1.0.7(tailwindcss@3.4.17) + typescript: 4.9.5 + uuid: 11.0.5 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - '@types/react-dom' + - supports-color + - ts-node + dev: false + + /@appflowyinc/editor@0.1.5(@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-BpTv1pQpgUqGkNM6KgtUSrTO0AYiTy0UHqUMG5VlAlXS/KDYrS3T1/IIkNaPT876lUvndxujBl5hSX4QXo0agA==} peerDependencies: i18next: ^22.4.10 i18next-resources-to-backend: ^1.2.1 @@ -548,7 +602,7 @@ packages: '@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-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.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-slot': 1.1.2(@types/react@18.2.66)(react@18.2.0) '@radix-ui/react-switch': 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-tooltip': 1.1.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) class-variance-authority: 0.7.1 @@ -558,6 +612,10 @@ packages: is-hotkey: 0.2.0 lucide-react: 0.468.0(react@18.2.0) mdast-util-from-markdown: 2.0.2 + mdast-util-gfm: 3.1.0 + mdast-util-gfm-table: 2.0.0 + micromark-extension-gfm: 3.0.0 + micromark-extension-gfm-table: 2.1.1 prismjs: 1.29.0 quill-delta: 5.1.0 react: 18.2.0 @@ -3903,6 +3961,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@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): + resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==} + 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-context': 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) + '@radix-ui/react-use-layout-effect': 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-collection@1.1.1(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} peerDependencies: @@ -3926,6 +4007,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@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): + resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} + 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-context': 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-slot': 1.1.2(@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-compose-refs@1.1.1(@types/react@18.2.66)(react@18.2.0): resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} peerDependencies: @@ -3989,6 +4093,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@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): + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + 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-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-escape-keydown': 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-focus-guards@1.1.1(@types/react@18.2.66)(react@18.2.0): resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -4142,6 +4270,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@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): + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + 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-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-layout-effect': 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-presence@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-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} peerDependencies: @@ -4183,6 +4332,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@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): + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + 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-slot': 1.1.2(@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: @@ -4257,6 +4426,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-slot@1.1.2(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + react: 18.2.0 + dev: false + /@radix-ui/react-switch@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-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==} peerDependencies: @@ -4283,6 +4466,37 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-toast@1.2.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==} + 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-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-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-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) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(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-tooltip@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-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==} peerDependencies: @@ -4429,6 +4643,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-visually-hidden@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-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} + 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-primitive': 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(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/rect@1.1.0: resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} dev: false @@ -8519,6 +8753,27 @@ packages: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true + /framer-motion@12.4.7(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + dependencies: + motion-dom: 12.4.5 + motion-utils: 12.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: false + /fromentries@1.3.2: resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} dev: true @@ -10406,6 +10661,10 @@ packages: wrap-ansi: 6.2.0 dev: true + /longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -10494,6 +10753,10 @@ packages: dependencies: tmpl: 1.0.5 + /markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + dev: false + /marked@13.0.3: resolution: {integrity: sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==} engines: {node: '>= 18'} @@ -10504,6 +10767,15 @@ packages: resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} dev: false + /mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + /mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} dependencies: @@ -10523,6 +10795,75 @@ packages: - supports-color dev: false + /mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.0 + dev: false + + /mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} dependencies: @@ -10544,6 +10885,20 @@ packages: vfile: 6.0.3 dev: false + /mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + dev: false + /mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} dependencies: @@ -10621,6 +10976,78 @@ packages: micromark-util-types: 2.0.0 dev: false + /micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + dependencies: + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.0 + dev: false + /micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} dependencies: @@ -10868,6 +11295,16 @@ packages: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} dev: false + /motion-dom@12.4.5: + resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==} + dependencies: + motion-utils: 12.0.0 + dev: false + + /motion-utils@12.0.0: + resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==} + dev: false + /mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -12045,6 +12482,15 @@ packages: react: 18.2.0 dev: false + /react-error-boundary@5.0.0(react@18.2.0): + resolution: {integrity: sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.26.0 + react: 18.2.0 + dev: false + /react-event-listener@0.6.6(react@18.2.0): resolution: {integrity: sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==} peerDependencies: @@ -12114,6 +12560,15 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-infinite-scroll-component@6.1.0(react@18.2.0): + resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==} + peerDependencies: + react: '>=16.0.0' + dependencies: + react: 18.2.0 + throttle-debounce: 2.3.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13594,6 +14049,11 @@ packages: real-require: 0.2.0 dev: true + /throttle-debounce@2.3.0: + resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==} + engines: {node: '>=8'} + dev: false + /throttleit@1.0.1: resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} dev: true @@ -13927,7 +14387,6 @@ packages: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true - dev: true /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} @@ -14158,6 +14617,11 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + dev: false + /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. diff --git a/src/@types/translations/en.json b/src/@types/translations/en.json index 8954855a..8778ac78 100644 --- a/src/@types/translations/en.json +++ b/src/@types/translations/en.json @@ -2695,7 +2695,8 @@ "error": { "pageNameIsEmpty": "The page name is empty, please try another one" }, - "visitOurWebsite": "Visit our official website" + "visitOurWebsite": "Visit our official website", + "addMessagesToPage": "Add messages to page" }, "globalComment": { "comments": "Comments", diff --git a/src/application/services/js-services/http/http_api.ts b/src/application/services/js-services/http/http_api.ts index abd1b774..cf2b1ae9 100644 --- a/src/application/services/js-services/http/http_api.ts +++ b/src/application/services/js-services/http/http_api.ts @@ -51,6 +51,10 @@ export * from './gotrue'; let axiosInstance: AxiosInstance | null = null; +export function getAxiosInstance() { + return axiosInstance; +} + export function initAPIService(config: AFCloudConfig) { if(axiosInstance) { return; diff --git a/src/application/services/js-services/index.ts b/src/application/services/js-services/index.ts index f3e99793..7894f27e 100644 --- a/src/application/services/js-services/index.ts +++ b/src/application/services/js-services/index.ts @@ -72,6 +72,10 @@ export class AFClientService implements AFService { APIService.initAPIService(config.cloudConfig); } + getAxiosInstance() { + return APIService.getAxiosInstance(); + } + getClientId() { return this.clientId; } diff --git a/src/application/services/js-services/sync.ts b/src/application/services/js-services/sync.ts index 62492524..64521a03 100644 --- a/src/application/services/js-services/sync.ts +++ b/src/application/services/js-services/sync.ts @@ -17,7 +17,7 @@ export class SyncManager { private isSending = false; - constructor (private doc: Y.Doc, private context: { + constructor(private doc: Y.Doc, private context: { userId: string, workspaceId: string, objectId: string, collabType: Types }) { this.versionVector = this.loadVersionVector(); @@ -26,41 +26,41 @@ export class SyncManager { this.setupListener(); } - private setupListener () { + private setupListener() { this.doc.on('update', (_update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) return; + if(origin === CollabOrigin.Remote) return; console.log('Local changes detected. Sending update...', origin); this.debouncedSendUpdate(); }); } - private getStorageKey (baseKey: string): string { + private getStorageKey(baseKey: string): string { return `${this.context.userId}_${baseKey}_${this.context.workspaceId}_${this.context.objectId}`; } - private loadVersionVector (): number { + private loadVersionVector(): number { const storedVector = localStorage.getItem(this.getStorageKey(VERSION_VECTOR_KEY)); return storedVector ? parseInt(storedVector, 10) : 0; } - private saveVersionVector () { + private saveVersionVector() { localStorage.setItem(this.getStorageKey(VERSION_VECTOR_KEY), this.versionVector.toString()); } - private loadUnsyncedFlag (): boolean { + private loadUnsyncedFlag(): boolean { return localStorage.getItem(this.getStorageKey(UNSYNCED_FLAG_KEY)) === 'true'; } - private saveUnsyncedFlag () { + private saveUnsyncedFlag() { localStorage.setItem(this.getStorageKey(UNSYNCED_FLAG_KEY), this.hasUnsyncedChanges.toString()); } - private loadLastSyncedAt (): string { + private loadLastSyncedAt(): string { return localStorage.getItem(this.getStorageKey(LAST_SYNCED_AT_KEY)) || ''; } - private saveLastSyncedAt () { + private saveLastSyncedAt() { localStorage.setItem(this.getStorageKey(LAST_SYNCED_AT_KEY), this.lastSyncedAt); } @@ -71,8 +71,8 @@ export class SyncManager { void this.sendUpdate(); }, 1000); // 1 second debounce - private async sendUpdate () { - if (this.isSending) return; + private async sendUpdate() { + if(this.isSending) return; this.isSending = true; try { @@ -85,14 +85,14 @@ export class SyncManager { const response = await updateCollab(this.context.workspaceId, this.context.objectId, this.context.collabType, update, context); - if (response) { + if(response) { console.log(`Update sent successfully. Server version: ${response.version_vector}`); // Update last synced time this.lastSyncedAt = String(Date.now()); this.saveLastSyncedAt(); - if (response.version_vector === this.versionVector) { + if(response.version_vector === this.versionVector) { // Our update was the latest this.hasUnsyncedChanges = false; this.saveUnsyncedFlag(); @@ -106,7 +106,7 @@ export class SyncManager { } else { return Promise.reject(response); } - } catch (error) { + } catch(error) { console.error('Failed to send update:', error); // Keep the unsynced flag as true this.hasUnsyncedChanges = true; @@ -116,23 +116,23 @@ export class SyncManager { } } - public initialize () { - if (this.hasUnsyncedChanges) { + public initialize() { + if(this.hasUnsyncedChanges) { console.log('Unsynced changes found. Sending update...'); // Send an update if there are unsynced changes this.debouncedSendUpdate(); } } - public getUnsyncedStatus (): boolean { + public getUnsyncedStatus(): boolean { return this.hasUnsyncedChanges; } - public getLastSyncedAt (): string { + public getLastSyncedAt(): string { return this.lastSyncedAt; } - public getCurrentVersionVector (): number { + public getCurrentVersionVector(): number { return this.versionVector; } } \ No newline at end of file diff --git a/src/application/services/services.type.ts b/src/application/services/services.type.ts index 237d5608..bf6ba1f2 100644 --- a/src/application/services/services.type.ts +++ b/src/application/services/services.type.ts @@ -35,9 +35,11 @@ import { TemplateCreator, TemplateCreatorFormValues, TemplateSummary, UploadTemplatePayload, } from '@/application/template.type'; +import { AxiosInstance } from 'axios'; export type AFService = PublishService & AppService & WorkspaceService & TemplateService & QuickNoteService & { getClientId: () => string; + getAxiosInstance: () => AxiosInstance | null; }; export interface AFServiceConfig { @@ -163,7 +165,7 @@ export interface PublishService { updatePublishHomepage: (workspaceId: string, viewId: string) => Promise; removePublishHomepage: (workspaceId: string) => Promise; - getPublishOutline (namespace: string): Promise; + getPublishOutline(namespace: string): Promise; getPublishViewGlobalComments: (viewId: string) => Promise; createCommentOnPublishView: (viewId: string, content: string, replyCommentId?: string) => Promise; diff --git a/src/application/slate-yjs/command/index.ts b/src/application/slate-yjs/command/index.ts index 34e526cb..2a082eb7 100644 --- a/src/application/slate-yjs/command/index.ts +++ b/src/application/slate-yjs/command/index.ts @@ -46,14 +46,14 @@ import { export const CustomEditor = { // 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)) { - if (node.formula) { + getBlockTextContent(node: Node, depth: number = Infinity): string { + if(Text.isText(node)) { + if(node.formula) { return node.formula; } - if (node.mention) { - if (node.mention.type === MentionType.Date) { + if(node.mention) { + if(node.mention.type === MentionType.Date) { const date = node.mention.date || ''; const isUnix = date?.length === 10; @@ -68,7 +68,7 @@ export const CustomEditor = { return node.text || ''; } - if (depth <= 0) { + if(depth <= 0) { return ''; // Prevent infinite recursion } @@ -77,9 +77,9 @@ export const CustomEditor = { .join(''); }, - setBlockData (editor: YjsEditor, blockId: string, updateData: T, select?: boolean) { + setBlockData(editor: YjsEditor, blockId: string, updateData: T, select?: boolean) { - if (editor.readOnly) { + if(editor.readOnly) { return; } @@ -95,7 +95,7 @@ export const CustomEditor = { } as Partial; const entry = findSlateEntryByBlockId(editor, blockId); - if (!entry) { + if(!entry) { console.error('Block not found'); return; } @@ -104,27 +104,27 @@ export const CustomEditor = { let atChild = false; const { selection } = editor; - if (selection && Path.isAncestor(path, selection.anchor.path)) { + if(selection && Path.isAncestor(path, selection.anchor.path)) { atChild = true; } Transforms.setNodes(editor, newProperties, { at: path }); - if (!select) return; + if(!select) return; - if (atChild) { + if(atChild) { Transforms.select(editor, Editor.start(editor, path)); } }, // Insert break line at the specified path - insertBreak (editor: YjsEditor, at?: BaseRange) { + insertBreak(editor: YjsEditor, at?: BaseRange) { const sharedRoot = getSharedRoot(editor); const newAt = getSelectionOrThrow(editor, at); const isCollapsed = Range.isCollapsed(newAt); - if (isCollapsed) { + if(isCollapsed) { handleCollapsedBreakWithTxn(editor, sharedRoot, newAt); } else { handleRangeBreak(editor, sharedRoot, newAt); @@ -132,7 +132,7 @@ export const CustomEditor = { }, - deleteBlockBackward (editor: YjsEditor, at?: BaseRange) { + deleteBlockBackward(editor: YjsEditor, at?: BaseRange) { console.trace('deleteBlockBackward', editor.selection, at); const sharedRoot = getSharedRoot(editor); @@ -140,7 +140,7 @@ export const CustomEditor = { const isCollapsed = Range.isCollapsed(newAt); - if (isCollapsed) { + if(isCollapsed) { const point = newAt.anchor; const blockEntry = getBlockEntry(editor, point); @@ -150,16 +150,16 @@ export const CustomEditor = { const blockType = block.get(YjsEditorKey.block_type) as BlockType; const parent = getParent(node.blockId as string, sharedRoot); - if (blockType !== BlockType.Paragraph && parent?.get(YjsEditorKey.block_type) === BlockType.QuoteBlock && LIST_BLOCK_TYPES.includes(blockType)) { + if(blockType !== BlockType.Paragraph && parent?.get(YjsEditorKey.block_type) === BlockType.QuoteBlock && LIST_BLOCK_TYPES.includes(blockType)) { handleNonParagraphBlockBackspaceAndEnterWithTxn(editor, sharedRoot, block, point); return; } - if (path.length > 1 && handleLiftBlockOnBackspaceAndEnterWithTxn(editor, sharedRoot, block, point)) { + if(path.length > 1 && handleLiftBlockOnBackspaceAndEnterWithTxn(editor, sharedRoot, block, point)) { return; } - if (blockType !== BlockType.Paragraph) { + if(blockType !== BlockType.Paragraph) { handleNonParagraphBlockBackspaceAndEnterWithTxn(editor, sharedRoot, block, point); return; } @@ -172,13 +172,13 @@ export const CustomEditor = { } }, - deleteBlockForward (editor: YjsEditor, at?: BaseRange) { + deleteBlockForward(editor: YjsEditor, at?: BaseRange) { const sharedRoot = getSharedRoot(editor); const newAt = getSelectionOrThrow(editor, at); const isCollapsed = Range.isCollapsed(newAt); - if (isCollapsed) { + if(isCollapsed) { const point = newAt.anchor; const blockEntry = getBlockEntry(editor, point); @@ -192,28 +192,28 @@ export const CustomEditor = { } }, - deleteEntireDocument (editor: YjsEditor) { + deleteEntireDocument(editor: YjsEditor) { handleDeleteEntireDocumentWithTxn(editor); }, - removeRange (editor: YjsEditor, at: BaseRange) { + removeRange(editor: YjsEditor, at: BaseRange) { removeRangeWithTxn(editor, getSharedRoot(editor), at); }, - tabEvent (editor: YjsEditor, event: KeyboardEvent) { + tabEvent(editor: YjsEditor, event: KeyboardEvent) { const type = event.shiftKey ? 'tabBackward' : 'tabForward'; const sharedRoot = getSharedRoot(editor); const { selection } = editor; - if (!selection) return; + if(!selection) return; const [point, endPoint] = editor.edges(selection); const { middleBlocks, startBlock: node, endBlock: endNode } = getAffectedBlocks(editor, selection); - if (type === 'tabBackward' && preventLiftNode(editor, node[0].blockId as string)) { + if(type === 'tabBackward' && preventLiftNode(editor, node[0].blockId as string)) { return; } - if (type === 'tabForward' && preventIndentNode(editor, node[0].blockId as string)) { + if(type === 'tabForward' && preventIndentNode(editor, node[0].blockId as string)) { return; } @@ -232,7 +232,7 @@ export const CustomEditor = { const isSameBlock = node[0].blockId === endNode[0].blockId; editor.deselect(); - if (isSameBlock) { + if(isSameBlock) { const block = getBlock(node[0].blockId as string, sharedRoot); let newBlockId: string | undefined; @@ -240,7 +240,7 @@ export const CustomEditor = { newBlockId = type === 'tabForward' ? indentBlock(sharedRoot, block) : liftBlock(sharedRoot, block); }], type === 'tabForward' ? 'indentBlock' : 'liftBlock'); - if (!newBlockId) return; + if(!newBlockId) return; const newBlockEntry = findSlateEntryByBlockId(editor, newBlockId); newStartBlockPath = newBlockEntry[1]; @@ -253,17 +253,17 @@ export const CustomEditor = { const blockId = entry[0].blockId as string; const block = getBlock(blockId, sharedRoot); - if (!block) return; + if(!block) return; executeOperations(sharedRoot, [() => { const newBlockId = type === 'tabForward' ? indentBlock(sharedRoot, block) : liftBlock(sharedRoot, block, index); - if (newBlockId) { + if(newBlockId) { newBlockIds.push(newBlockId); } }], type === 'tabForward' ? 'indentBlock' : 'liftBlock'); }); - if (newBlockIds.length === 0) return; + if(newBlockIds.length === 0) return; const newStartBlockEntry = findSlateEntryByBlockId(editor, newBlockIds[0]); const newEndBlockEntry = findSlateEntryByBlockId(editor, newBlockIds[newBlockIds.length - 1]); @@ -286,18 +286,18 @@ export const CustomEditor = { }); }, - toggleToggleList (editor: YjsEditor, blockId: string) { + toggleToggleList(editor: YjsEditor, blockId: string) { const sharedRoot = getSharedRoot(editor); const data = dataStringTOJson(getBlock(blockId, sharedRoot).get(YjsEditorKey.block_data)) as ToggleListBlockData; const { selection } = editor; - if (selection && Range.isExpanded(selection)) { + if(selection && Range.isExpanded(selection)) { Transforms.collapse(editor, { edge: 'start' }); } let selected = false; - if (selection) { + if(selection) { const point = Editor.start(editor, selection); const [node] = getBlockEntry(editor, point); @@ -310,13 +310,13 @@ export const CustomEditor = { }, selected); }, - toggleTodoList (editor: YjsEditor, blockId: string, shiftKey: boolean) { + toggleTodoList(editor: YjsEditor, blockId: string, shiftKey: boolean) { const sharedRoot = getSharedRoot(editor); const block = getBlock(blockId, sharedRoot); const data = dataStringTOJson(block.get(YjsEditorKey.block_data)) as TodoListBlockData; const checked = data.checked; - if (!shiftKey) { + if(!shiftKey) { CustomEditor.setBlockData(editor, blockId, { checked: !checked, }, false); @@ -344,12 +344,12 @@ export const CustomEditor = { }); }, - toggleMark (editor: ReactEditor, { + toggleMark(editor: ReactEditor, { key, value, }: { key: EditorMarkFormat, value: boolean | string }) { - if (CustomEditor.isMarkActive(editor, key)) { + if(CustomEditor.isMarkActive(editor, key)) { CustomEditor.removeMark(editor, key); } else { CustomEditor.addMark(editor, { @@ -358,11 +358,11 @@ export const CustomEditor = { } }, - getTextNodes (editor: ReactEditor) { + getTextNodes(editor: ReactEditor) { return getSelectionTexts(editor); }, - addMark (editor: ReactEditor, { + addMark(editor: ReactEditor, { key, value, }: { key: EditorMarkFormat, value: boolean | string | Mention @@ -370,18 +370,18 @@ export const CustomEditor = { editor.addMark(key, value); }, - removeMark (editor: ReactEditor, key: EditorMarkFormat) { + removeMark(editor: ReactEditor, key: EditorMarkFormat) { editor.removeMark(key); }, - turnToBlock (editor: YjsEditor, blockId: string, type: BlockType, data: T) { + turnToBlock(editor: YjsEditor, blockId: string, type: BlockType, data: T) { const operations: (() => void)[] = []; const sharedRoot = getSharedRoot(editor); const sourceBlock = getBlock(blockId, sharedRoot); const sourceType = sourceBlock.get(YjsEditorKey.block_type) as BlockType; const oldData = dataStringTOJson(sourceBlock.get(YjsEditorKey.block_data)); - if (sourceType === type && isEqual(oldData, data)) { + if(sourceType === type && isEqual(oldData, data)) { return; } @@ -395,31 +395,31 @@ export const CustomEditor = { return newBlockId; }, - isBlockActive (editor: YjsEditor, type: BlockType) { + isBlockActive(editor: YjsEditor, type: BlockType) { try { const [node] = getBlockEntry(editor); return node.type === type; - } catch (e) { + } catch(e) { return false; } }, - hasMark (editor: ReactEditor, key: string) { + hasMark(editor: ReactEditor, key: string) { const selection = editor.selection; - if (!selection) return false; + if(!selection) return false; const isExpanded = Range.isExpanded(selection); - if (isExpanded) { + if(isExpanded) { const texts = getSelectionTexts(editor); return texts.some((node) => { const { text, ...attributes } = node; - if (!text) return true; + if(!text) return true; return Boolean((attributes as Record)[key]); }); } @@ -429,20 +429,20 @@ export const CustomEditor = { return marks ? !!marks[key] : false; }, - getAllMarks (editor: ReactEditor) { + getAllMarks(editor: ReactEditor) { const selection = editor.selection; - if (!selection) return []; + if(!selection) return []; const isExpanded = Range.isExpanded(selection); - if (isExpanded) { + if(isExpanded) { const texts = getSelectionTexts(editor); return texts.map((node) => { const { text, ...attributes } = node; - if (!text) return {}; + if(!text) return {}; return attributes as EditorInlineAttributes; }); } @@ -452,35 +452,41 @@ export const CustomEditor = { return [marks]; }, - isMarkActive (editor: ReactEditor, key: string) { - const selection = editor.selection; + isMarkActive(editor: ReactEditor, key: string) { + try { + const selection = editor.selection; - if (!selection) return false; + if(!selection) return false; - const isExpanded = Range.isExpanded(selection); + const isExpanded = Range.isExpanded(selection); - if (isExpanded) { + if(isExpanded) { - const texts = getSelectionTexts(editor); + const texts = getSelectionTexts(editor); - return texts.every((node) => { - const { text, ...attributes } = node; + return texts.every((node) => { + const { text, ...attributes } = node; - if (!text) return true; - return Boolean((attributes as Record)[key]); - }); + if(!text) return true; + return Boolean((attributes as Record)[key]); + }); + + } + + const marks = Editor.marks(editor) as Record | null; + + return marks ? !!marks[key] : false; + } catch(e) { + return false; } - const marks = Editor.marks(editor) as Record | null; - - return marks ? !!marks[key] : false; }, - addChildBlock (editor: YjsEditor, blockId: string, type: BlockType, data: BlockData) { + addChildBlock(editor: YjsEditor, blockId: string, type: BlockType, data: BlockData) { const sharedRoot = getSharedRoot(editor); const parent = getBlock(blockId, sharedRoot); - if (!parent) { + if(!parent) { console.warn('Parent block not found'); return; } @@ -490,7 +496,7 @@ export const CustomEditor = { data, }, parent, 0); - if (!newBlockId) { + if(!newBlockId) { console.warn('Failed to add block'); return; } @@ -498,61 +504,61 @@ export const CustomEditor = { try { const [, path] = findSlateEntryByBlockId(editor, newBlockId); - if (path) { + if(path) { ReactEditor.focus(editor); const point = editor.start(path); Transforms.select(editor, point); return newBlockId; } - } catch (e) { + } catch(e) { console.error(e); } }, - addBlock (editor: YjsEditor, blockId: string, direction: 'below' | 'above', type: BlockType, data: BlockData) { + addBlock(editor: YjsEditor, blockId: string, direction: 'below' | 'above', type: BlockType, data: BlockData) { const parent = getParent(blockId, editor.sharedRoot); const index = getBlockIndex(blockId, editor.sharedRoot); - if (!parent) return; + if(!parent) return; const newBlockId = addBlock(editor, { ty: type, data, }, parent, direction === 'below' ? index + 1 : index); - if (!newBlockId) { + if(!newBlockId) { return; } try { const [, path] = findSlateEntryByBlockId(editor, newBlockId); - if (path) { + if(path) { ReactEditor.focus(editor); const point = editor.start(path); Transforms.select(editor, point); return newBlockId; } - } catch (e) { + } catch(e) { console.error(e); } }, - addBelowBlock (editor: YjsEditor, blockId: string, type: BlockType, data: BlockData) { + addBelowBlock(editor: YjsEditor, blockId: string, type: BlockType, data: BlockData) { return CustomEditor.addBlock(editor, blockId, 'below', type, data); }, - addAboveBlock (editor: YjsEditor, blockId: string, type: BlockType, data: BlockData) { + addAboveBlock(editor: YjsEditor, blockId: string, type: BlockType, data: BlockData) { return CustomEditor.addBlock(editor, blockId, 'above', type, data); }, - deleteBlock (editor: YjsEditor, blockId: string) { + deleteBlock(editor: YjsEditor, blockId: string) { const sharedRoot = getSharedRoot(editor); const parent = getParent(blockId, sharedRoot); - if (!parent) { + if(!parent) { console.warn('Parent block not found'); return; } @@ -561,8 +567,8 @@ export const CustomEditor = { const prevBlockId = getPreviousSiblingBlock(sharedRoot, getBlock(blockId, sharedRoot)); let point: BasePoint | undefined; - if (!prevBlockId) { - if (parent.get(YjsEditorKey.block_type) !== BlockType.Page) { + if(!prevBlockId) { + if(parent.get(YjsEditorKey.block_type) !== BlockType.Page) { const [, path] = findSlateEntryByBlockId(editor, parent.get(YjsEditorKey.block_id)); point = editor.start(path); @@ -573,7 +579,7 @@ export const CustomEditor = { point = editor.end(path); } - if (point && ReactEditor.hasRange(editor, { + if(point && ReactEditor.hasRange(editor, { anchor: point, focus: point, })) { @@ -582,7 +588,7 @@ export const CustomEditor = { Transforms.deselect(editor); } - } catch (e) { + } catch(e) { // do nothing } @@ -591,7 +597,7 @@ export const CustomEditor = { }], 'deleteBlock'); const children = editor.children; - if (children.length === 0) { + if(children.length === 0) { addBlock(editor, { ty: BlockType.Paragraph, data: {}, @@ -601,14 +607,14 @@ export const CustomEditor = { ReactEditor.focus(editor); }, - duplicateBlock (editor: YjsEditor, blockId: string, prevId?: string) { + duplicateBlock(editor: YjsEditor, blockId: string, prevId?: string) { const sharedRoot = getSharedRoot(editor); const block = getBlock(blockId, sharedRoot); const parent = getParent(blockId, sharedRoot); const prevIndex = getBlockIndex(prevId || blockId, sharedRoot); - if (!parent) { + if(!parent) { console.warn('Parent block not found'); return; } @@ -618,7 +624,7 @@ export const CustomEditor = { executeOperations(sharedRoot, [() => { newBlockId = deepCopyBlock(sharedRoot, block); - if (!newBlockId) { + if(!newBlockId) { console.warn('Copied block not found'); return; } @@ -631,8 +637,8 @@ export const CustomEditor = { return newBlockId; }, - pastedText (editor: YjsEditor, text: string) { - if (!beforePasted(editor)) + pastedText(editor: YjsEditor, text: string) { + if(!beforePasted(editor)) return; const point = editor.selection?.anchor as BasePoint; @@ -640,19 +646,19 @@ export const CustomEditor = { Transforms.insertNodes(editor, { text }, { at: point, select: true, voids: false }); }, - highlight (editor: ReactEditor) { + highlight(editor: ReactEditor) { const selection = editor.selection; - if (!selection) return; + if(!selection) return; const [start, end] = Range.edges(selection); - if (isEqual(start, end)) return; + if(isEqual(start, end)) return; const marks = CustomEditor.getAllMarks(editor); marks.forEach((mark) => { - if (mark[EditorMarkFormat.BgColor]) { + if(mark[EditorMarkFormat.BgColor]) { CustomEditor.removeMark(editor, EditorMarkFormat.BgColor); } else { CustomEditor.addMark(editor, { diff --git a/src/application/slate-yjs/plugins/withYjs.ts b/src/application/slate-yjs/plugins/withYjs.ts index 4005c0c4..1b0a1b4b 100644 --- a/src/application/slate-yjs/plugins/withYjs.ts +++ b/src/application/slate-yjs/plugins/withYjs.ts @@ -102,23 +102,23 @@ export function withYjs( const initializeDocumentContent = () => { const content = yDocToSlateContent(doc); - if (!content) { + if(!content) { return; } const selection = e.selection; - if (readSummary) { + if(readSummary) { e.children = content.children.slice(0, 10); } else { e.children = content.children; } - if (selection && !ReactEditor.hasRange(editor, selection)) { + if(selection && !ReactEditor.hasRange(editor, selection)) { try { Transforms.select(e, Editor.start(editor, [0])); - } catch (e) { + } catch(e) { console.error(e); editor.deselect(); } @@ -130,7 +130,7 @@ export function withYjs( }; const applyIntercept = (op: Operation) => { - if (YjsEditor.connected(e) && !e.interceptLocalChange) { + if(YjsEditor.connected(e) && !e.interceptLocalChange) { YjsEditor.storeLocalChange(e, op); } @@ -145,7 +145,7 @@ export function withYjs( e.interceptLocalChange = true; // Initialize or update the document content to ensure it is in the correct state before applying remote events - if (transaction.origin === CollabOrigin.Remote) { + if(transaction.origin === CollabOrigin.Remote) { initializeDocumentContent(); } else { @@ -154,8 +154,8 @@ export function withYjs( Editor.withoutNormalizing(e, () => { translateYEvents(e, events); }); - if (selection) { - if (!ReactEditor.hasRange(editor, selection)) { + if(selection) { + if(!ReactEditor.hasRange(editor, selection)) { editor.deselect(); } else { e.select(selection); @@ -169,13 +169,13 @@ export function withYjs( }; const handleYEvents = (events: Array, transaction: Transaction) => { - if (transaction.origin === CollabOrigin.Local) return; + if(transaction.origin === CollabOrigin.Local) return; YjsEditor.applyRemoteEvents(e, events, transaction); }; e.connect = () => { - if (YjsEditor.connected(e)) { + if(YjsEditor.connected(e)) { throw new Error('Already connected'); } @@ -186,7 +186,7 @@ export function withYjs( }; e.disconnect = () => { - if (!YjsEditor.connected(e)) { + if(!YjsEditor.connected(e)) { throw new Error('Not connected'); } @@ -215,7 +215,7 @@ export function withYjs( e.apply = applyIntercept; e.onChange = () => { - if (YjsEditor.connected(e)) { + if(YjsEditor.connected(e)) { YjsEditor.flushLocalChanges(e); } diff --git a/src/application/slate-yjs/utils/convert.ts b/src/application/slate-yjs/utils/convert.ts index df8a7785..f53a9428 100644 --- a/src/application/slate-yjs/utils/convert.ts +++ b/src/application/slate-yjs/utils/convert.ts @@ -24,7 +24,7 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un const blocks = getBlocks(sharedRoot); const block = blocks.get(id)?.toJSON() as BlockJson; - if (!block) { + if(!block) { console.error('Block not found', id); return; } @@ -37,15 +37,15 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un const slateNode = blockToSlateNode(block); - if (slateNode.type === BlockType.TableBlock) { + if(slateNode.type === BlockType.TableBlock) { slateNode.children = sortTableCells(children as TableCellNode[]); - } else if (slateNode.type === BlockType.TableCell) { + } else if(slateNode.type === BlockType.TableCell) { slateNode.children = children.slice(0, 1); } else { slateNode.children = children; } - if (slateNode.type === BlockType.Page) { + if(slateNode.type === BlockType.Page) { return slateNode; } @@ -55,9 +55,9 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un const yText = textId ? textMap.get(textId) : undefined; - if (!yText) { + if(!yText) { - if (children.length === 0) { + if(children.length === 0) { children.push({ text: '', }); @@ -65,10 +65,10 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un // Compatible data // The old version of delta data is fully covered through the data field - if (slateNode.data) { + if(slateNode.data) { const data = slateNode.data as BlockData; - if (YjsEditorKey.delta in data) { + if(YjsEditorKey.delta in data) { textId = block.id; delta = data.delta; } else { @@ -82,7 +82,7 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un try { const slateDelta = delta.flatMap(deltaInsertToSlateNode); - if (slateDelta.length === 0) { + if(slateDelta.length === 0) { slateDelta.push({ text: '', }); @@ -96,7 +96,7 @@ export function traverseBlock(id: string, sharedRoot: YSharedRoot): Element | un children.unshift(textNode); return slateNode; - } catch (e) { + } catch(e) { return; } } @@ -107,14 +107,14 @@ export function yDataToSlateContent(sharedRoot: YSharedRoot): Element | undefine const blocks = getBlocks(sharedRoot); const root = blocks.get(rootId); - if (!root) return; + if(!root) return; const result = traverseBlock(rootId, sharedRoot); - if (!result) return; + if(!result) return; return result; - } catch (e) { + } catch(e) { return; } } @@ -122,7 +122,7 @@ export function yDataToSlateContent(sharedRoot: YSharedRoot): Element | undefine export function yDocToSlateContent(doc: YDoc): Element | undefined { const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; - if (!sharedRoot || sharedRoot.size === 0) return; + if(!sharedRoot || sharedRoot.size === 0) return; return yDataToSlateContent(sharedRoot); } @@ -132,7 +132,7 @@ export function blockToSlateNode(block: BlockJson): Element { try { blockData = data ? JSON.parse(data) : {}; - } catch (e) { + } catch(e) { // do nothing } @@ -152,7 +152,7 @@ export interface YDelta { export function deltaInsertToSlateNode({ attributes, insert }: YDelta): Element | Text | Element[] { - if (attributes) { + if(attributes) { dealWithEmptyAttribute(attributes); } @@ -164,8 +164,8 @@ export function deltaInsertToSlateNode({ attributes, insert }: YDelta): Element // eslint-disable-next-line function dealWithEmptyAttribute(attributes: Record) { - for (const key in attributes) { - if (!attributes[key]) { + for(const key in attributes) { + if(!attributes[key]) { delete attributes[key]; } } @@ -197,19 +197,22 @@ export function slateContentInsertToYData( data: node.data || {}, }); - const [textNode, ...children] = (node.children[0] as Element).textId ? [node.children[0] as Element, ...node.children.slice(1)] : [null, ...node.children]; + const [textNode, ...children] = (node.children[0] as Element).type === 'text' ? [node.children[0] as Element, ...node.children.slice(1)] : [null, ...node.children]; - if (textNode) { + if(textNode) { const text = getText(block.get(YjsEditorKey.block_external_id), sharedRoot); - const ops = (textNode.children as Text[]).map(slateNodeToDeltaInsert); - text.applyDelta(ops); + if(text) { + const ops = (textNode.children as Text[]).map(slateNodeToDeltaInsert); + + text.applyDelta(ops); + } } updateBlockParent(sharedRoot, block, parent, index); children.forEach((child, i) => { - if (Element.isElement(child)) { + if(Element.isElement(child)) { processNode(child, block.get(YjsEditorKey.block_id), i); } }); diff --git a/src/application/slate-yjs/utils/editor.ts b/src/application/slate-yjs/utils/editor.ts index 8a303129..497f32aa 100644 --- a/src/application/slate-yjs/utils/editor.ts +++ b/src/application/slate-yjs/utils/editor.ts @@ -51,10 +51,10 @@ import { updateBlockParent, } from '@/application/slate-yjs/utils/yjs'; -export function ensureBlockText (editor: YjsEditor) { +export function ensureBlockText(editor: YjsEditor) { const { selection } = editor; - if (!selection) { + if(!selection) { return; } @@ -72,17 +72,17 @@ export function ensureBlockText (editor: YjsEditor) { const startBlockType = startNode.type as BlockType; const endBlockType = endNode.type as BlockType; - if (!startBlockId || !endBlockId) { + if(!startBlockId || !endBlockId) { return; } const compatibleBlocks: string[] = []; - if (!isEmbedBlockTypes(startBlockType)) { + if(!isEmbedBlockTypes(startBlockType)) { compatibleBlocks.push(startBlockId); } - if (!isEmbedBlockTypes(endBlockType)) { + if(!isEmbedBlockTypes(endBlockType)) { compatibleBlocks.push(endBlockId); } @@ -90,7 +90,7 @@ export function ensureBlockText (editor: YjsEditor) { const block = getBlock(blockId, sharedRoot); const textId = block.get(YjsEditorKey.block_external_id); - if (!textId || !getText(textId, sharedRoot)) { + if(!textId || !getText(textId, sharedRoot)) { const data = dataStringTOJson(block.get(YjsEditorKey.block_data)) as BlockData; compatibleDataDeltaToYText(sharedRoot, data.delta || [], blockId); @@ -98,13 +98,13 @@ export function ensureBlockText (editor: YjsEditor) { }); } -export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, at: BaseRange) { +export function handleCollapsedBreakWithTxn(editor: YjsEditor, sharedRoot: YSharedRoot, at: BaseRange) { const { startBlock, startOffset } = getBreakInfo(editor, sharedRoot, at); const [blockNode, path] = startBlock; const blockId = blockNode.blockId as string; const block = getBlock(blockId, sharedRoot); - if (!block) { + if(!block) { throw new Error('Block not found'); } @@ -112,7 +112,7 @@ export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSha const textId = block.get(YjsEditorKey.block_external_id); let yText = getText(textId, sharedRoot); - if (!yText && !isEmbedBlockTypes(blockType)) { + if(!yText && !isEmbedBlockTypes(blockType)) { const data = blockNode.data as BlockData; const delta = new Delta(data.delta || []); @@ -120,14 +120,14 @@ export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSha } - if (yText.length === 0) { + if(yText.length === 0) { const point = Editor.start(editor, at); const parent = getParent(blockId, sharedRoot); const parentType = parent?.get(YjsEditorKey.block_type); const index = getBlockIndex(blockId, sharedRoot); - if (SOFT_BREAK_TYPES.includes(blockType) && parent) { + if(SOFT_BREAK_TYPES.includes(blockType) && parent) { addBlock(editor, { ty: BlockType.Paragraph, data: {}, @@ -136,16 +136,16 @@ export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSha return; } - if (blockType !== BlockType.Paragraph && parentType && [BlockType.QuoteBlock, BlockType.CalloutBlock].includes(parentType)) { + if(blockType !== BlockType.Paragraph && parentType && [BlockType.QuoteBlock, BlockType.CalloutBlock].includes(parentType)) { handleNonParagraphBlockBackspaceAndEnterWithTxn(editor, sharedRoot, block, point); return; } - if (path.length > 1 && handleLiftBlockOnBackspaceAndEnterWithTxn(editor, sharedRoot, block, point)) { + if(path.length > 1 && handleLiftBlockOnBackspaceAndEnterWithTxn(editor, sharedRoot, block, point)) { return; } - if (blockType !== BlockType.Paragraph) { + if(blockType !== BlockType.Paragraph) { handleNonParagraphBlockBackspaceAndEnterWithTxn(editor, sharedRoot, block, point); return; } @@ -156,7 +156,7 @@ export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSha executeOperations(sharedRoot, operations, 'insertBreak'); - if (select) { + if(select) { moveToNextLine(editor, block, at, blockId); } else { Transforms.select(editor, Editor.start(editor, at)); @@ -164,7 +164,7 @@ export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSha } -export function removeRangeWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, range: Range) { +export function removeRangeWithTxn(editor: YjsEditor, sharedRoot: YSharedRoot, range: Range) { const { startBlock, endBlock, middleBlocks, startRange, endRange } = getAffectedBlocks(editor, range); const operations: (() => void)[] = []; const isSameBlock = startBlock[0].blockId === endBlock[0].blockId; @@ -172,7 +172,7 @@ export function removeRangeWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, console.log('startBlock', { startBlock, startRange }); operations.push(() => { deleteSlateRangeInBlock(sharedRoot, editor, startBlock[0], startRange); - if (!isSameBlock) { + if(!isSameBlock) { deleteSlateRangeInBlock(sharedRoot, editor, endBlock[0], endRange); mergeBlocks(sharedRoot, endBlock[0], startBlock[0]); @@ -186,24 +186,24 @@ export function removeRangeWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, } -export function handleRangeBreak (editor: YjsEditor, sharedRoot: YSharedRoot, range: Range) { +export function handleRangeBreak(editor: YjsEditor, sharedRoot: YSharedRoot, range: Range) { removeRangeWithTxn(editor, sharedRoot, range); const selection = editor.selection; - if (!selection) return; + if(!selection) return; handleCollapsedBreakWithTxn(editor, sharedRoot, selection); } -function moveToNextLine (editor: Editor, block: YBlock, at: BaseRange, blockId: string) { +function moveToNextLine(editor: Editor, block: YBlock, at: BaseRange, blockId: string) { const { selection } = editor; - if (!selection) return; + if(!selection) return; const { anchor, focus } = selection; - if (!Node.has(editor, anchor.path) || !Node.has(editor, focus.path)) { + if(!Node.has(editor, anchor.path) || !Node.has(editor, focus.path)) { Transforms.select(editor, Editor.start(editor, at)); Transforms.move(editor, { distance: 1, unit: 'line' }); return; @@ -212,14 +212,14 @@ function moveToNextLine (editor: Editor, block: YBlock, at: BaseRange, blockId: const blockType = block.get(YjsEditorKey.block_type); const isToggleListBlock = blockType === BlockType.ToggleListBlock; - if (isToggleListBlock) { + if(isToggleListBlock) { const blockData = dataStringTOJson(block.get(YjsEditorKey.block_data)) as ToggleListBlockData; const isCollapsed = blockData.collapsed; - if (isCollapsed) { + if(isCollapsed) { const nextBlockPath = getNextSiblingBlockPath(editor, blockId); - if (nextBlockPath) { + if(nextBlockPath) { Transforms.select(editor, Editor.start(editor, nextBlockPath)); } else { Transforms.select(editor, Editor.start(editor, at)); @@ -232,12 +232,12 @@ function moveToNextLine (editor: Editor, block: YBlock, at: BaseRange, blockId: Transforms.move(editor, { distance: 1, unit: 'line' }); } -export function getNextSiblingBlockPath (editor: Editor, blockId: string) { +export function getNextSiblingBlockPath(editor: Editor, blockId: string) { const [blockSlateNode] = editor.nodes({ match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId === blockId, }); - if (!blockSlateNode) { + if(!blockSlateNode) { return; } @@ -246,27 +246,27 @@ export function getNextSiblingBlockPath (editor: Editor, blockId: string) { const index = blockPath[blockPath.length - 1]; const nextPath = [...blockParentPath, index + 1]; - if (!nextPath || !Node.has(editor, nextPath)) { + if(!nextPath || !Node.has(editor, nextPath)) { return; } return nextPath; } -export function getBlockTextRange (editor: YjsEditor, entry: NodeEntry) { +export function getBlockTextRange(editor: YjsEditor, entry: NodeEntry) { const [node, path] = entry; const textId = TEXT_BLOCK_TYPES.includes(node.type as BlockType) ? (node.children[0] as Element)?.textId : null; const start = Editor.start(editor, path); - if (!textId) { + if(!textId) { return [start, editor.end(path)]; } return [start, Editor.end(editor, [...path, 0])]; } -export function getAffectedBlocks (editor: YjsEditor, range: Range): { +export function getAffectedBlocks(editor: YjsEditor, range: Range): { startBlock: NodeEntry; endBlock: NodeEntry; middleBlocks: NodeEntry[]; @@ -292,15 +292,15 @@ export function getAffectedBlocks (editor: YjsEditor, range: Range): { const isSameBlock = startBlock[0].blockId === endBlock[0].blockId; - if (!isSameBlock) { - for (const entry of Editor.nodes(editor, { + if(!isSameBlock) { + for(const entry of Editor.nodes(editor, { at: range, match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined && n.blockId !== startBlock[0].blockId && n.blockId !== endBlock[0].blockId, })) { const [, path] = entry as NodeEntry; const isAncestorOfStart = Path.isAncestor(path, startBlock[1]); - if (!isAncestorOfStart) { + if(!isAncestorOfStart) { middleBlocks.push(entry as NodeEntry); } } @@ -318,11 +318,11 @@ export function getAffectedBlocks (editor: YjsEditor, range: Range): { return { startBlock, endBlock, middleBlocks, startRange, endRange }; } -function getTextIdFromSlateNode (node: Element) { +function getTextIdFromSlateNode(node: Element) { return node.textId ?? (node.children[0] as Element).textId; } -export function deleteSlateRangeInBlock (sharedRoot: YSharedRoot, editor: Editor, block: Element, range: BaseRange) { +export function deleteSlateRangeInBlock(sharedRoot: YSharedRoot, editor: Editor, block: Element, range: BaseRange) { const [start, end] = Editor.edges(editor, range); const relativeOffset = slatePointToRelativePosition(sharedRoot, editor, start); @@ -330,14 +330,14 @@ export function deleteSlateRangeInBlock (sharedRoot: YSharedRoot, editor: Editor const endRelativeOffset = slatePointToRelativePosition(sharedRoot, editor, end); const endPos = Y.createAbsolutePositionFromRelativePosition(endRelativeOffset.point, assertDocExists(sharedRoot)); - if (!startPos || !endPos) { + if(!startPos || !endPos) { throw new Error('Unable to create absolute position'); } deleteRangeInBlock(sharedRoot, block, startPos.index, endPos.index); } -export function deleteRangeInBlock ( +export function deleteRangeInBlock( sharedRoot: YSharedRoot, block: Element, start: number, @@ -345,14 +345,17 @@ export function deleteRangeInBlock ( ) { const textId = getTextIdFromSlateNode(block); - if (textId) { + if(textId) { const yText = getText(textId, sharedRoot); - yText.delete(start, end - start); + if(yText) { + yText.delete(start, end - start); + + } } } -export function mergeBlocks ( +export function mergeBlocks( sharedRoot: YSharedRoot, sourceBlock: Element, targetBlock: Element, @@ -360,7 +363,7 @@ export function mergeBlocks ( const sourceTextId = getTextIdFromSlateNode(sourceBlock); const targetTextId = getTextIdFromSlateNode(targetBlock); - if (!sourceTextId || !targetTextId) { + if(!sourceTextId || !targetTextId) { return; } @@ -370,7 +373,7 @@ export function mergeBlocks ( const sourceOps = sourceYText.toDelta() as Op[]; sourceOps.forEach((op) => { - if (op.insert && typeof op.insert === 'string') { + if(op.insert && typeof op.insert === 'string') { targetYText.insert(targetYText.length, op.insert, op.attributes); } }); @@ -382,14 +385,14 @@ export function mergeBlocks ( deleteBlock(sharedRoot, sourceBlock.blockId as string); } -export function getBreakInfo (editor: YjsEditor, sharedRoot: YSharedRoot, at: BaseRange) { +export function getBreakInfo(editor: YjsEditor, sharedRoot: YSharedRoot, at: BaseRange) { const startPoint = Editor.start(editor, at); const doc = assertDocExists(sharedRoot); const startRelativeRange = slatePointToRelativePosition(sharedRoot, editor, startPoint); const startTextId = getTextIdFromSlateNode(startRelativeRange.entry[0]); - if (!startTextId) { + if(!startTextId) { throw new Error('Text id not found'); } @@ -398,7 +401,7 @@ export function getBreakInfo (editor: YjsEditor, sharedRoot: YSharedRoot, at: Ba doc, ); - if (!startPos) { + if(!startPos) { throw new Error('Unable to create absolute position'); } @@ -407,13 +410,13 @@ export function getBreakInfo (editor: YjsEditor, sharedRoot: YSharedRoot, at: Ba match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, }) as NodeEntry; - if (!startBlock) { + if(!startBlock) { throw new Error('Block not found'); } let yText = getText(startTextId, sharedRoot); - if (!yText) { + if(!yText) { const data = startBlock[0].data as BlockData; const delta = new Delta(data.delta || []); @@ -425,13 +428,13 @@ export function getBreakInfo (editor: YjsEditor, sharedRoot: YSharedRoot, at: Ba return { startBlock, startOffset }; } -export function isAtBlockStart (editor: Editor, point: Point) { +export function isAtBlockStart(editor: Editor, point: Point) { const entry = Editor.above(editor, { at: point, match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.textId !== undefined, }); - if (!entry) return false; + if(!entry) return false; const [, path] = entry; const start = Editor.start(editor, path); @@ -439,13 +442,13 @@ export function isAtBlockStart (editor: Editor, point: Point) { return Point.equals(point, start); } -export function isAtBlockEnd (editor: Editor, point: Point) { +export function isAtBlockEnd(editor: Editor, point: Point) { const entry = Editor.above(editor, { at: point, match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.textId !== undefined, }); - if (!entry) return false; + if(!entry) return false; const [, path] = entry; const end = Editor.end(editor, path); @@ -453,29 +456,29 @@ export function isAtBlockEnd (editor: Editor, point: Point) { return Point.equals(point, end); } -export function getSharedRoot (editor: YjsEditor) { - if (!editor.sharedRoot || !editor.sharedRoot.doc) { +export function getSharedRoot(editor: YjsEditor) { + if(!editor.sharedRoot || !editor.sharedRoot.doc) { throw new Error('Shared root not found'); } return editor.sharedRoot; } -export function getSelectionOrThrow (editor: YjsEditor, at?: BaseRange) { +export function getSelectionOrThrow(editor: YjsEditor, at?: BaseRange) { const newAt = at || editor.selection; - if (!newAt) { + if(!newAt) { throw new Error('Selection not found'); } return newAt; } -export function getBlockEntry (editor: YjsEditor, point?: Point) { +export function getBlockEntry(editor: YjsEditor, point?: Point) { const { selection } = editor; const at = point || (selection ? Editor.start(editor, selection) : null); - if (!at) { + if(!at) { throw new Error('Point not found'); } @@ -484,18 +487,18 @@ export function getBlockEntry (editor: YjsEditor, point?: Point) { match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, }); - if (!blockEntry) { + if(!blockEntry) { throw new Error('Block not found'); } return blockEntry as NodeEntry; } -export function handleNonParagraphBlockBackspaceAndEnterWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: BasePoint) { +export function handleNonParagraphBlockBackspaceAndEnterWithTxn(editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: BasePoint) { const data = dataStringTOJson(block.get(YjsEditorKey.block_data)); const blockType = block.get(YjsEditorKey.block_type); - if (blockType === BlockType.ToggleListBlock && (data as ToggleListBlockData).level) { + if(blockType === BlockType.ToggleListBlock && (data as ToggleListBlockData).level) { const [, path] = getBlockEntry(editor, point); Transforms.setNodes(editor, { @@ -515,7 +518,7 @@ export function handleNonParagraphBlockBackspaceAndEnterWithTxn (editor: YjsEdit executeOperations(sharedRoot, operations, 'turnToBlock'); } -export function handleLiftBlockOnBackspaceAndEnterWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: Point) { +export function handleLiftBlockOnBackspaceAndEnterWithTxn(editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: Point) { const operations: (() => void)[] = []; const parent = getBlock(block.get(YjsEditorKey.block_parent), sharedRoot); const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); @@ -525,18 +528,18 @@ export function handleLiftBlockOnBackspaceAndEnterWithTxn (editor: YjsEditor, sh const hasNextSibling = index < parentChildren.length - 1; const hasChildren = getChildrenArray(block.get(YjsEditorKey.block_children), sharedRoot).length > 0; - if (preventLiftNode(editor, block.get(YjsEditorKey.block_id))) { + if(preventLiftNode(editor, block.get(YjsEditorKey.block_id))) { return false; } - if (!hasChildren && !hasNextSibling) { + if(!hasChildren && !hasNextSibling) { let newPath: number[] | undefined; operations.push(() => { newPath = liftEditorNode(editor, sharedRoot, block, point); }); executeOperations(sharedRoot, operations, 'liftBlock'); - if (!newPath) return false; + if(!newPath) return false; const newPoint = { path: [...newPath, ...point.path.slice(path.length)], @@ -551,7 +554,7 @@ export function handleLiftBlockOnBackspaceAndEnterWithTxn (editor: YjsEditor, sh return false; } -export function handleMergeBlockBackwardWithTxn (editor: YjsEditor, node: Element, point: Point) { +export function handleMergeBlockBackwardWithTxn(editor: YjsEditor, node: Element, point: Point) { const operations: (() => void)[] = []; const sharedRoot = getSharedRoot(editor); @@ -564,14 +567,14 @@ export function handleMergeBlockBackwardWithTxn (editor: YjsEditor, node: Elemen voids: true, }); - if (!prevText) { + if(!prevText) { return; } const [, prevPath] = prevText as NodeEntry; const prevPoint = Editor.end(editor, prevPath); - if (!prevPoint) { + if(!prevPoint) { throw new Error('prevPoint not found'); } @@ -580,7 +583,7 @@ export function handleMergeBlockBackwardWithTxn (editor: YjsEditor, node: Elemen match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, }); - if (!target) { + if(!target) { throw new Error('target not found'); } @@ -591,7 +594,7 @@ export function handleMergeBlockBackwardWithTxn (editor: YjsEditor, node: Elemen operations.push(() => { mergeBlocks(sharedRoot, node, targetNode); }); - } catch (e) { + } catch(e) { // at the beginning of the document console.info('at the beginning of the document'); } @@ -599,14 +602,14 @@ export function handleMergeBlockBackwardWithTxn (editor: YjsEditor, node: Elemen executeOperations(sharedRoot, operations, 'deleteBlockBackward'); } -export function handleMergeBlockForwardWithTxn (editor: YjsEditor, node: Element, point: Point) { +export function handleMergeBlockForwardWithTxn(editor: YjsEditor, node: Element, point: Point) { const operations: (() => void)[] = []; const sharedRoot = getSharedRoot(editor); try { const nextPoint = Editor.after(editor, point); - if (!nextPoint) { + if(!nextPoint) { throw new Error('nextPoint not found'); } @@ -615,7 +618,7 @@ export function handleMergeBlockForwardWithTxn (editor: YjsEditor, node: Element match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, }); - if (!source) { + if(!source) { throw new Error('source not found'); } @@ -624,7 +627,7 @@ export function handleMergeBlockForwardWithTxn (editor: YjsEditor, node: Element operations.push(() => { mergeBlocks(sharedRoot, sourceNode, node); }); - } catch (e) { + } catch(e) { // at the end of the document console.info('at the end of the document'); } @@ -632,14 +635,14 @@ export function handleMergeBlockForwardWithTxn (editor: YjsEditor, node: Element executeOperations(sharedRoot, operations, 'deleteBlockForward'); } -export function preventIndentNode (editor: YjsEditor, blockId: string) { +export function preventIndentNode(editor: YjsEditor, blockId: string) { const sharedRoot = getSharedRoot(editor); const block = getBlock(blockId, sharedRoot); const parent = getBlock(block.get(YjsEditorKey.block_parent), sharedRoot); const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); const index = parentChildren.toArray().findIndex((id) => id === block.get(YjsEditorKey.block_id)); - if (index === 0) { + if(index === 0) { return true; } @@ -647,19 +650,19 @@ export function preventIndentNode (editor: YjsEditor, blockId: string) { const previousSiblingId = parentChildren.get(index - 1); const previousSibling = getBlock(previousSiblingId, sharedRoot); - if (!previousSibling) { + if(!previousSibling) { return true; } // Check if the parent block is a container block - if (!CONTAINER_BLOCK_TYPES.includes(previousSibling.get(YjsEditorKey.block_type))) { + if(!CONTAINER_BLOCK_TYPES.includes(previousSibling.get(YjsEditorKey.block_type))) { return true; } return false; } -export function findSlateEntryByBlockId (editor: Editor, blockId: string) { +export function findSlateEntryByBlockId(editor: Editor, blockId: string) { const [node] = Editor.nodes(editor, { match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId === blockId, at: [], @@ -670,14 +673,14 @@ export function findSlateEntryByBlockId (editor: Editor, blockId: string) { return node as NodeEntry; } -export function beforePasted (editor: ReactEditor) { +export function beforePasted(editor: ReactEditor) { const { selection } = editor; - if (!selection) { + if(!selection) { return false; } - if (Range.isExpanded(selection)) { + if(Range.isExpanded(selection)) { Transforms.collapse(editor, { edge: 'start' }); editor.delete({ @@ -688,10 +691,10 @@ export function beforePasted (editor: ReactEditor) { return true; } -export function getSelectedPaths (editor: ReactEditor) { +export function getSelectedPaths(editor: ReactEditor) { const { selection } = editor; - if (!selection) { + if(!selection) { return null; } @@ -718,13 +721,13 @@ export function getSelectedPaths (editor: ReactEditor) { Element.isElement(n) && n.blockId !== undefined; - if (!isBlockElement) return false; + if(!isBlockElement) return false; // Get the range of the block element const nodeRange = Editor.range(editor, path); const [anchor, focus] = editor.edges(nodeRange); - if (n.blockId === startBlockId || n.blockId === endBlockId) { + if(n.blockId === startBlockId || n.blockId === endBlockId) { return true; } @@ -734,13 +737,13 @@ export function getSelectedPaths (editor: ReactEditor) { }; // Check if the block element is fully within the selection - if (isIntersecting(anchor, focus)) { + if(isIntersecting(anchor, focus)) { return true; } else { // Check if the block element contains a text node that is within the selection const textNode = (n.children[0] as Element)?.textId !== undefined ? n.children[0] : null; - if (textNode) { + if(textNode) { const [anchor, focus] = editor.edges([...path, 0]); return isIntersecting(anchor, focus); @@ -756,7 +759,7 @@ export function getSelectedPaths (editor: ReactEditor) { return blockEntries.map(([, path]) => path); } -export function filterValidNodes (editor: ReactEditor, selectedPaths: Path[]): [ +export function filterValidNodes(editor: ReactEditor, selectedPaths: Path[]): [ Element, Path, ][] { @@ -781,7 +784,7 @@ export function filterValidNodes (editor: ReactEditor, selectedPaths: Path[]): [ }); } -export function sortNodesByDepth (editor: Editor, selectedPaths: Path[]) { +export function sortNodesByDepth(editor: Editor, selectedPaths: Path[]) { const pathsWithDepth = selectedPaths.map(path => ({ path, depth: path.length, @@ -790,7 +793,7 @@ export function sortNodesByDepth (editor: Editor, selectedPaths: Path[]) { return pathsWithDepth.sort((a, b) => { // use depth to sort - if (b.depth !== a.depth) { + if(b.depth !== a.depth) { return b.depth - a.depth; } @@ -799,19 +802,21 @@ export function sortNodesByDepth (editor: Editor, selectedPaths: Path[]) { }); } -export function preventLiftNode (editor: YjsEditor, blockId: string) { +export function preventLiftNode(editor: YjsEditor, blockId: string) { const [, path] = findSlateEntryByBlockId(editor, blockId); const level = path.length; - if (level < 2) { + const parent = getParent(blockId, getSharedRoot(editor)); + + if(level < 2 || (parent && [BlockType.ColumnsBlock, BlockType.ColumnBlock].includes(parent.get(YjsEditorKey.block_type)))) { return true; } return false; } -export function liftEditorNode (editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: Point) { - if (preventLiftNode(editor, block.get(YjsEditorKey.block_id))) { +export function liftEditorNode(editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: Point) { + if(preventLiftNode(editor, block.get(YjsEditorKey.block_id))) { return; } @@ -831,7 +836,7 @@ export function liftEditorNode (editor: YjsEditor, sharedRoot: YSharedRoot, bloc } -export function isEntireDocumentSelected (editor: YjsEditor) { +export function isEntireDocumentSelected(editor: YjsEditor) { const selection = getSelectionOrThrow(editor); const [start, end] = Editor.edges(editor, selection); const startEdge = Editor.start(editor, []); @@ -840,7 +845,7 @@ export function isEntireDocumentSelected (editor: YjsEditor) { return Point.equals(start, startEdge) && Point.equals(end, endEdge); } -export function handleDeleteEntireDocumentWithTxn (editor: YjsEditor) { +export function handleDeleteEntireDocumentWithTxn(editor: YjsEditor) { const sharedRoot = getSharedRoot(editor); const operations = [() => { editor.deselect(); @@ -850,7 +855,7 @@ export function handleDeleteEntireDocumentWithTxn (editor: YjsEditor) { const blockId = appendEmptyParagraph(sharedRoot); blocks.forEach((_, k) => { - if (k !== pageId && k !== blockId) { + if(k !== pageId && k !== blockId) { deleteBlock(sharedRoot, k); } }); @@ -861,23 +866,23 @@ export function handleDeleteEntireDocumentWithTxn (editor: YjsEditor) { Transforms.select(editor, Editor.start(editor, [0])); } -export function getNodeAtPath (children: Descendant[], path: Path): Descendant | null { +export function getNodeAtPath(children: Descendant[], path: Path): Descendant | null { let currentNode: Descendant | null = null; let currentChildren = children; - for (let i = 0; i < path.length; i++) { + for(let i = 0; i < path.length; i++) { const index = path[i]; - if (index >= currentChildren.length) { + if(index >= currentChildren.length) { return null; } currentNode = currentChildren[index]; - if (i === path.length - 1) { + if(i === path.length - 1) { return currentNode; } - if (!Element.isElement(currentNode) || !currentNode.children) { + if(!Element.isElement(currentNode) || !currentNode.children) { return null; } @@ -887,16 +892,16 @@ export function getNodeAtPath (children: Descendant[], path: Path): Descendant | return currentNode; } -export function getSelectionTexts (editor: ReactEditor) { +export function getSelectionTexts(editor: ReactEditor) { const selection = editor.selection; - if (!selection) return []; + if(!selection) return []; const texts: Text[] = []; const isExpanded = Range.isExpanded(selection); - if (isExpanded) { + if(isExpanded) { const start = Range.start(selection); const end = Range.end(selection); @@ -912,7 +917,7 @@ export function getSelectionTexts (editor: ReactEditor) { ).forEach((match) => { const node = match[0] as Element; - if (Text.isText(node)) { + if(Text.isText(node)) { texts.push(node); } }); @@ -921,14 +926,14 @@ export function getSelectionTexts (editor: ReactEditor) { return texts; } -export function getOffsetPointFromSlateRange (editor: YjsEditor, point: BasePoint): { offset: number; textId: string } { +export function getOffsetPointFromSlateRange(editor: YjsEditor, point: BasePoint): { offset: number; textId: string } { const [node] = editor.nodes({ at: point, match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.textId !== undefined, }); - if (!node) { + if(!node) { throw new Error('Node not found'); } @@ -940,12 +945,12 @@ export function getOffsetPointFromSlateRange (editor: YjsEditor, point: BasePoin }; } -export function getSlatePointFromOffset (editor: YjsEditor, range: { offset: number; textId: string }): BasePoint { +export function getSlatePointFromOffset(editor: YjsEditor, range: { offset: number; textId: string }): BasePoint { const [node] = editor.nodes({ match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.textId === range.textId, }); - if (!node) { + if(!node) { throw new Error('Node not found'); } @@ -956,7 +961,7 @@ export function getSlatePointFromOffset (editor: YjsEditor, range: { offset: num return start; } -export function addBlock (editor: YjsEditor, { +export function addBlock(editor: YjsEditor, { ty, data, }: { diff --git a/src/application/slate-yjs/utils/yjs.ts b/src/application/slate-yjs/utils/yjs.ts index 4b6df993..d9949599 100644 --- a/src/application/slate-yjs/utils/yjs.ts +++ b/src/application/slate-yjs/utils/yjs.ts @@ -1,3 +1,9 @@ +import { + CONTAINER_BLOCK_TYPES, + isEmbedBlockTypes, + LIST_BLOCK_TYPES, + TOGGLE_BLOCK_TYPES, +} from '@/application/slate-yjs/command/const'; import { BlockData, BlockType, @@ -12,14 +18,8 @@ import { YTextMap, } from '@/application/types'; import { nanoid } from 'nanoid'; -import * as Y from 'yjs'; -import { - CONTAINER_BLOCK_TYPES, - isEmbedBlockTypes, - LIST_BLOCK_TYPES, - TOGGLE_BLOCK_TYPES, -} from '@/application/slate-yjs/command/const'; import Delta, { Op } from 'quill-delta'; +import * as Y from 'yjs'; export function getTextMap(sharedRoot: YSharedRoot) { const document = sharedRoot.get(YjsEditorKey.document); @@ -89,7 +89,7 @@ export function createBlock(sharedRoot: YSharedRoot, { childrenMap.set(id, new Y.Array()); - if (!isEmbedBlockTypes(ty)) { + if(!isEmbedBlockTypes(ty)) { block.set(YjsEditorKey.block_external_id, id); block.set(YjsEditorKey.block_external_type, 'text'); const textMap = meta.get(YjsEditorKey.text_map) as YTextMap; @@ -103,7 +103,7 @@ export function createBlock(sharedRoot: YSharedRoot, { export function assertDocExists(sharedRoot: YSharedRoot): YDoc { const doc = sharedRoot.doc; - if (!doc) { + if(!doc) { throw new Error('Document not found'); } @@ -125,7 +125,7 @@ export function updateBlockParent(sharedRoot: YSharedRoot, block: YBlock, parent block.set(YjsEditorKey.block_parent, parent.get(YjsEditorKey.block_id)); const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); - if (index >= parentChildren.length) { + if(index >= parentChildren.length) { parentChildren.push([block.get(YjsEditorKey.block_id)]); return; } @@ -136,7 +136,7 @@ export function updateBlockParent(sharedRoot: YSharedRoot, block: YBlock, parent export function getPageId(sharedRoot: YSharedRoot) { const document = getDocument(sharedRoot); - if (!document) { + if(!document) { throw new Error('Document not found'); } @@ -224,7 +224,7 @@ export function compatibleDataDeltaToYText(sharedRoot: YSharedRoot, ops: Op[], b export function deleteBlock(sharedRoot: YSharedRoot, blockId: string) { const block = getBlock(blockId, sharedRoot); - if (!block) return; + if(!block) return; const document = getDocument(sharedRoot); const blocks = document.get(YjsEditorKey.blocks) as YBlocks; @@ -247,15 +247,33 @@ export function deleteBlock(sharedRoot: YSharedRoot, blockId: string) { const parent = getBlock(parentId, sharedRoot); - if (!parent) return; + if(!parent) return; const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); + const afterDeletedLength = parentChildren.length - 1; + const parentType = parent.get(YjsEditorKey.block_type); const index = parentChildren.toArray().findIndex((id) => id === blockId); - if (index !== -1) { + if(index !== -1) { parentChildren.delete(index, 1); } + // delete parent if it's empty column block + if(parentType === BlockType.ColumnBlock && afterDeletedLength === 0) { + deleteBlock(sharedRoot, parentId); + } + + // delete parent and move children to grandparent if it's one column block + if(parentType === BlockType.ColumnsBlock && afterDeletedLength === 1) { + const targetParent = getBlock(parent.get(YjsEditorKey.block_parent), sharedRoot); + const targetIndex = getBlockIndex(parentId, sharedRoot); + + const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); + const sourceBlock = getBlock(parentChildren.get(0), sharedRoot); + + transferChildren(sharedRoot, sourceBlock, targetParent, targetIndex); + deleteBlock(sharedRoot, parentId); + } } export function liftChildren(sharedRoot: YSharedRoot, sourceBlock: YBlock, targetBlock: YBlock) { @@ -263,11 +281,11 @@ export function liftChildren(sharedRoot: YSharedRoot, sourceBlock: YBlock, targe const targetParent = getBlock(targetBlock.get(YjsEditorKey.block_parent), sharedRoot); const targetChildrenArray = getChildrenArray(targetParent.get(YjsEditorKey.block_children), sharedRoot); - if (!sourceChildrenArray || !targetChildrenArray) return; + if(!sourceChildrenArray || !targetChildrenArray) return; const index = targetChildrenArray.toArray().findIndex((id) => id === targetBlock.get(YjsEditorKey.block_id)); const targetIndex = index !== -1 ? index + 1 : targetChildrenArray.length; - if (sourceChildrenArray.length > 0) { + if(sourceChildrenArray.length > 0) { deepCopyChildren(sharedRoot, sourceChildrenArray, targetChildrenArray, targetParent.get(YjsEditorKey.block_id), targetIndex); sourceChildrenArray.toArray().forEach((id) => { deleteBlock(sharedRoot, id); @@ -280,14 +298,14 @@ export function copyBlockText(sharedRoot: YSharedRoot, sourceBlock: YBlock, targ const sourceTextId = sourceBlock.get(YjsEditorKey.block_external_id); const targetTextId = targetBlock.get(YjsEditorKey.block_external_id); - if (!sourceTextId || !targetTextId) { + if(!sourceTextId || !targetTextId) { return; } const sourceText = getText(sourceTextId, sharedRoot); const targetText = getText(targetTextId, sharedRoot); - if (!sourceText || !targetText) { + if(!sourceText || !targetText) { return; } @@ -303,7 +321,7 @@ export function prepareBreakOperation(sharedRoot: YSharedRoot, block: YBlock, of const parentId = block.get(YjsEditorKey.block_parent); const parent = getBlock(parentId, sharedRoot); - if (!parent) { + if(!parent) { throw new Error('Parent block not found'); } @@ -314,11 +332,11 @@ export function prepareBreakOperation(sharedRoot: YSharedRoot, block: YBlock, of } export function getSplitBlockType(block: YBlock) { - switch (block.get(YjsEditorKey.block_type)) { + switch(block.get(YjsEditorKey.block_type)) { case BlockType.ToggleListBlock: { const data = dataStringTOJson(block.get(YjsEditorKey.block_data)) as ToggleListBlockData; - if (!data.collapsed) { + if(!data.collapsed) { return BlockType.Paragraph; } else { return block.get(YjsEditorKey.block_type); @@ -356,13 +374,13 @@ export function splitBlock(sharedRoot: YSharedRoot, block: YBlock, offset: numbe const blockType = block.get(YjsEditorKey.block_type); - if (TOGGLE_BLOCK_TYPES.includes(blockType)) { + if(TOGGLE_BLOCK_TYPES.includes(blockType)) { const data = dataStringTOJson(block.get(YjsEditorKey.block_data)) as ToggleListBlockData; - if (!data.collapsed) { + if(!data.collapsed) { const blockChildrenArray = getChildrenArray(block.get(YjsEditorKey.block_children), sharedRoot); - if (blockChildrenArray) { + if(blockChildrenArray) { updateBlockParent(sharedRoot, newBlock, block, 0); } @@ -380,7 +398,7 @@ export function splitBlock(sharedRoot: YSharedRoot, block: YBlock, offset: numbe export function ensureBlockHasChildren(sharedRoot: YSharedRoot, block: YBlock) { const childrenArray = getChildrenArray(block.get(YjsEditorKey.block_children), sharedRoot); - if (!childrenArray) { + if(!childrenArray) { const newArray = new Y.Array(); const childrenMap = getChildrenMap(sharedRoot); @@ -395,8 +413,8 @@ export function transferChildren(sharedRoot: YSharedRoot, sourceBlock: YBlock, t const targetChildrenArray = ensureBlockHasChildren(sharedRoot, targetBlock); - if (!sourceChildrenArray || !targetChildrenArray) return; - if (sourceChildrenArray.length > 0) { + if(!sourceChildrenArray || !targetChildrenArray) return; + if(sourceChildrenArray.length > 0) { deepCopyChildren(sharedRoot, sourceChildrenArray, targetChildrenArray, targetBlock.get(YjsEditorKey.block_id), index); sourceChildrenArray.toArray().forEach((id) => { deleteBlock(sharedRoot, id); @@ -412,13 +430,13 @@ export function turnToBlock(sharedRoot: YSharedRoot, source }); const newBlockId = newBlock.get(YjsEditorKey.block_id); - if (!isEmbedBlockTypes(type)) { + if(!isEmbedBlockTypes(type)) { copyBlockText(sharedRoot, sourceBlock, newBlock); } const parent = getBlock(sourceBlock.get(YjsEditorKey.block_parent), sharedRoot); - if (!parent) { + if(!parent) { return newBlockId; } @@ -427,7 +445,7 @@ export function turnToBlock(sharedRoot: YSharedRoot, source updateBlockParent(sharedRoot, newBlock, parent, index); - if (CONTAINER_BLOCK_TYPES.includes(type)) { + if(CONTAINER_BLOCK_TYPES.includes(type)) { transferChildren(sharedRoot, sourceBlock, newBlock); } else { liftChildren(sharedRoot, sourceBlock, newBlock); @@ -444,7 +462,7 @@ export function turnToBlock(sharedRoot: YSharedRoot, source export function dataStringTOJson(data: string): object { try { return JSON.parse(data); - } catch (e) { + } catch(e) { return {}; } } @@ -454,14 +472,14 @@ export function moveNode(sharedRoot: YSharedRoot, sourceBlock: YBlock, targetPar const copiedBlockId = deepCopyBlock(sharedRoot, sourceBlock); - if (!copiedBlockId) { + if(!copiedBlockId) { console.warn('Failed to copy block'); return; } const copiedBlock = getBlock(copiedBlockId, sharedRoot); - if (!copiedBlock) { + if(!copiedBlock) { console.warn('Copied block not found'); return; } @@ -485,13 +503,13 @@ export function deepCopyBlock(sharedRoot: YSharedRoot, sourceBlock: YBlock): str const sourceChildrenArray = getChildrenArray(sourceBlock.get(YjsEditorKey.block_children), sharedRoot); const targetChildrenArray = getChildrenArray(newBlock.get(YjsEditorKey.block_children), sharedRoot); - if (sourceChildrenArray && targetChildrenArray) { + if(sourceChildrenArray && targetChildrenArray) { deepCopyChildren(sharedRoot, sourceChildrenArray, targetChildrenArray, newBlock.get(YjsEditorKey.block_id)); } return newBlock.get(YjsEditorKey.block_id); - } catch (error) { + } catch(error) { console.error('Error in deepCopyBlock:', error); return null; } @@ -502,26 +520,26 @@ export function indentBlock(sharedRoot: YSharedRoot, block: YBlock) { const parentId = block.get(YjsEditorKey.block_parent); const parent = getBlock(parentId, sharedRoot); - if (!parent) { + if(!parent) { console.warn('Cannot indent block: parent not found'); return; } const parentChildrenArray = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); - if (!parentChildrenArray) { + if(!parentChildrenArray) { console.warn('Cannot indent block: parent children array not found'); return; } const blockIndex = parentChildrenArray.toArray().findIndex((id) => id === block.get(YjsEditorKey.block_id)); - if (blockIndex === -1) { + if(blockIndex === -1) { console.warn('Cannot indent block: block not found in parent\'s children'); return; } - if (blockIndex === 0) { + if(blockIndex === 0) { console.warn('Cannot indent block: block is the first child'); return; } @@ -529,14 +547,14 @@ export function indentBlock(sharedRoot: YSharedRoot, block: YBlock) { const previousSiblingId = parentChildrenArray.get(blockIndex - 1); const previousSibling = getBlock(previousSiblingId, sharedRoot); - if (!previousSibling) { + if(!previousSibling) { console.warn('Cannot indent block: previous sibling not found'); return; } const previousSiblingChildrenArray = getChildrenArray(previousSibling.get(YjsEditorKey.block_children), sharedRoot); - if (!previousSiblingChildrenArray) { + if(!previousSiblingChildrenArray) { console.warn('Cannot indent block: previous sibling children array not found'); return; } @@ -548,17 +566,17 @@ export function extendNextSiblingsToToggleHeading(sharedRoot: YSharedRoot, block const type = block.get(YjsEditorKey.block_type); const data = dataStringTOJson(block.get(YjsEditorKey.block_data)) as ToggleListBlockData; - if (type !== BlockType.ToggleListBlock || !data.level) return; + if(type !== BlockType.ToggleListBlock || !data.level) return; const nextSiblings = getNextSiblings(sharedRoot, block); - if (!nextSiblings || nextSiblings.length === 0) return; + if(!nextSiblings || nextSiblings.length === 0) return; // find the next sibling with the same or higher level const index = nextSiblings.findIndex((id) => { const block = getBlock(id, sharedRoot); const blockData = dataStringTOJson(block.get(YjsEditorKey.block_data)); - if ('level' in blockData && (blockData as { + if('level' in blockData && (blockData as { level: number }).level <= ((data as unknown as ToggleListBlockData).level as number)) { return true; @@ -580,19 +598,19 @@ export function extendNextSiblingsToToggleHeading(sharedRoot: YSharedRoot, block export function getPreviousSiblingBlock(sharedRoot: YSharedRoot, block: YBlock) { const parent = getBlock(block.get(YjsEditorKey.block_parent), sharedRoot); - if (!parent) return; + if(!parent) return; const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); const index = parentChildren.toArray().findIndex((id) => id === block.get(YjsEditorKey.block_id)); - if (index === 0) return null; + if(index === 0) return null; return parentChildren.get(index - 1); } export function getNextSiblings(sharedRoot: YSharedRoot, block: YBlock) { const parent = getBlock(block.get(YjsEditorKey.block_parent), sharedRoot); - if (!parent) return; + if(!parent) return; const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); const index = parentChildren.toArray().findIndex((id) => id === block.get(YjsEditorKey.block_id)); @@ -606,7 +624,7 @@ export function getSplitBlockOperations(sharedRoot: YSharedRoot, block: YBlock, } { const operations: (() => void)[] = []; - if (offset === 0) { + if(offset === 0) { operations.push(() => { const type = block.get(YjsEditorKey.block_type); const data = dataStringTOJson(block.get(YjsEditorKey.block_data)); @@ -640,7 +658,7 @@ export function deepCopyChildren(sharedRoot: YSharedRoot, sourceArray: Y.Array { const sourceChild = getBlock(childId, sharedRoot); - if (sourceChild) { + if(sourceChild) { const oldData = dataStringTOJson(sourceChild.get(YjsEditorKey.block_data)); const newChild = createBlock(sharedRoot, { ty: sourceChild.get(YjsEditorKey.block_type), @@ -650,16 +668,16 @@ export function deepCopyChildren(sharedRoot: YSharedRoot, sourceArray: Y.Array 0) { + if(sourceChildrenArray && sourceChildrenArray.length > 0) { const newChildrenArray = getChildrenArray(newChild.get(YjsEditorKey.block_children), sharedRoot); - if (newChildrenArray) { + if(newChildrenArray) { deepCopyChildren(sharedRoot, sourceChildrenArray, newChildrenArray, newChild.get(YjsEditorKey.block_id)); } } @@ -674,7 +692,7 @@ export function deepCopyChildren(sharedRoot: YSharedRoot, sourceArray: Y.Array id === parentId); - if (parentIndex === -1) { + if(parentIndex === -1) { console.warn('Cannot lift block: parent not found in grandparent\'s children'); return; } @@ -737,7 +755,7 @@ export function appendEmptyParagraph(sharedRoot: YSharedRoot): string { export function getParent(blockId: string, sharedRoot: YSharedRoot) { const block = getBlock(blockId, sharedRoot); - if (!block) { + if(!block) { return; } diff --git a/src/application/types.ts b/src/application/types.ts index 9e9e4921..ac162a8f 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -40,6 +40,8 @@ export enum BlockType { SimpleTableBlock = 'simple_table', SimpleTableRowBlock = 'simple_table_row', SimpleTableCellBlock = 'simple_table_cell', + ColumnsBlock = 'simple_columns', + ColumnBlock = 'simple_column', } export enum InlineBlockType { @@ -198,6 +200,10 @@ export interface SubpageNodeData extends BlockData { view_id: string; } +export interface ColumnNodeData extends BlockData { + width?: number; +} + export enum MentionType { PageRef = 'page', Date = 'date', diff --git a/src/assets/doc-forward.svg b/src/assets/doc-forward.svg new file mode 100644 index 00000000..62dc5a7d --- /dev/null +++ b/src/assets/doc-forward.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/_shared/outline/OutlineItemContent.tsx b/src/components/_shared/outline/OutlineItemContent.tsx index 1e9ae497..8f9c8703 100644 --- a/src/components/_shared/outline/OutlineItemContent.tsx +++ b/src/components/_shared/outline/OutlineItemContent.tsx @@ -1,4 +1,4 @@ -import { UIVariant, View, ViewLayout } from '@/application/types'; +import { UIVariant, View } from '@/application/types'; import SpaceIcon from '@/components/_shared/view-icon/SpaceIcon'; import PublishIcon from '@/components/_shared/view-icon/PublishIcon'; import { Tooltip } from '@mui/material'; @@ -6,7 +6,7 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import PageIcon from '@/components/_shared/view-icon/PageIcon'; -function OutlineItemContent ({ +function OutlineItemContent({ item, setIsExpanded, navigateToView, @@ -27,25 +27,24 @@ function OutlineItemContent ({ return (
{ - if (isSpace || (!item.is_published && variant === 'publish') || item.layout === ViewLayout.AIChat) { + onClick={async() => { + if(isSpace || (!item.is_published && variant === 'publish')) { setIsExpanded(prev => !prev); return; } try { await navigateToView?.(view_id); - } catch (e) { + } catch(e) { // do nothing } }} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ - cursor: item.layout === ViewLayout.AIChat ? 'not-allowed' : 'pointer', paddingLeft: variant === 'favorite' || variant === 'recent' ? '8px' : item.children?.length ? 0 : 1.125 * (level + 1) + 'em', }} - className={`flex flex-1 select-none items-center gap-1.5 overflow-hidden`} + className={`flex flex-1 select-none items-center pointer gap-1.5 overflow-hidden`} > {isSpace && extra ? void; +}) { + const service = useService(); + const workspaceId = useCurrentWorkspaceId(); + const currentUser = useCurrentUser(); + const isMobile = getPlatform().isMobile; + const [openMobilePrompt, setOpenMobilePrompt] = React.useState(isMobile); + + const { + refreshOutline, + updatePage, + } = useAppHandlers(); + + const { + selectionMode, + onOpenSelectionMode: handleOpenSelectionMode, + onCloseSelectionMode: handleCloseSelectionMode, + onOpenView, + openViewId, + onCloseView, + drawerOpen, + } = useAIChatContext(); + + const requestInstance = useMemo(() => { + if(!service || !workspaceId) return; + const axiosInstance = service.getAxiosInstance(); + + if(!axiosInstance) return; + + const request = new ChatRequest(workspaceId, chatId, axiosInstance); + + const { createViewWithContent } = request; + + request.updateViewName = async(view, name) => { + try { + await updatePage?.(view.view_id, { + name, + icon: view.icon || undefined, + }); + void refreshOutline?.(); + } catch(error) { + return Promise.reject(error); + } + }; + + request.insertContentToView = async(viewId, data) => { + onOpenView(viewId, data); + }; + + request.createViewWithContent = async(parentViewId, name, data) => { + try { + const res = await createViewWithContent.apply(request, [parentViewId, name, data]); + + await refreshOutline?.(); + onOpenView(res.view_id); + + return res; + } catch(error) { + return Promise.reject(error); + } + }; + + return request; + }, [onOpenView, service, workspaceId, chatId, updatePage, refreshOutline]); + + useEffect(() => { + if(onRendered) { + onRendered(); + } + }, [onRendered]); + + if(!requestInstance) return null; + + return ( +
+
+ +
+ + { + + {'📱 Mobile device detected'} + + +
{`Chat listings only. For full chat features:`}
+
    +
  • • Use desktop browser
  • +
  • • Download AppFlowy's mobile app
  • +
+
+ + + + +
} +
+ + ); +} + +export default AIChat; \ No newline at end of file diff --git a/src/components/ai-chat/AIChatDrawer.tsx b/src/components/ai-chat/AIChatDrawer.tsx new file mode 100644 index 00000000..960d2d42 --- /dev/null +++ b/src/components/ai-chat/AIChatDrawer.tsx @@ -0,0 +1,43 @@ +import DrawerContent from '@/components/ai-chat/DrawerContent'; +import DrawerHeader from '@/components/ai-chat/DrawerHeader'; +import Pinned from '@/components/ai-chat/Pinned'; +import Resizer from './Resizer'; +import { useAIChatContext } from '@/components/ai-chat/AIChatProvider'; +import React from 'react'; + +function AIChatDrawer() { + const { + drawerOpen, + openViewId, + drawerWidth, + onSetDrawerWidth, + } = useAIChatContext(); + + return ( +
+
+ {openViewId && +
+ + +
} + + +
+ + {!drawerOpen && openViewId && } +
+ ); +} + +export default AIChatDrawer; \ No newline at end of file diff --git a/src/components/ai-chat/AIChatProvider.tsx b/src/components/ai-chat/AIChatProvider.tsx new file mode 100644 index 00000000..091f3078 --- /dev/null +++ b/src/components/ai-chat/AIChatProvider.tsx @@ -0,0 +1,125 @@ +import AIChatDrawer from '@/components/ai-chat/AIChatDrawer'; +import { useAppViewId } from '@/components/app/app.hooks'; +import { EditorData } from '@appflowyinc/editor'; +import React, { useCallback, useEffect, useState } from 'react'; + +const DEFAULT_WIDTH = 600; + +export const AIChatContext = React.createContext<{ + chatId?: string; + selectionMode: boolean; + onOpenSelectionMode: () => void; + onCloseSelectionMode: () => void; + openViewId: string | null; + onOpenView: (viewId: string, insertData?: EditorData) => void; + onCloseView: () => void; + drawerWidth: number; + onSetDrawerWidth: (width: number) => void; + getInsertData: (viewId: string) => EditorData | undefined; + clearInsertData: (viewId: string) => void; + setDrawerOpen: (open: boolean) => void; + drawerOpen?: boolean; +} | undefined>(undefined); + +export function useAIChatContext() { + const context = React.useContext(AIChatContext); + + if(!context) { + throw new Error('useAIChatContext must be used within a AIChatProvider'); + } + + return context; +} + +export function AIChatProvider({ + children, +}: { + children: React.ReactNode; +}) { + const chatId = useAppViewId(); + const [drawerOpen, setDrawerOpen] = useState(false); + const [openViewId, setOpenViewId] = useState(null); + const [selectionMode, setSelectionMode] = useState(false); + const [drawerWidth, setDrawerWidth] = useState(DEFAULT_WIDTH); + const [insertData, setInsertData] = useState>(new Map()); + + useEffect(() => { + setSelectionMode(false); + setOpenViewId(null); + setDrawerOpen(false); + setDrawerWidth(DEFAULT_WIDTH); + setInsertData(new Map()); + }, [chatId]); + + useEffect(() => { + if(!openViewId) { + setDrawerOpen(false); + setInsertData(new Map()); + } + }, [openViewId]); + + const handleOpenSelectionMode = useCallback(() => { + setSelectionMode(true); + }, []); + + const handleCloseSelectionMode = useCallback(() => { + setSelectionMode(false); + }, []); + + const handleOpenView = useCallback((viewId: string, insertData?: EditorData) => { + setDrawerOpen(true); + setOpenViewId(viewId); + + // ensure that the insert data is updated before the view id has been updated + requestAnimationFrame(() => { + if(insertData) { + setInsertData(() => { + const newMap = new Map(); + + newMap.set(viewId, insertData); + return newMap; + }); + } + }); + + }, []); + + const handleCloseView = useCallback(() => { + setOpenViewId(null); + }, []); + + const getInsertData = useCallback((viewId: string) => { + return insertData.get(viewId); + }, [insertData]); + + const clearInsertData = useCallback((viewId: string) => { + setInsertData((prev) => { + prev.delete(viewId); + return prev; + }); + }, []); + + return ( + + {children} + + + ); +} + diff --git a/src/components/ai-chat/DrawerContent.tsx b/src/components/ai-chat/DrawerContent.tsx new file mode 100644 index 00000000..b3e41bcf --- /dev/null +++ b/src/components/ai-chat/DrawerContent.tsx @@ -0,0 +1,199 @@ +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 { Document } from '@/components/document'; +import RecordNotFound from '@/components/error/RecordNotFound'; +import { debounce } from 'lodash-es'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import smoothScrollIntoViewIfNeeded from 'smooth-scroll-into-view-if-needed'; + +function DrawerContent({ + openViewId, +}: { + openViewId: string; +}) { + const { + toView, + loadViewMeta, + createRowDoc, + loadView, + updatePage, + addPage, + deletePage, + openPageModal, + loadViews, + setWordCount, + uploadFile, + } = useAppHandlers(); + const { + getInsertData, + clearInsertData, + drawerOpen, + } = useAIChatContext(); + + const [doc, setDoc] = React.useState<{ + id: string; + doc: YDoc; + } | undefined>(undefined); + + const initialScrolling = React.useRef(false); + const [notFound, setNotFound] = React.useState(false); + const [editor, setEditor] = useState(undefined); + + const onEditorConnected = useCallback((editor: YjsEditor) => { + setEditor(editor); + }, []); + + const loadPageDoc = useCallback(async(id: string) => { + + setNotFound(false); + setDoc(undefined); + try { + const doc = await loadView(id); + + setDoc({ doc, id }); + } catch(e) { + setNotFound(true); + console.error(e); + } + + }, [loadView]); + + const view = useAppView(openViewId); + + useEffect(() => { + if(openViewId) { + void loadPageDoc(openViewId); + } else { + setDoc(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [openViewId]); + + useEffect(() => { + const insertData = getInsertData(openViewId); + + if(!doc || !insertData || !drawerOpen || doc.id !== openViewId || editor === undefined) { + return; + } + + try { + editor.deselect(); + insertDataToDoc(doc.doc, insertData); + + clearInsertData(openViewId); + } catch(e) { + console.error(e); + } + + initialScrolling.current = true; + + }, [editor, clearInsertData, doc, getInsertData, openViewId, drawerOpen]); + + useEffect(() => { + const container = document.querySelector('.ai-chat-view'); + + const debounceStopScrolling = debounce(() => { + initialScrolling.current = false; + }, 500); + + const observer = new MutationObserver(() => { + if(!initialScrolling.current) { + return; + } + + const textBox = document.querySelector('.ai-chat-view [role="textbox"]'); + + if(textBox) { + void smoothScrollIntoViewIfNeeded(textBox, { + behavior: 'smooth', + block: 'end', + onScrollChange: () => { + debounceStopScrolling(); + }, + }); + + // focus to end of the text + const range = document.createRange(); + + range.selectNodeContents(textBox); + range.collapse(false); + const selection = window.getSelection(); + + if(selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + } + }); + + if(container) { + observer.observe(container, { + childList: true, + subtree: true, + }); + } + + return () => { + observer.disconnect(); + }; + }, []); + + const viewMeta: ViewMetaProps | null = useMemo(() => { + return view ? { + name: view.name, + icon: view.icon || undefined, + cover: view.extra?.cover || undefined, + layout: view.layout, + visibleViewIds: [], + viewId: view.view_id, + extra: view.extra, + } : null; + }, [view]); + + const handleUploadFile = useCallback((file: File) => { + if(view && uploadFile) { + return uploadFile(view.view_id, file); + } + + return Promise.reject(); + }, [uploadFile, view]); + + if(notFound) { + return ( + + ); + } + + return ( +
+ { + doc && viewMeta && ( + + ) + } + +
+ ); +} + +export default DrawerContent; \ No newline at end of file diff --git a/src/components/ai-chat/DrawerHeader.tsx b/src/components/ai-chat/DrawerHeader.tsx new file mode 100644 index 00000000..baf22213 --- /dev/null +++ b/src/components/ai-chat/DrawerHeader.tsx @@ -0,0 +1,64 @@ +import { useAIChatContext } from '@/components/ai-chat/AIChatProvider'; +import { useAppHandlers } from '@/components/app/app.hooks'; +import MoreActions from '@/components/app/header/MoreActions'; +import { IconButton, Tooltip } from '@mui/material'; +import React from 'react'; +import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as ExpandMoreIcon } from '@/assets/full_view.svg'; +import ShareButton from 'src/components/app/share/ShareButton'; + +function DrawerHeader() { + const { t } = useTranslation(); + const { + setDrawerOpen, + onCloseView, + openViewId, + } = useAIChatContext(); + + const { + toView, + } = useAppHandlers(); + + if(!openViewId) { + return null; + } + + return ( +
+
+ + { + setDrawerOpen(false); + }} + > + + + + + { + if(!openViewId) return; + await toView(openViewId); + onCloseView(); + }} + > + + + +
+
+ + +
+
+ ); +} + +export default DrawerHeader; \ No newline at end of file diff --git a/src/components/ai-chat/Pinned.tsx b/src/components/ai-chat/Pinned.tsx new file mode 100644 index 00000000..e866dbd7 --- /dev/null +++ b/src/components/ai-chat/Pinned.tsx @@ -0,0 +1,25 @@ +import { useAIChatContext } from '@/components/ai-chat/AIChatProvider'; +import { Button } from '@mui/material'; +import React from 'react'; +import { ReactComponent as EditIcon } from '@/assets/edit.svg'; + +function Pinned() { + const { + setDrawerOpen, + } = useAIChatContext(); + + return ( +
+ +
+ ); +} + +export default Pinned; \ No newline at end of file diff --git a/src/components/ai-chat/Resizer.tsx b/src/components/ai-chat/Resizer.tsx new file mode 100644 index 00000000..d837a514 --- /dev/null +++ b/src/components/ai-chat/Resizer.tsx @@ -0,0 +1,59 @@ +import React, { useState, useCallback, useEffect } from 'react'; + +const Resizer = ({ + minWidth = Math.min(400, window.innerWidth / 4), + maxWidth = Math.max(400, window.innerWidth / 2), + onResize, + drawerWidth, +}: { + drawerWidth: number; + minWidth?: number; + maxWidth?: number; + onResize?: (width: number) => void; +}) => { + const [isResizing, setIsResizing] = useState(false); + + const startResizing = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setIsResizing(true); + }, []); + + const stopResizing = useCallback(() => { + setIsResizing(false); + }, []); + + const resize = useCallback( + (mouseMoveEvent: MouseEvent) => { + if(isResizing) { + mouseMoveEvent.stopPropagation(); + mouseMoveEvent.preventDefault(); + const newWidth = window.innerWidth - mouseMoveEvent.clientX; + + if(newWidth >= minWidth && newWidth <= maxWidth) { + onResize?.(newWidth); + } + } + }, + [isResizing, minWidth, maxWidth, onResize], + ); + + useEffect(() => { + window.addEventListener('mousemove', resize); + window.addEventListener('mouseup', stopResizing); + return () => { + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResizing); + }; + }, [resize, stopResizing]); + + return ( +
0 ? startResizing : undefined} + /> + ); +}; + +export default Resizer; \ No newline at end of file diff --git a/src/components/ai-chat/index.ts b/src/components/ai-chat/index.ts new file mode 100644 index 00000000..27625e8d --- /dev/null +++ b/src/components/ai-chat/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const AIChat = lazy(() => import('src/components/ai-chat/AIChat')); diff --git a/src/components/ai-chat/utils.ts b/src/components/ai-chat/utils.ts new file mode 100644 index 00000000..83f56967 --- /dev/null +++ b/src/components/ai-chat/utils.ts @@ -0,0 +1,62 @@ +import { + createBlock, executeOperations, + getBlocks, + getChildrenArray, + getChildrenMap, + getDocument, getPageId, getText, getTextMap, updateBlockParent, +} from '@/application/slate-yjs/utils/yjs'; +import { + BlockType, + YDoc, + YjsEditorKey, + YSharedRoot, +} from '@/application/types'; +import { EditorData, EditorNode } from '@appflowyinc/editor'; + +export function insertDataToDoc(doc: YDoc, insertData: EditorData) { + const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + executeOperations(sharedRoot, [() => { + const document = getDocument(sharedRoot); + + if(!document) return; + + const blocks = getBlocks(sharedRoot); + const pageId = getPageId(sharedRoot); + const textMap = getTextMap(sharedRoot); + const childrenMap = getChildrenMap(sharedRoot); + + if(!blocks || !textMap || !childrenMap) return; + const traverse = (node: EditorNode, parentId: string) => { + const block = createBlock(sharedRoot, { + ty: node.type as unknown as BlockType, + data: node.data as object, + }); + + const parent = blocks.get(parentId); + + const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot); + const index = parentChildren.toArray().length; + + 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); + + if(node.children) { + node.children.forEach(child => { + traverse(child, block.get(YjsEditorKey.block_id)); + }); + } + }; + + insertData.forEach(node => { + traverse(node, pageId); + }); + }], 'AIChatInsertContent'); + +} + diff --git a/src/components/app/MainLayout.tsx b/src/components/app/MainLayout.tsx index a7e10aa0..abecaa5a 100644 --- a/src/components/app/MainLayout.tsx +++ b/src/components/app/MainLayout.tsx @@ -1,5 +1,6 @@ import { useOutlineDrawer } from '@/components/_shared/outline/outline.hooks'; import { AFScroller } from '@/components/_shared/scroller'; +import { useAIChatContext } from '@/components/ai-chat/AIChatProvider'; import { useViewErrorStatus } from '@/components/app/app.hooks'; import { AppHeader } from '@/components/app/header'; import Main from '@/components/app/Main'; @@ -17,17 +18,35 @@ function MainLayout() { setDrawerWidth, toggleOpenDrawer, } = useOutlineDrawer(); + const { + drawerOpen: chatViewDrawerOpen, + drawerWidth: openViewDrawerWidth, + } = useAIChatContext(); const { notFound, deleted } = useViewErrorStatus(); const main = useMemo(() => { - if (deleted) { - return ; + if(deleted) { + return ; } - return notFound ? :
; + return notFound ? :
; }, [deleted, notFound]); + const width = useMemo(() => { + let diff = 0; + + if(drawerOpened) { + diff = drawerWidth; + } + + if(chatViewDrawerOpen) { + diff += openViewDrawerWidth; + } + + return `calc(100% - ${diff}px)`; + }, [drawerOpened, drawerWidth, openViewDrawerWidth, chatViewDrawerOpen]); + return (
(undefined); const [notFound, setNotFound] = React.useState(false); - const loadPageDoc = useCallback(async (id: string) => { + const loadPageDoc = useCallback(async(id: string) => { setNotFound(false); setDoc(undefined); try { const doc = await loadView(id); - + setDoc({ doc, id }); - } catch (e) { + } catch(e) { setNotFound(true); console.error(e); } @@ -79,8 +79,7 @@ function ViewModal({ }, [loadView]); useEffect(() => { - if (open && viewId) { - + if(open && viewId) { void loadPageDoc(viewId); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -92,7 +91,7 @@ function ViewModal({ }, [onClose]); const view = useMemo(() => { - if (!outline || !viewId) return; + if(!outline || !viewId) return; return findView(outline, viewId); }, [outline, viewId]); @@ -108,7 +107,7 @@ function ViewModal({ } : null; }, [view]); const handleUploadFile = useCallback((file: File) => { - if (view && uploadFile) { + if(view && uploadFile) { return uploadFile(view.view_id, file); } @@ -121,7 +120,7 @@ function ViewModal({ }, []); const modalTitle = useMemo(() => { - if (!viewId) return null; + if(!viewId) return null; const space = findAncestors(outline || [], viewId)?.find(item => item.extra?.is_space); return ( @@ -130,12 +129,12 @@ function ViewModal({ { + onClick={async() => { await toView(viewId); handleClose(); }} > - + } color={'inherit'} className={'justify-start px-1.5'} - endIcon={} + endIcon={} >{space.name} )}
- + - +
@@ -186,7 +185,7 @@ function ViewModal({ const layout = view?.layout || ViewLayout.Document; const View = useMemo(() => { - switch (layout) { + switch(layout) { case ViewLayout.Document: return Document; case ViewLayout.Grid: @@ -199,7 +198,7 @@ function ViewModal({ }, [layout]) as React.FC; const viewDom = useMemo(() => { - if (!doc || !viewMeta || doc.id !== viewMeta.viewId) return null; + if(!doc || !viewMeta || doc.id !== viewMeta.viewId) return null; return {modalTitle} {notFound ? ( - + ) : (
{viewDom} diff --git a/src/components/app/app.hooks.tsx b/src/components/app/app.hooks.tsx index 946570e8..322b2cdc 100644 --- a/src/components/app/app.hooks.tsx +++ b/src/components/app/app.hooks.tsx @@ -1,13 +1,18 @@ import { invalidToken } from '@/application/session/token'; import { - AppendBreadcrumb, CreatePagePayload, - CreateRowDoc, CreateSpacePayload, + AppendBreadcrumb, + CreatePagePayload, + CreateRowDoc, + CreateSpacePayload, DatabaseRelations, LoadView, - LoadViewMeta, Subscription, TextCount, + LoadViewMeta, + Subscription, + TextCount, Types, UIVariant, - UpdatePagePayload, UpdateSpacePayload, + UpdatePagePayload, + UpdateSpacePayload, UserWorkspaceInfo, View, ViewLayout, @@ -16,6 +21,7 @@ import { YSharedRoot, } from '@/application/types'; import { findAncestors, findView, findViewByLayout } from '@/components/_shared/outline/utils'; +import { AIChatProvider } from '@/components/ai-chat/AIChatProvider'; import RequestAccess from '@/components/app/landing-pages/RequestAccess'; import { AFConfigContext, useService } from '@/components/main/app.hooks'; import { sortBy, uniqBy } from 'lodash-es'; @@ -64,6 +70,7 @@ export interface AppContextType { getSubscriptions?: () => Promise; publish?: (view: View, publishName?: string, visibleViewIds?: string[]) => Promise; unpublish?: (viewId: string) => Promise; + refreshOutline?: () => Promise; } const USER_NO_ACCESS_CODE = [1024, 1012]; @@ -87,6 +94,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const currentWorkspaceId = useMemo(() => params.workspaceId || userWorkspaceInfo?.selectedWorkspace.id, [params.workspaceId, userWorkspaceInfo?.selectedWorkspace.id]); const [workspaceDatabases, setWorkspaceDatabases] = useState(undefined); const [outline, setOutline] = useState(); + const outlineRef = useRef(); const [favoriteViews, setFavoriteViews] = useState(); const [recentViews, setRecentViews] = useState(); const [trashList, setTrashList] = React.useState(); @@ -168,7 +176,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }, []); const loadViewMeta = useCallback(async(viewId: string, callback?: (meta: View) => void) => { - const view = findView(outline || [], viewId); + const view = findView(outlineRef.current || [], viewId); const deletedView = trashList?.find((v) => v.view_id === viewId); if(deletedView) { @@ -190,7 +198,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { ...view, database_relations: workspaceDatabases, }; - }, [trashList, outline, workspaceDatabases]); + }, [trashList, workspaceDatabases]); const toView = useCallback(async(viewId: string, blockId?: string, keepSearch?: boolean) => { let url = `/app/${currentWorkspaceId}/${viewId}`; @@ -327,6 +335,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { } setOutline(res); + outlineRef.current = res; if(!force) return; const firstView = findViewByLayout(res, [ViewLayout.Document, ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar]); @@ -461,6 +470,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { await loadUserWorkspaceInfo(); localStorage.removeItem('last_view_id'); setOutline(undefined); + outlineRef.current = undefined; navigate(`/app/${workspaceId}`); }, [navigate, service, userWorkspaceInfo, loadUserWorkspaceInfo]); @@ -473,7 +483,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { try { const viewId = await service.addAppPage(currentWorkspaceId, parentViewId, payload); - void loadOutline(currentWorkspaceId, false); + await loadOutline(currentWorkspaceId, false); return viewId; } catch(e) { @@ -662,6 +672,11 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { await loadOutline(currentWorkspaceId, false); }, [currentWorkspaceId, loadOutline, service]); + const refreshOutline = useCallback(async() => { + if(!currentWorkspaceId) return; + await loadOutline(currentWorkspaceId, false); + }, [currentWorkspaceId, loadOutline]); + return { getSubscriptions, publish, unpublish, + refreshOutline, }} > - {requestAccessOpened ? : children} - { - { - setOpenModalViewId(undefined); - }} - /> - } + + {requestAccessOpened ? : children} + { + { + setOpenModalViewId(undefined); + }} + /> + } + ; }; @@ -844,6 +862,7 @@ export function useAppHandlers() { getSubscriptions: context.getSubscriptions, publish: context.publish, unpublish: context.unpublish, + refreshOutline: context.refreshOutline, }; } diff --git a/src/components/app/header/DocumentInfo.tsx b/src/components/app/header/DocumentInfo.tsx index dd2dd59f..d5775d54 100644 --- a/src/components/app/header/DocumentInfo.tsx +++ b/src/components/app/header/DocumentInfo.tsx @@ -1,9 +1,10 @@ +import { ViewLayout } from '@/application/types'; import { useAppView, useAppWordCount } from '@/components/app/app.hooks'; +import { getCharacters, getWords } from '@/utils/word'; import { Divider } from '@mui/material'; import dayjs from 'dayjs'; import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { getCharacters, getWords } from '@/utils/word'; function DocumentInfo({ viewId }: { viewId: string; @@ -50,14 +51,18 @@ function DocumentInfo({ viewId }: { <>
-
- {t('moreAction.wordCountLabel')}{words} -
-
- {t('moreAction.charCountLabel')}{chars} -
+ {view.layout === ViewLayout.Document && (<> +
+ {t('moreAction.wordCountLabel')}{words} +
+
+ {t('moreAction.charCountLabel')}{chars} +
+ + )} + {view.created_at &&
{t('moreAction.createdAtLabel')}{formatTime(view.created_at)}
} diff --git a/src/components/app/header/MoreActions.tsx b/src/components/app/header/MoreActions.tsx index d4efb119..64fffa07 100644 --- a/src/components/app/header/MoreActions.tsx +++ b/src/components/app/header/MoreActions.tsx @@ -1,18 +1,28 @@ -import DocumentInfo from '@/components/app/header/DocumentInfo'; -import MoreActionsContent from './MoreActionsContent'; -import React from 'react'; -import { Popover } from '@/components/_shared/popover'; -import { IconButton } from '@mui/material'; +import { ViewLayout } from '@/application/types'; import { ReactComponent as MoreIcon } from '@/assets/more.svg'; +import { Popover } from '@/components/_shared/popover'; +import { useAIChatContext } from '@/components/ai-chat/AIChatProvider'; +import { useAppView } from '@/components/app/app.hooks'; +import DocumentInfo from '@/components/app/header/DocumentInfo'; +import { Button, Divider, IconButton } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import MoreActionsContent from './MoreActionsContent'; +import { ReactComponent as DocForwardIcon } from '@/assets/doc-forward.svg'; -function MoreActions ({ +function MoreActions({ viewId, onDeleted, }: { viewId: string; onDeleted?: () => void; }) { + const { + selectionMode, + onOpenSelectionMode, + } = useAIChatContext(); + const view = useAppView(viewId); const [anchorEl, setAnchorEl] = React.useState(null); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -24,6 +34,12 @@ function MoreActions ({ const open = Boolean(anchorEl); + const { t } = useTranslation(); + + if(view?.layout === ViewLayout.AIChat && selectionMode) { + return null; + } + return ( <> @@ -54,6 +70,19 @@ function MoreActions ({ }, }} > + {view?.layout === ViewLayout.AIChat ? <> + + + : null} { handleClose(); diff --git a/src/components/app/header/MoreActionsContent.tsx b/src/components/app/header/MoreActionsContent.tsx index b93e5bd0..06c1f425 100644 --- a/src/components/app/header/MoreActionsContent.tsx +++ b/src/components/app/header/MoreActionsContent.tsx @@ -8,7 +8,7 @@ import { ReactComponent as DeleteIcon } from '@/assets/trash.svg'; import { ReactComponent as DuplicateIcon } from '@/assets/duplicate.svg'; import { ReactComponent as MoveToIcon } from '@/assets/move_to.svg'; -function MoreActionsContent ({ itemClicked, viewId, movePopoverOrigins, onDeleted }: { +function MoreActionsContent({ itemClicked, viewId, movePopoverOrigins, onDeleted }: { itemClicked?: () => void; onDeleted?: () => void; viewId: string; @@ -19,7 +19,7 @@ function MoreActionsContent ({ itemClicked, viewId, movePopoverOrigins, onDelete const [deleteModalOpen, setDeleteModalOpen] = useState(false); return ( -
+