mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-10 11:28:45 +00:00
Compare commits
132 Commits
v0.7.0-can
...
v0.8.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8e49ee3be | ||
|
|
1012807c65 | ||
|
|
1c7c27712e | ||
|
|
7f28c78d8c | ||
|
|
4bb874756d | ||
|
|
0882d47dc9 | ||
|
|
0c16eb1189 | ||
|
|
f2ac4c7eda | ||
|
|
0d531782ca | ||
|
|
47ff376195 | ||
|
|
33cc9d25a1 | ||
|
|
58ceeb9c5f | ||
|
|
3c00b69805 | ||
|
|
2678ca9330 | ||
|
|
6f488d963b | ||
|
|
8face25bdf | ||
|
|
0e32803247 | ||
|
|
3282344d4a | ||
|
|
78d23d86f5 | ||
|
|
3a0797955c | ||
|
|
9449e66396 | ||
|
|
b6200ab56d | ||
|
|
ff23561e21 | ||
|
|
e718428d50 | ||
|
|
ea34d66e14 | ||
|
|
d3c719d89a | ||
|
|
dcd070b3e7 | ||
|
|
32c08e49c5 | ||
|
|
28a496bc67 | ||
|
|
4386894e8a | ||
|
|
33613c7041 | ||
|
|
db1b4d48b8 | ||
|
|
f007e2cecb | ||
|
|
7e4df4c3d1 | ||
|
|
03b98b433b | ||
|
|
1b17743ed3 | ||
|
|
03f12f6aa4 | ||
|
|
0176d66a94 | ||
|
|
f078154b9b | ||
|
|
35a4c63c27 | ||
|
|
70f3508005 | ||
|
|
16e22e614b | ||
|
|
c8b2728e27 | ||
|
|
452c780d40 | ||
|
|
9567471e7f | ||
|
|
4d4923cd37 | ||
|
|
e85404a9c5 | ||
|
|
1d43e46f99 | ||
|
|
5a8c1dcb57 | ||
|
|
9ffc523443 | ||
|
|
39693a19bd | ||
|
|
18fcaff5ee | ||
|
|
568d5e4cdf | ||
|
|
99c24b5cd8 | ||
|
|
a3087d14d8 | ||
|
|
cc7de52caf | ||
|
|
05865d51c6 | ||
|
|
45089e176f | ||
|
|
00a41b95b9 | ||
|
|
765efd19da | ||
|
|
ac59e28fcd | ||
|
|
77dab70ff7 | ||
|
|
0b66e911b1 | ||
|
|
6388a798c9 | ||
|
|
c45149b664 | ||
|
|
ce0c1c39e2 | ||
|
|
52809a2783 | ||
|
|
be3909370e | ||
|
|
f79733e5df | ||
|
|
2d95de06d6 | ||
|
|
97502231a3 | ||
|
|
d20a6d2677 | ||
|
|
9f43c0ddc8 | ||
|
|
4cb1bf6a9f | ||
|
|
d96263fde9 | ||
|
|
ed8b2d9927 | ||
|
|
7b3be389d4 | ||
|
|
68755f4303 | ||
|
|
0e1f712dcc | ||
|
|
0ab1cfdeb6 | ||
|
|
8185ee991b | ||
|
|
1001d7462a | ||
|
|
f9929ebd61 | ||
|
|
aa69a7cad2 | ||
|
|
4de063de98 | ||
|
|
d765d0350d | ||
|
|
d2459a5837 | ||
|
|
e1f604d857 | ||
|
|
af4e860176 | ||
|
|
a3d665503f | ||
|
|
b47fbde479 | ||
|
|
115f46a4fa | ||
|
|
b0f8486ef2 | ||
|
|
57c27e6a4b | ||
|
|
f591939a6a | ||
|
|
2d41cce90f | ||
|
|
3b1aff1db1 | ||
|
|
3a64b43032 | ||
|
|
59f53760d1 | ||
|
|
2980c1afac | ||
|
|
39054a7c3d | ||
|
|
4b7e47e265 | ||
|
|
4e7824583d | ||
|
|
ba53c74130 | ||
|
|
bc263e7afb | ||
|
|
bc27412425 | ||
|
|
fa8086d525 | ||
|
|
04534c2008 | ||
|
|
780fffb88f | ||
|
|
1e72d3c270 | ||
|
|
1e38d36161 | ||
|
|
bb9908e1fa | ||
|
|
6bafa83cef | ||
|
|
2c249781a2 | ||
|
|
8334ac031b | ||
|
|
635ca081e4 | ||
|
|
10f879f29a | ||
|
|
521e505a01 | ||
|
|
f968587f6f | ||
|
|
e70f8e74ec | ||
|
|
32fd01ed33 | ||
|
|
00718f8c9a | ||
|
|
20ee9d485d | ||
|
|
e3f66d7e22 | ||
|
|
be81e63eed | ||
|
|
2cf4e8ebce | ||
|
|
e6e98975ed | ||
|
|
ccb0df10e4 | ||
|
|
dd31d1e8c6 | ||
|
|
a494bad543 | ||
|
|
363699a175 | ||
|
|
439ef1ba90 |
@@ -24,7 +24,8 @@
|
||||
"debug",
|
||||
"storage",
|
||||
"infra",
|
||||
"plugin-infra"
|
||||
"plugin-cli",
|
||||
"sdk"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,3 +9,6 @@ lib
|
||||
.eslintrc.js
|
||||
packages/i18n/src/i18n-generated.ts
|
||||
e2e-dist-*
|
||||
static
|
||||
web-static
|
||||
public
|
||||
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -22,10 +22,15 @@ const createPattern = packageName => [
|
||||
allowTypeImports: false,
|
||||
},
|
||||
{
|
||||
group: ['@blocksuite/store'],
|
||||
group: ['@blocksuite /store'],
|
||||
message: "Import from '@blocksuite/global/utils'",
|
||||
importNames: ['assertExists', 'assertEquals'],
|
||||
},
|
||||
{
|
||||
group: ['react-router-dom'],
|
||||
message: 'Use `useNavigateHelper` instead',
|
||||
importNames: ['useNavigate'],
|
||||
},
|
||||
];
|
||||
|
||||
const allPackages = [
|
||||
@@ -38,7 +43,8 @@ const allPackages = [
|
||||
'packages/i18n',
|
||||
'packages/jotai',
|
||||
'packages/native',
|
||||
'packages/plugin-infra',
|
||||
'packages/infra',
|
||||
'packages/sdk',
|
||||
'packages/templates',
|
||||
'packages/theme',
|
||||
'packages/workspace',
|
||||
@@ -144,6 +150,11 @@ const config = {
|
||||
message: "Import from '@blocksuite/global/utils'",
|
||||
importNames: ['assertExists', 'assertEquals'],
|
||||
},
|
||||
{
|
||||
group: ['react-router-dom'],
|
||||
message: 'Use `useNavigateHelper` instead',
|
||||
importNames: ['useNavigate'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
4
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -21,8 +21,8 @@ body:
|
||||
label: Distribution version
|
||||
description: What version of AFFiNE are you using?
|
||||
options:
|
||||
- macOS x64
|
||||
- macOS ARM 64
|
||||
- macOS x64 (Intel)
|
||||
- macOS ARM 64 (Apple Silicon)
|
||||
- Windows x64
|
||||
- Linux
|
||||
- Web (app.affine.pro)
|
||||
|
||||
16
.github/actions/setup-maker/action.yml
vendored
Normal file
16
.github/actions/setup-maker/action.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Setup maker
|
||||
description: 'Setup maker dmg for electron'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Install @electron-forge/maker-dmg'
|
||||
if: runner.os == 'macos'
|
||||
shell: bash
|
||||
working-directory: ./apps/electron
|
||||
run: yarn add @electron-forge/maker-dmg --dev
|
||||
env:
|
||||
HUSKY: '0'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||
SENTRYCLI_SKIP_DOWNLOAD: '1'
|
||||
8
.github/actions/setup-node/action.yml
vendored
8
.github/actions/setup-node/action.yml
vendored
@@ -17,10 +17,6 @@ inputs:
|
||||
description: 'Download the Electron binary'
|
||||
required: false
|
||||
default: 'true'
|
||||
npm-token:
|
||||
description: 'The NPM token to use for private packages.'
|
||||
required: false
|
||||
default: ''
|
||||
hard-link-nm:
|
||||
description: 'set nmMode to hardlinks-local in .yarnrc.yml'
|
||||
required: false
|
||||
@@ -48,20 +44,20 @@ runs:
|
||||
shell: bash
|
||||
run: yarn install ${{ inputs.extra-flags }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
|
||||
HUSKY: '0'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||
SENTRYCLI_SKIP_DOWNLOAD: '1'
|
||||
|
||||
- name: yarn install (try again)
|
||||
if: ${{ steps.install.outcome == 'failure' }}
|
||||
shell: bash
|
||||
run: yarn install ${{ inputs.extra-flags }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
|
||||
HUSKY: '0'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||
SENTRYCLI_SKIP_DOWNLOAD: '1'
|
||||
|
||||
- name: Get installed Playwright version
|
||||
id: playwright-version
|
||||
|
||||
13
.github/actions/setup-sentry/action.yml
vendored
Normal file
13
.github/actions/setup-sentry/action.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Setup @sentry/cli
|
||||
description: 'Setup @sentry/cli'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Install @sentry/cli from brew'
|
||||
if: runner.os == 'macos'
|
||||
shell: bash
|
||||
run: brew install getsentry/tools/sentry-cli
|
||||
- name: 'Install @sentry/cli from npm'
|
||||
if: runner.os != 'macos'
|
||||
shell: bash
|
||||
run: sudo npm install -g @sentry/cli --unsafe-perm
|
||||
10
.github/labeler.yml
vendored
10
.github/labeler.yml
vendored
@@ -22,8 +22,14 @@ plugin:bookmark-block:
|
||||
plugin:copilot:
|
||||
- 'plugins/copilot/**/*'
|
||||
|
||||
mod:plugin-infra:
|
||||
- 'packages/plugin-infra/**/*'
|
||||
mod:infra:
|
||||
- 'packages/infra/**/*'
|
||||
|
||||
mod:sdk:
|
||||
- 'packages/sdk/**/*'
|
||||
|
||||
mod:plugin-cli:
|
||||
- 'packages/plugin-cli/**/*'
|
||||
|
||||
mod:workspace: 'packages/workspace/**/*'
|
||||
|
||||
|
||||
142
.github/workflows/build.yml
vendored
142
.github/workflows/build.yml
vendored
@@ -47,8 +47,6 @@ jobs:
|
||||
electron-install: false
|
||||
- name: Run i18n codegen
|
||||
run: yarn i18n-codegen gen
|
||||
- name: Run Type Check
|
||||
run: yarn typecheck
|
||||
- name: Run ESLint
|
||||
run: yarn lint:eslint --max-warnings=0
|
||||
- name: Run Prettier
|
||||
@@ -58,6 +56,21 @@ jobs:
|
||||
yarn lint:prettier
|
||||
- name: Run circular
|
||||
run: yarn circular
|
||||
- name: Run Type Check
|
||||
run: yarn typecheck
|
||||
|
||||
build-server:
|
||||
name: Build Server
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: false
|
||||
- name: Build Server
|
||||
run: yarn nx build @affine/server
|
||||
- name: Upload server dist
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -110,6 +123,8 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build Plugins
|
||||
run: yarn run build:plugins
|
||||
- name: Build Core
|
||||
run: yarn nx build @affine/core
|
||||
- name: Upload core artifact
|
||||
@@ -119,10 +134,33 @@ jobs:
|
||||
path: ./apps/core/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-storage:
|
||||
name: Build Storage
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
- name: Build Storage
|
||||
run: yarn build:storage
|
||||
- name: Upload storage.node
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storage.node
|
||||
path: ./packages/storage/storage.node
|
||||
if-no-files-found: error
|
||||
|
||||
server-test:
|
||||
name: Server Test
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
needs: build-storage
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
@@ -158,24 +196,17 @@ jobs:
|
||||
working-directory: apps/server
|
||||
env:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
- name: Download storage.node
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
- name: Build Storage
|
||||
run: yarn build:storage
|
||||
name: storage.node
|
||||
path: ./apps/server
|
||||
- name: Run server tests
|
||||
run: yarn test:coverage
|
||||
working-directory: apps/server
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
- name: Upload storage.node
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storage.node
|
||||
path: ./packages/storage/storage.node
|
||||
if-no-files-found: error
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
@@ -207,6 +238,48 @@ jobs:
|
||||
run: |
|
||||
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test"
|
||||
|
||||
e2e-plugin-test:
|
||||
name: E2E Plugin Test
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
needs: build-core
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
playwright-install: true
|
||||
electron-install: false
|
||||
- name: Download core artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: core
|
||||
path: ./apps/core/dist
|
||||
- name: Run playwright tests
|
||||
run: yarn e2e --forbid-only
|
||||
working-directory: tests/affine-plugin
|
||||
env:
|
||||
COVERAGE: true
|
||||
- name: Collect code coverage report
|
||||
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
|
||||
|
||||
- name: Upload e2e test coverage results
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./.coverage/lcov.info
|
||||
flags: e2e-plugin-test
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-results-e2e-plugin
|
||||
path: ./test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
e2e-test:
|
||||
name: E2E Test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -261,7 +334,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
needs: build-core
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
spec:
|
||||
- { package: 0.7.0-canary.18 }
|
||||
- { package: 0.8.0-canary.7 }
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
@@ -278,22 +355,18 @@ jobs:
|
||||
|
||||
- name: Unzip
|
||||
run: yarn unzip
|
||||
working-directory: ./tests/affine-legacy/0.7.0-canary.18
|
||||
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
|
||||
|
||||
- name: Run legacy playwright tests
|
||||
- name: Run playwright tests
|
||||
run: yarn e2e --forbid-only
|
||||
working-directory: ./tests/affine-legacy/0.7.0-canary.18
|
||||
|
||||
- name: Run vitest
|
||||
run: yarn test
|
||||
working-directory: ./tests/affine-legacy/0.7.0-canary.18
|
||||
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-results-e2e-migration
|
||||
path: ./tests/affine-legacy/0.7.0-canary.18/test-results
|
||||
name: test-results-e2e-migration-${{ matrix.spec.package }}
|
||||
path: ./tests/affine-legacy/${{ matrix.spec.package }}/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
desktop-test:
|
||||
@@ -338,6 +411,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
playwright-install: true
|
||||
hard-link-nm: false
|
||||
@@ -349,9 +423,8 @@ jobs:
|
||||
- name: Run unit tests
|
||||
if: ${{ matrix.spec.test }}
|
||||
shell: bash
|
||||
run: yarn nx test @affine/monorepo
|
||||
env:
|
||||
NATIVE_TEST: 'true'
|
||||
run: yarn vitest
|
||||
working-directory: ./apps/electron
|
||||
|
||||
- name: Download core artifact
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -365,6 +438,12 @@ jobs:
|
||||
- name: Build Desktop Layers
|
||||
run: yarn workspace @affine/electron build
|
||||
|
||||
- name: Upload desktop dist
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
path: ./apps/electron/dist
|
||||
|
||||
- name: Run desktop tests
|
||||
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
|
||||
@@ -379,12 +458,13 @@ jobs:
|
||||
|
||||
- name: Make bundle
|
||||
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
|
||||
env:
|
||||
SKIP_BUNDLE: true
|
||||
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
|
||||
|
||||
- name: Bundle output check
|
||||
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
|
||||
run: |
|
||||
./scripts/unzip-macos-arm64.sh
|
||||
yarn ts-node-esm ./scripts/macos-arm64-output-check.mts
|
||||
working-directory: apps/electron
|
||||
|
||||
@@ -436,11 +516,11 @@ jobs:
|
||||
build-docker:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
name: Build Docker
|
||||
needs:
|
||||
- lint
|
||||
- desktop-test
|
||||
- server-test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server
|
||||
- build-core
|
||||
- build-storage
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download core artifact
|
||||
|
||||
2
.github/workflows/cancel.yml
vendored
2
.github/workflows/cancel.yml
vendored
@@ -14,5 +14,5 @@ jobs:
|
||||
- uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
# See https://api.github.com/repos/toeverything/AFFiNE/actions/workflows
|
||||
workflow_id: 44038251, 61883931
|
||||
workflow_id: 44038251, 61883931, 65188160
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
9
.github/workflows/dependabot.yml
vendored
Normal file
9
.github/workflows/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
versioning-strategy: increase
|
||||
commit-message:
|
||||
prefix: 'chore'
|
||||
9
.github/workflows/nightly-build.yml
vendored
9
.github/workflows/nightly-build.yml
vendored
@@ -50,6 +50,8 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup @sentry/cli
|
||||
uses: ./.github/actions/setup-sentry
|
||||
- name: Replace Version
|
||||
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
||||
- name: generate-assets
|
||||
@@ -110,7 +112,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Maker
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-maker
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
@@ -120,7 +126,7 @@ jobs:
|
||||
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: before-make-web-static
|
||||
name: core
|
||||
path: apps/electron/resources/web-static
|
||||
|
||||
- name: Build Plugins
|
||||
@@ -159,6 +165,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
|
||||
mv apps/electron/out/*/make/AppImage/x64/*.AppImage ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.AppImage
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
7
.github/workflows/release-desktop-app.yml
vendored
7
.github/workflows/release-desktop-app.yml
vendored
@@ -47,6 +47,8 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup @sentry/cli
|
||||
uses: ./.github/actions/setup-sentry
|
||||
- name: Get canary version
|
||||
id: get-canary-version
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
@@ -112,7 +114,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Maker
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-maker
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
@@ -159,6 +165,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
|
||||
mv apps/electron/out/*/make/AppImage/x64/*.AppImage ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.AppImage
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
13
README.md
13
README.md
@@ -28,7 +28,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
|
||||
[](https://github.com/toeverything/AFFiNE/releases/latest)
|
||||
[![stars-icon]](https://github.com/toeverything/AFFiNE)
|
||||
@@ -126,10 +126,11 @@ If you have questions, you are welcome to contact us. One of the best places to
|
||||
>
|
||||
> (Currently, plugins are under heavy development, and the SDK is not yet available.)
|
||||
|
||||
| Name | |
|
||||
| ------------------------------------------------ | ----------------------------------------- |
|
||||
| [@affine/bookmark-block](plugins/bookmark-block) | A block for bookmarking a website |
|
||||
| [@affine/copilot](plugins/copilot) | AI Copilot that help you document writing |
|
||||
| Official Plugin | Description |
|
||||
| ----------------------------------------------------- | ----------------------------------------- |
|
||||
| [@affine/bookmark-plugin](plugins/bookmark) | A block for bookmarking a website |
|
||||
| [@affine/copilot-plugin](plugins/copilot) | AI Copilot that help you document writing |
|
||||
| [@affine/image-preview-plugin](plugins/image-preview) | Component for previewing an image |
|
||||
|
||||
## Thanks
|
||||
|
||||
@@ -197,7 +198,7 @@ See [LICENSE] for details.
|
||||
[jobs available]: ./docs/jobs.md
|
||||
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
|
||||
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
|
||||
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
|
||||
[rust-version-icon]: https://img.shields.io/badge/Rust-1.71.0-dea584
|
||||
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
|
||||
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
|
||||
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.1-success
|
||||
|
||||
@@ -8,9 +8,9 @@ export const productionCacheGroups = {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module: any) {
|
||||
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||
const name =
|
||||
module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)?.[1] ??
|
||||
'unknown';
|
||||
const name = module.context.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||
)?.[1];
|
||||
return `npm-async-${name}`;
|
||||
},
|
||||
priority: Number.MAX_SAFE_INTEGER,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
|
||||
import { PerfseePlugin } from '@perfsee/webpack';
|
||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||
@@ -32,6 +31,7 @@ const OptimizeOptionOptions: (
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: {
|
||||
@@ -79,6 +79,10 @@ export const createConfiguration: (
|
||||
name: 'affine',
|
||||
// to set a correct base path for the source map
|
||||
context: projectRoot,
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
outputModule: false,
|
||||
},
|
||||
output: {
|
||||
environment: {
|
||||
module: true,
|
||||
@@ -106,11 +110,12 @@ export const createConfiguration: (
|
||||
devtool:
|
||||
buildFlags.mode === 'production'
|
||||
? buildFlags.distribution === 'desktop'
|
||||
? 'inline-cheap-source-map'
|
||||
? 'nosources-source-map'
|
||||
: 'source-map'
|
||||
: 'eval-cheap-module-source-map',
|
||||
|
||||
resolve: {
|
||||
symlinks: true,
|
||||
extensionAlias: {
|
||||
'.js': ['.js', '.tsx', '.ts'],
|
||||
'.mjs': ['.mjs', '.mts'],
|
||||
@@ -129,6 +134,10 @@ export const createConfiguration: (
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
// Do not mock Node.js globals
|
||||
node: false,
|
||||
requireJs: false,
|
||||
import: true,
|
||||
// Treat as missing export as error
|
||||
strictExportPresence: true,
|
||||
},
|
||||
@@ -136,6 +145,20 @@ export const createConfiguration: (
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js?$/,
|
||||
enforce: 'pre',
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('source-map-loader'),
|
||||
options: {
|
||||
filterSourceMappingUrl: (
|
||||
_url: string,
|
||||
resourcePath: string
|
||||
) => {
|
||||
return resourcePath.includes('@blocksuite');
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
@@ -251,14 +274,6 @@ export const createConfiguration: (
|
||||
ignoreOrder: true,
|
||||
}),
|
||||
]),
|
||||
new HTMLPlugin({
|
||||
template: join(rootPath, '.webpack', 'template.html'),
|
||||
inject: 'body',
|
||||
scriptLoading: 'defer',
|
||||
minify: false,
|
||||
chunks: ['index', 'plugin'],
|
||||
filename: 'index.html',
|
||||
}),
|
||||
new VanillaExtractPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': JSON.stringify({}),
|
||||
@@ -268,7 +283,10 @@ export const createConfiguration: (
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: resolve(rootPath, 'public'), to: resolve(rootPath, 'dist') },
|
||||
{
|
||||
from: resolve(rootPath, 'public'),
|
||||
to: resolve(rootPath, 'dist'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -23,7 +23,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableTestProperties: false,
|
||||
enableBroadcastChannelProvider: true,
|
||||
enableDebugPage: true,
|
||||
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0717',
|
||||
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0728',
|
||||
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
|
||||
enablePreloading: true,
|
||||
enableNewSettingModal: true,
|
||||
@@ -43,7 +43,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableTestProperties: true,
|
||||
enableBroadcastChannelProvider: true,
|
||||
enableDebugPage: true,
|
||||
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0717',
|
||||
changelogUrl: 'https://github.com/toeverything/AFFiNE/releases',
|
||||
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
|
||||
enablePreloading: true,
|
||||
enableNewSettingModal: true,
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
rel="shortcut icon"
|
||||
href="https://affine.pro/favicon.ico"
|
||||
/>
|
||||
<link rel="stylesheet" href="/plugins/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createConfiguration, rootPath } from './config.js';
|
||||
import { merge } from 'webpack-merge';
|
||||
import { resolve } from 'node:path';
|
||||
import { join, resolve } from 'node:path';
|
||||
import type { BuildFlags } from '@affine/cli/config';
|
||||
import { getRuntimeConfig } from './runtime-config.js';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
|
||||
export default async function (cli_env: any, _: any) {
|
||||
const flags: BuildFlags = JSON.parse(
|
||||
@@ -14,15 +15,41 @@ export default async function (cli_env: any, _: any) {
|
||||
const config = createConfiguration(flags, runtimeConfig);
|
||||
return merge(config, {
|
||||
entry: {
|
||||
index: {
|
||||
asyncChunks: false,
|
||||
import: resolve(rootPath, 'src/index.tsx'),
|
||||
'polyfill/ses': {
|
||||
import: resolve(rootPath, 'src/polyfill/ses.ts'),
|
||||
},
|
||||
plugin: {
|
||||
dependOn: ['index'],
|
||||
asyncChunks: true,
|
||||
dependOn: ['polyfill/ses'],
|
||||
import: resolve(rootPath, 'src/bootstrap/register-plugins.ts'),
|
||||
},
|
||||
app: {
|
||||
chunkLoading: 'import',
|
||||
dependOn: ['polyfill/ses', 'plugin'],
|
||||
import: resolve(rootPath, 'src/index.tsx'),
|
||||
},
|
||||
'_plugin/index.test': {
|
||||
chunkLoading: 'import',
|
||||
dependOn: ['polyfill/ses', 'plugin'],
|
||||
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new HTMLPlugin({
|
||||
template: join(rootPath, '.webpack', 'template.html'),
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
chunks: ['app', 'plugin', 'polyfill/ses'],
|
||||
filename: 'index.html',
|
||||
}),
|
||||
new HTMLPlugin({
|
||||
template: join(rootPath, '.webpack', 'template.html'),
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
chunks: ['_plugin/index.test', 'plugin', 'polyfill/ses'],
|
||||
filename: '_plugin/index.html',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@affine/core",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.51",
|
||||
"version": "0.8.0-canary.10",
|
||||
"scripts": {
|
||||
"build": "yarn -T run build-core",
|
||||
"dev": "yarn -T run dev-core",
|
||||
@@ -11,7 +11,6 @@
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/copilot": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/graphql": "workspace:*",
|
||||
@@ -19,20 +18,20 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/icons": "^2.1.26",
|
||||
"@blocksuite/lit": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/icons": "^2.1.29",
|
||||
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.14.1",
|
||||
"@mui/material": "^5.14.2",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
@@ -49,7 +48,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-is": "18.2.0",
|
||||
"react-resizable-panels": "^0.0.53",
|
||||
"react-resizable-panels": "^0.0.54",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"ses": "^0.18.5",
|
||||
@@ -61,15 +60,16 @@
|
||||
"devDependencies": {
|
||||
"@perfsee/webpack": "^1.8.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@sentry/webpack-plugin": "^2.4.0",
|
||||
"@sentry/webpack-plugin": "^2.5.0",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@swc/core": "^1.3.70",
|
||||
"@swc/core": "^1.3.71",
|
||||
"@types/webpack-env": "^1.18.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"express": "^4.18.2",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-plugin-coverage-instrument": "^0.0.19",
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"name": "@affine/core",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"root": "apps/core",
|
||||
"sourceRoot": "apps/core/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": ["tag:plugin"],
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"{projectRoot}/.webpack/**/*",
|
||||
"{projectRoot}/**/*",
|
||||
"{projectRoot}/public/**/*",
|
||||
"{workspaceRoot}/packages/component/src/**/*",
|
||||
"{workspaceRoot}/packages/debug/src/**/*",
|
||||
"{workspaceRoot}/packages/graphql/src/**/*",
|
||||
@@ -45,7 +51,7 @@
|
||||
"options": {
|
||||
"script": "build"
|
||||
},
|
||||
"outputs": ["{projectRoot}/dist", "{projectRoot}/public/plugins"]
|
||||
"outputs": ["{projectRoot}/dist"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ const PORT = process.env.PORT || 8080;
|
||||
app.use('/', express.static('dist'));
|
||||
|
||||
app.get('/*', (req, res) => {
|
||||
if (req.url.startsWith('/plugins')) {
|
||||
res.sendFile(req.url, { root: 'dist' });
|
||||
}
|
||||
res.sendFile('index.html', { root: 'dist' });
|
||||
});
|
||||
|
||||
|
||||
47
apps/core/src/_plugin/index.test.tsx
Normal file
47
apps/core/src/_plugin/index.test.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { use } from 'foxact/use';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Provider } from 'jotai/react';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { _pluginNestedImportsMap } from '../bootstrap/plugins/setup';
|
||||
import { pluginRegisterPromise } from '../bootstrap/register-plugins';
|
||||
|
||||
async function main() {
|
||||
const { setup } = await import('../bootstrap/setup');
|
||||
await setup();
|
||||
const root = document.getElementById('app');
|
||||
assertExists(root);
|
||||
|
||||
const App = () => {
|
||||
use(pluginRegisterPromise);
|
||||
const plugins = useAtomValue(registeredPluginAtom);
|
||||
_pluginNestedImportsMap.forEach(value => {
|
||||
const exports = value.get('index.js');
|
||||
assertExists(exports);
|
||||
assertExists(exports?.get('entry'));
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div data-plugins-load-status="success">
|
||||
Successfully loaded plugins:
|
||||
</div>
|
||||
{plugins.map(plugin => {
|
||||
return <div key={plugin}>{plugin}</div>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<Provider store={rootStore}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
|
||||
@@ -3,120 +3,15 @@ import '@affine/component/theme/theme.css';
|
||||
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import type { RouterState } from '@remix-run/router';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import { use } from 'foxact/use';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, memo, Suspense, useEffect } from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { lazy, memo, Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { historyBaseAtom, MAX_HISTORY } from './atoms/history';
|
||||
import { router } from './router';
|
||||
import createEmotionCache from './utils/create-emotion-cache';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
lazy: () => import('./pages/index'),
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/all',
|
||||
lazy: () => import('./pages/workspace/all-page'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/trash',
|
||||
lazy: () => import('./pages/workspace/trash-page'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/:pageId',
|
||||
lazy: () => import('./pages/workspace/detail-page'),
|
||||
},
|
||||
]);
|
||||
|
||||
//#region atoms bootstrap
|
||||
|
||||
currentWorkspaceIdAtom.onMount = set => {
|
||||
const callback = (state: RouterState) => {
|
||||
const value = state.location.pathname.split('/')[2];
|
||||
if (value) {
|
||||
set(value);
|
||||
localStorage.setItem('last_workspace_id', value);
|
||||
}
|
||||
};
|
||||
callback(router.state);
|
||||
|
||||
const unsubscribe = router.subscribe(callback);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
currentPageIdAtom.onMount = set => {
|
||||
const callback = (state: RouterState) => {
|
||||
const value = state.location.pathname.split('/')[3];
|
||||
if (value) {
|
||||
set(value);
|
||||
}
|
||||
};
|
||||
callback(router.state);
|
||||
|
||||
const unsubscribe = router.subscribe(callback);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
historyBaseAtom.onMount = set => {
|
||||
const unsubscribe = router.subscribe(state => {
|
||||
set(prev => {
|
||||
const url = state.location.pathname;
|
||||
console.log('push', url, prev.skip, prev.stack.length, prev.current);
|
||||
if (prev.skip) {
|
||||
return {
|
||||
stack: [...prev.stack],
|
||||
current: prev.current,
|
||||
skip: false,
|
||||
};
|
||||
} else {
|
||||
if (prev.current < prev.stack.length - 1) {
|
||||
const newStack = prev.stack.slice(0, prev.current);
|
||||
newStack.push(url);
|
||||
if (newStack.length > MAX_HISTORY) {
|
||||
newStack.shift();
|
||||
}
|
||||
return {
|
||||
stack: newStack,
|
||||
current: newStack.length - 1,
|
||||
skip: false,
|
||||
};
|
||||
} else {
|
||||
const newStack = [...prev.stack, url];
|
||||
if (newStack.length > MAX_HISTORY) {
|
||||
newStack.shift();
|
||||
}
|
||||
return {
|
||||
stack: newStack,
|
||||
current: newStack.length - 1,
|
||||
skip: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
};
|
||||
//#endregion
|
||||
|
||||
const i18n = createI18n();
|
||||
const cache = createEmotionCache();
|
||||
|
||||
const DevTools = lazy(() =>
|
||||
@@ -132,21 +27,32 @@ const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export const App = memo(function App() {
|
||||
useEffect(() => {
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
async function loadLanguage() {
|
||||
if (environment.isBrowser) {
|
||||
const { createI18n, setUpLanguage } = await import('@affine/i18n');
|
||||
const i18n = createI18n();
|
||||
document.documentElement.lang = i18n.language;
|
||||
// todo(himself65): this is a hack, we should use a better way to set the language
|
||||
setUpLanguage(i18n)?.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}, []);
|
||||
await setUpLanguage(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
const languageLoadingPromise = loadLanguage().catch(console.error);
|
||||
|
||||
export const App = memo(function App() {
|
||||
use(languageLoadingPromise);
|
||||
return (
|
||||
<CacheProvider value={cache}>
|
||||
<AffineContext>
|
||||
<DebugProvider>
|
||||
<Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
<RouterProvider
|
||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</DebugProvider>
|
||||
</AffineContext>
|
||||
</CacheProvider>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
|
||||
import { useCallback } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { router } from '../router';
|
||||
|
||||
export type History = {
|
||||
stack: string[];
|
||||
current: number;
|
||||
@@ -11,11 +14,62 @@ export type History = {
|
||||
|
||||
export const MAX_HISTORY = 50;
|
||||
|
||||
export const historyBaseAtom = atomWithStorage<History>('router-history', {
|
||||
stack: [],
|
||||
current: 0,
|
||||
skip: false,
|
||||
});
|
||||
const historyBaseAtom = atomWithStorage<History>(
|
||||
'router-history',
|
||||
{
|
||||
stack: [],
|
||||
current: 0,
|
||||
skip: false,
|
||||
},
|
||||
createJSONStorage(() => sessionStorage)
|
||||
);
|
||||
|
||||
historyBaseAtom.onMount = set => {
|
||||
const unsubscribe = router.subscribe(state => {
|
||||
set(prev => {
|
||||
const url = state.location.pathname;
|
||||
|
||||
// if stack top is the same as current, skip
|
||||
if (prev.stack[prev.current] === url) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (prev.skip) {
|
||||
return {
|
||||
stack: [...prev.stack],
|
||||
current: prev.current,
|
||||
skip: false,
|
||||
};
|
||||
} else {
|
||||
if (prev.current < prev.stack.length - 1) {
|
||||
const newStack = prev.stack.slice(0, prev.current);
|
||||
newStack.push(url);
|
||||
if (newStack.length > MAX_HISTORY) {
|
||||
newStack.shift();
|
||||
}
|
||||
return {
|
||||
stack: newStack,
|
||||
current: newStack.length - 1,
|
||||
skip: false,
|
||||
};
|
||||
} else {
|
||||
const newStack = [...prev.stack, url];
|
||||
if (newStack.length > MAX_HISTORY) {
|
||||
newStack.shift();
|
||||
}
|
||||
return {
|
||||
stack: newStack,
|
||||
current: newStack.length - 1,
|
||||
skip: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
export function useHistoryAtom() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -93,7 +93,7 @@ export const pageSettingFamily = atomFamily((pageId: string) =>
|
||||
|
||||
export const setPageModeAtom = atom(
|
||||
void 0,
|
||||
(get, set, pageId: string, mode: PageMode) => {
|
||||
(_, set, pageId: string, mode: PageMode) => {
|
||||
set(pageSettingFamily(pageId), { mode });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { atom } from 'jotai/vanilla';
|
||||
|
||||
import { pageSettingFamily } from './index';
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { migrateToSubdoc } from '@affine/env/blocksuite';
|
||||
import { setupGlobal } from '@affine/env/global';
|
||||
import type {
|
||||
LocalIndexedDBDownloadProvider,
|
||||
WorkspaceAdapter,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import {
|
||||
type RootWorkspaceMetadataV2,
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import {
|
||||
migrateLocalBlobStorage,
|
||||
upgradeV1ToV2,
|
||||
} from '@affine/workspace/migration';
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
console.log('setup global');
|
||||
setupGlobal();
|
||||
|
||||
rootStore.set(
|
||||
workspaceAdaptersAtom,
|
||||
WorkspaceAdapters as Record<
|
||||
WorkspaceFlavour,
|
||||
WorkspaceAdapter<WorkspaceFlavour>
|
||||
>
|
||||
);
|
||||
|
||||
const value = localStorage.getItem('jotai-workspaces');
|
||||
if (value) {
|
||||
try {
|
||||
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
|
||||
const promises: Promise<void>[] = [];
|
||||
const newMetadata = [...metadata];
|
||||
metadata.forEach(oldMeta => {
|
||||
if (!('version' in oldMeta)) {
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
assertExists(adapter);
|
||||
const upgrade = async () => {
|
||||
const workspace = await adapter.CRUD.get(oldMeta.id);
|
||||
if (!workspace) {
|
||||
console.warn('cannot find workspace', oldMeta.id);
|
||||
return;
|
||||
}
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
const doc = workspace.blockSuiteWorkspace.doc;
|
||||
const provider = createIndexedDBDownloadProvider(workspace.id, doc, {
|
||||
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness,
|
||||
}) as LocalIndexedDBDownloadProvider;
|
||||
provider.sync();
|
||||
await provider.whenReady;
|
||||
const newDoc = migrateToSubdoc(doc);
|
||||
if (doc === newDoc) {
|
||||
console.log('doc not changed');
|
||||
return;
|
||||
}
|
||||
const newWorkspace = upgradeV1ToV2(workspace);
|
||||
|
||||
const newId = await adapter.CRUD.create(
|
||||
newWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
await adapter.CRUD.delete(workspace as any);
|
||||
console.log('migrated', oldMeta.id, newId);
|
||||
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
id: newId,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
};
|
||||
await migrateLocalBlobStorage(workspace.id, newId);
|
||||
};
|
||||
|
||||
// create a new workspace and push it to metadata
|
||||
promises.push(upgrade());
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
console.log('migration done');
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('migration failed');
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
|
||||
window.dispatchEvent(new CustomEvent('migration-done'));
|
||||
window.$migrationDone = true;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('error when migrating data', e);
|
||||
}
|
||||
}
|
||||
|
||||
const createFirst = (): RootWorkspaceMetadataV2[] => {
|
||||
const Plugins = Object.values(WorkspaceAdapters).sort(
|
||||
(a, b) => a.loadPriority - b.loadPriority
|
||||
);
|
||||
|
||||
return Plugins.flatMap(Plugin => {
|
||||
return Plugin.Events['app:init']?.().map(
|
||||
id =>
|
||||
({
|
||||
id,
|
||||
flavour: Plugin.flavour,
|
||||
// new workspace should all support sub-doc feature
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
}) satisfies RootWorkspaceMetadataV2
|
||||
);
|
||||
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
|
||||
};
|
||||
|
||||
await rootStore
|
||||
.get(rootWorkspacesMetadataAtom)
|
||||
.then(meta => {
|
||||
if (meta.length === 0 && localStorage.getItem('is-first-open') === null) {
|
||||
const result = createFirst();
|
||||
console.info('create first workspace', result);
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
rootStore.set(rootWorkspacesMetadataAtom, result).catch(console.error);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
86
apps/core/src/bootstrap/plugins/endowments/fercher.ts
Normal file
86
apps/core/src/bootstrap/plugins/endowments/fercher.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
export interface FetchOptions {
|
||||
fetch?: typeof fetch;
|
||||
signal?: AbortSignal;
|
||||
|
||||
normalizeURL?(url: string): string;
|
||||
|
||||
/**
|
||||
* Virtualize a url
|
||||
* @param url URL to be rewrite
|
||||
* @param direction Direction of this rewrite.
|
||||
* 'in' means the url is from the outside world and should be virtualized.
|
||||
* 'out' means the url is from the inside world and should be de-virtualized to fetch the real target.
|
||||
*/
|
||||
rewriteURL?(url: string, direction: 'in' | 'out'): string;
|
||||
|
||||
replaceRequest?(request: Request): Request | PromiseLike<Request>;
|
||||
|
||||
replaceResponse?(response: Response): Response | PromiseLike<Response>;
|
||||
|
||||
canConnect?(url: string): boolean | PromiseLike<boolean>;
|
||||
}
|
||||
|
||||
export function createFetch(options: FetchOptions) {
|
||||
const {
|
||||
fetch: _fetch = fetch,
|
||||
signal,
|
||||
rewriteURL,
|
||||
replaceRequest,
|
||||
replaceResponse,
|
||||
canConnect,
|
||||
normalizeURL,
|
||||
} = options;
|
||||
|
||||
return async function fetch(input: RequestInfo, init?: RequestInit) {
|
||||
let request = new Request(input, {
|
||||
...init,
|
||||
signal: getMergedSignal(init?.signal, signal) || null,
|
||||
});
|
||||
|
||||
if (normalizeURL) request = new Request(normalizeURL(request.url), request);
|
||||
if (canConnect && !(await canConnect(request.url)))
|
||||
throw new TypeError('Failed to fetch');
|
||||
if (rewriteURL)
|
||||
request = new Request(rewriteURL(request.url, 'out'), request);
|
||||
if (replaceRequest) request = await replaceRequest(request);
|
||||
|
||||
let response = await _fetch(request);
|
||||
|
||||
if (rewriteURL) {
|
||||
const { url, redirected, type } = response;
|
||||
// Note: Response constructor does not allow us to set the url of a response.
|
||||
// we have to define the own property on it. This is not a good simulation.
|
||||
// To prevent get the original url by Response.prototype.[[get url]].call(response)
|
||||
// we copy a response and set it's url to empty.
|
||||
response = new Response(response.body, response);
|
||||
Object.defineProperties(response, {
|
||||
url: { value: url, configurable: true },
|
||||
redirected: { value: redirected, configurable: true },
|
||||
type: { value: type, configurable: true },
|
||||
});
|
||||
Object.defineProperty(response, 'url', {
|
||||
configurable: true,
|
||||
value: rewriteURL(url, 'in'),
|
||||
});
|
||||
}
|
||||
if (replaceResponse) response = await replaceResponse(response);
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
function getMergedSignal(
|
||||
signal: AbortSignal | undefined | null,
|
||||
signal2: AbortSignal | undefined | null
|
||||
) {
|
||||
if (!signal) return signal2;
|
||||
if (!signal2) return signal;
|
||||
|
||||
const abortController = new AbortController();
|
||||
signal.addEventListener('abort', () => abortController.abort(), {
|
||||
once: true,
|
||||
});
|
||||
signal2.addEventListener('abort', () => abortController.abort(), {
|
||||
once: true,
|
||||
});
|
||||
return abortController.signal;
|
||||
}
|
||||
110
apps/core/src/bootstrap/plugins/endowments/timer.ts
Normal file
110
apps/core/src/bootstrap/plugins/endowments/timer.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
type Handler = (...args: any[]) => void;
|
||||
|
||||
export interface Timers {
|
||||
setTimeout: (handler: Handler, timeout?: number, ...args: any[]) => number;
|
||||
clearTimeout: (handle: number) => void;
|
||||
setInterval: (handler: Handler, timeout?: number, ...args: any[]) => number;
|
||||
clearInterval: (handle: number) => void;
|
||||
requestAnimationFrame: (callback: Handler) => number;
|
||||
cancelAnimationFrame: (handle: number) => void;
|
||||
requestIdleCallback?: typeof window.requestIdleCallback | undefined;
|
||||
cancelIdleCallback?: typeof window.cancelIdleCallback | undefined;
|
||||
queueMicrotask: typeof window.queueMicrotask;
|
||||
}
|
||||
|
||||
export function createTimers(
|
||||
abortSignal: AbortSignal,
|
||||
originalTimes: Timers = {
|
||||
requestAnimationFrame,
|
||||
cancelAnimationFrame,
|
||||
requestIdleCallback:
|
||||
typeof requestIdleCallback === 'function'
|
||||
? requestIdleCallback
|
||||
: undefined,
|
||||
cancelIdleCallback:
|
||||
typeof cancelIdleCallback === 'function' ? cancelIdleCallback : undefined,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
queueMicrotask,
|
||||
}
|
||||
): Timers {
|
||||
const {
|
||||
requestAnimationFrame: _requestAnimationFrame,
|
||||
cancelAnimationFrame: _cancelAnimationFrame,
|
||||
setInterval: _setInterval,
|
||||
clearInterval: _clearInterval,
|
||||
setTimeout: _setTimeout,
|
||||
clearTimeout: _clearTimeout,
|
||||
cancelIdleCallback: _cancelIdleCallback,
|
||||
requestIdleCallback: _requestIdleCallback,
|
||||
queueMicrotask: _queueMicrotask,
|
||||
} = originalTimes;
|
||||
|
||||
const interval_timer_id: number[] = [];
|
||||
const idle_id: number[] = [];
|
||||
const raf_id: number[] = [];
|
||||
|
||||
abortSignal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
raf_id.forEach(_cancelAnimationFrame);
|
||||
interval_timer_id.forEach(_clearInterval);
|
||||
_cancelIdleCallback && idle_id.forEach(_cancelIdleCallback);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
return {
|
||||
// id is a positive number, it never repeats.
|
||||
requestAnimationFrame(callback) {
|
||||
raf_id[raf_id.length] = _requestAnimationFrame(callback);
|
||||
return raf_id.length;
|
||||
},
|
||||
cancelAnimationFrame(handle) {
|
||||
const id = raf_id[handle - 1];
|
||||
if (!id) return;
|
||||
_cancelAnimationFrame(id);
|
||||
},
|
||||
setInterval(handler, timeout) {
|
||||
interval_timer_id[interval_timer_id.length] = (_setInterval as any)(
|
||||
handler,
|
||||
timeout
|
||||
);
|
||||
return interval_timer_id.length;
|
||||
},
|
||||
clearInterval(id) {
|
||||
if (!id) return;
|
||||
const handle = interval_timer_id[id - 1];
|
||||
if (!handle) return;
|
||||
_clearInterval(handle);
|
||||
},
|
||||
setTimeout(handler, timeout) {
|
||||
idle_id[idle_id.length] = (_setTimeout as any)(handler, timeout);
|
||||
return idle_id.length;
|
||||
},
|
||||
clearTimeout(id) {
|
||||
if (!id) return;
|
||||
const handle = idle_id[id - 1];
|
||||
if (!handle) return;
|
||||
_clearTimeout(handle);
|
||||
},
|
||||
requestIdleCallback: _requestIdleCallback
|
||||
? function requestIdleCallback(callback, options) {
|
||||
idle_id[idle_id.length] = _requestIdleCallback(callback, options);
|
||||
return idle_id.length;
|
||||
}
|
||||
: undefined,
|
||||
cancelIdleCallback: _cancelIdleCallback
|
||||
? function cancelIdleCallback(handle) {
|
||||
const id = idle_id[handle - 1];
|
||||
if (!id) return;
|
||||
_cancelIdleCallback(id);
|
||||
}
|
||||
: undefined,
|
||||
queueMicrotask(callback) {
|
||||
_queueMicrotask(() => abortSignal.aborted || callback());
|
||||
},
|
||||
};
|
||||
}
|
||||
449
apps/core/src/bootstrap/plugins/setup.ts
Normal file
449
apps/core/src/bootstrap/plugins/setup.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import * as AFFiNEComponent from '@affine/component';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { CallbackMap, PluginContext } from '@affine/sdk/entry';
|
||||
import { FormatQuickBar } from '@blocksuite/blocks';
|
||||
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
||||
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import * as Icons from '@blocksuite/icons';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
currentPageAtom,
|
||||
currentWorkspaceAtom,
|
||||
editorItemsAtom,
|
||||
headerItemsAtom,
|
||||
rootStore,
|
||||
settingItemsAtom,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import * as Jotai from 'jotai/index';
|
||||
import { Provider } from 'jotai/react';
|
||||
import * as JotaiUtils from 'jotai/utils';
|
||||
import * as React from 'react';
|
||||
import { createElement, type PropsWithChildren } from 'react';
|
||||
import * as ReactJSXRuntime from 'react/jsx-runtime';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import * as ReactDomClient from 'react-dom/client';
|
||||
import * as SWR from 'swr';
|
||||
|
||||
import { createFetch } from './endowments/fercher';
|
||||
import { createTimers } from './endowments/timer';
|
||||
|
||||
const dynamicImportKey = '$h_import';
|
||||
|
||||
const permissionLogger = new DebugLogger('plugins:permission');
|
||||
const importLogger = new DebugLogger('plugins:import');
|
||||
|
||||
const setupRootImportsMap = () => {
|
||||
_rootImportsMap.set('react', new Map(Object.entries(React)));
|
||||
_rootImportsMap.set(
|
||||
'react/jsx-runtime',
|
||||
new Map(Object.entries(ReactJSXRuntime))
|
||||
);
|
||||
_rootImportsMap.set('react-dom', new Map(Object.entries(ReactDom)));
|
||||
_rootImportsMap.set(
|
||||
'react-dom/client',
|
||||
new Map(Object.entries(ReactDomClient))
|
||||
);
|
||||
_rootImportsMap.set('@blocksuite/icons', new Map(Object.entries(Icons)));
|
||||
_rootImportsMap.set(
|
||||
'@affine/component',
|
||||
new Map(Object.entries(AFFiNEComponent))
|
||||
);
|
||||
_rootImportsMap.set(
|
||||
'@blocksuite/blocks/std',
|
||||
new Map(Object.entries(BlockSuiteBlocksStd))
|
||||
);
|
||||
_rootImportsMap.set(
|
||||
'@blocksuite/global/utils',
|
||||
new Map(Object.entries(BlockSuiteGlobalUtils))
|
||||
);
|
||||
_rootImportsMap.set('jotai', new Map(Object.entries(Jotai)));
|
||||
_rootImportsMap.set('jotai/utils', new Map(Object.entries(JotaiUtils)));
|
||||
_rootImportsMap.set(
|
||||
'@affine/sdk/entry',
|
||||
new Map(
|
||||
Object.entries({
|
||||
rootStore: rootStore,
|
||||
currentWorkspaceAtom: currentWorkspaceAtom,
|
||||
currentPageAtom: currentPageAtom,
|
||||
contentLayoutAtom: contentLayoutAtom,
|
||||
})
|
||||
)
|
||||
);
|
||||
_rootImportsMap.set('swr', new Map(Object.entries(SWR)));
|
||||
};
|
||||
|
||||
// module -> importName -> updater[]
|
||||
export const _rootImportsMap = new Map<string, Map<string, any>>();
|
||||
setupRootImportsMap();
|
||||
// pluginName -> module -> importName -> updater[]
|
||||
export const _pluginNestedImportsMap = new Map<
|
||||
string,
|
||||
Map<string, Map<string, any>>
|
||||
>();
|
||||
|
||||
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
|
||||
export const createImports = (pluginName: string) => {
|
||||
if (pluginImportsFunctionMap.has(pluginName)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return pluginImportsFunctionMap.get(pluginName)!;
|
||||
}
|
||||
const imports = (
|
||||
newUpdaters: [string, [string, ((val: any) => void)[]][]][]
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
|
||||
importLogger.debug('currentImportMap', pluginName, currentImportMap);
|
||||
|
||||
for (const [module, moduleUpdaters] of newUpdaters) {
|
||||
importLogger.debug('imports module', module, moduleUpdaters);
|
||||
let moduleImports = _rootImportsMap.get(module);
|
||||
if (!moduleImports) {
|
||||
moduleImports = currentImportMap.get(module);
|
||||
}
|
||||
if (moduleImports) {
|
||||
for (const [importName, importUpdaters] of moduleUpdaters) {
|
||||
const updateImport = (value: any) => {
|
||||
for (const importUpdater of importUpdaters) {
|
||||
importUpdater(value);
|
||||
}
|
||||
};
|
||||
if (moduleImports.has(importName)) {
|
||||
const val = moduleImports.get(importName);
|
||||
updateImport(val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
'cannot find module in plugin import map',
|
||||
module,
|
||||
currentImportMap,
|
||||
_pluginNestedImportsMap
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
pluginImportsFunctionMap.set(pluginName, imports);
|
||||
return imports;
|
||||
};
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
const pluginFetch = createFetch({});
|
||||
const timer = createTimers(abortController.signal);
|
||||
|
||||
const sharedGlobalThis = Object.assign(Object.create(null), timer, {
|
||||
fetch: pluginFetch,
|
||||
});
|
||||
|
||||
const dynamicImportMap = new Map<
|
||||
string,
|
||||
(moduleName: string) => Promise<any>
|
||||
>();
|
||||
|
||||
export const createOrGetDynamicImport = (
|
||||
baseUrl: string,
|
||||
pluginName: string
|
||||
) => {
|
||||
if (dynamicImportMap.has(pluginName)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return dynamicImportMap.get(pluginName)!;
|
||||
}
|
||||
const dynamicImport = async (moduleName: string): Promise<any> => {
|
||||
const codeUrl = `${baseUrl}/${moduleName}`;
|
||||
const analysisUrl = `${baseUrl}/${moduleName}.json`;
|
||||
const response = await fetch(codeUrl);
|
||||
const analysisResponse = await fetch(analysisUrl);
|
||||
const analysis = await analysisResponse.json();
|
||||
const exports = analysis.exports as string[];
|
||||
const code = await response.text();
|
||||
const moduleCompartment = new Compartment(
|
||||
createOrGetGlobalThis(
|
||||
pluginName,
|
||||
// use singleton here to avoid infinite loop
|
||||
createOrGetDynamicImport(pluginName, baseUrl)
|
||||
)
|
||||
);
|
||||
const entryPoint = moduleCompartment.evaluate(code, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
const moduleExports = {} as Record<string, any>;
|
||||
const setVarProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, p: string): any {
|
||||
return (newValue: any) => {
|
||||
moduleExports[p] = newValue;
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
entryPoint({
|
||||
imports: createImports(pluginName),
|
||||
liveVar: setVarProxy,
|
||||
onceVar: setVarProxy,
|
||||
});
|
||||
importLogger.debug('import', moduleName, exports, moduleExports);
|
||||
return moduleExports;
|
||||
};
|
||||
dynamicImportMap.set(pluginName, dynamicImport);
|
||||
return dynamicImport;
|
||||
};
|
||||
|
||||
const globalThisMap = new Map<string, any>();
|
||||
|
||||
export const createOrGetGlobalThis = (
|
||||
pluginName: string,
|
||||
dynamicImport: (moduleName: string) => Promise<any>
|
||||
) => {
|
||||
if (globalThisMap.has(pluginName)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return globalThisMap.get(pluginName)!;
|
||||
}
|
||||
const pluginGlobalThis = Object.assign(
|
||||
Object.create(null),
|
||||
sharedGlobalThis,
|
||||
{
|
||||
process: Object.freeze({
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
},
|
||||
}),
|
||||
// dynamic import function
|
||||
[dynamicImportKey]: dynamicImport,
|
||||
// UNSAFE: React will read `window` and `document`
|
||||
window: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, key) {
|
||||
permissionLogger.debug(`${pluginName} is accessing window`, key);
|
||||
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
|
||||
const result = Reflect.get(window, key);
|
||||
if (typeof result === 'function') {
|
||||
return function (...args: any[]) {
|
||||
permissionLogger.debug(
|
||||
`${pluginName} is calling window`,
|
||||
key,
|
||||
args
|
||||
);
|
||||
return result.apply(window, args);
|
||||
};
|
||||
}
|
||||
permissionLogger.debug('window', key, result);
|
||||
return result;
|
||||
},
|
||||
}
|
||||
),
|
||||
document: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, key) {
|
||||
permissionLogger.debug(`${pluginName} is accessing document`, key);
|
||||
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
|
||||
const result = Reflect.get(document, key);
|
||||
if (typeof result === 'function') {
|
||||
return function (...args: any[]) {
|
||||
permissionLogger.debug(
|
||||
`${pluginName} is calling window`,
|
||||
key,
|
||||
args
|
||||
);
|
||||
return result.apply(document, args);
|
||||
};
|
||||
}
|
||||
permissionLogger.debug('document', key, result);
|
||||
return result;
|
||||
},
|
||||
}
|
||||
),
|
||||
navigator: {
|
||||
userAgent: navigator.userAgent,
|
||||
},
|
||||
|
||||
// safe to use for all plugins
|
||||
Error: globalThis.Error,
|
||||
TypeError: globalThis.TypeError,
|
||||
RangeError: globalThis.RangeError,
|
||||
console: globalThis.console,
|
||||
crypto: globalThis.crypto,
|
||||
|
||||
// copilot uses these
|
||||
CustomEvent: globalThis.CustomEvent,
|
||||
Date: globalThis.Date,
|
||||
Math: globalThis.Math,
|
||||
URL: globalThis.URL,
|
||||
URLSearchParams: globalThis.URLSearchParams,
|
||||
Headers: globalThis.Headers,
|
||||
TextEncoder: globalThis.TextEncoder,
|
||||
TextDecoder: globalThis.TextDecoder,
|
||||
Request: globalThis.Request,
|
||||
|
||||
// image-preview uses these
|
||||
Blob: globalThis.Blob,
|
||||
ClipboardItem: globalThis.ClipboardItem,
|
||||
|
||||
// fixme: use our own db api
|
||||
indexedDB: globalThis.indexedDB,
|
||||
IDBRequest: globalThis.IDBRequest,
|
||||
IDBDatabase: globalThis.IDBDatabase,
|
||||
IDBCursorWithValue: globalThis.IDBCursorWithValue,
|
||||
IDBFactory: globalThis.IDBFactory,
|
||||
IDBKeyRange: globalThis.IDBKeyRange,
|
||||
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
|
||||
IDBTransaction: globalThis.IDBTransaction,
|
||||
IDBObjectStore: globalThis.IDBObjectStore,
|
||||
IDBIndex: globalThis.IDBIndex,
|
||||
IDBCursor: globalThis.IDBCursor,
|
||||
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
|
||||
}
|
||||
);
|
||||
globalThisMap.set(pluginName, pluginGlobalThis);
|
||||
return pluginGlobalThis;
|
||||
};
|
||||
|
||||
export const setupPluginCode = async (
|
||||
baseUrl: string,
|
||||
pluginName: string,
|
||||
filename: string
|
||||
) => {
|
||||
if (!_pluginNestedImportsMap.has(pluginName)) {
|
||||
_pluginNestedImportsMap.set(pluginName, new Map());
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
|
||||
const isMissingPackage = (name: string) =>
|
||||
_rootImportsMap.has(name) && !currentImportMap.has(name);
|
||||
|
||||
const bundleAnalysis = await fetch(`${baseUrl}/${filename}.json`).then(res =>
|
||||
res.json()
|
||||
);
|
||||
const moduleExports = bundleAnalysis.exports as Record<string, [string]>;
|
||||
const moduleImports = bundleAnalysis.imports as string[];
|
||||
const moduleReexports = bundleAnalysis.reexports as Record<
|
||||
string,
|
||||
[localName: string, exportedName: string][]
|
||||
>;
|
||||
await Promise.all(
|
||||
moduleImports.map(name => {
|
||||
if (isMissingPackage(name)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
importLogger.debug('missing package', name);
|
||||
return setupPluginCode(baseUrl, pluginName, name);
|
||||
}
|
||||
})
|
||||
);
|
||||
const code = await fetch(`${baseUrl}/${filename.replace(/^\.\//, '')}`).then(
|
||||
res => res.text()
|
||||
);
|
||||
importLogger.debug('evaluating', filename);
|
||||
const moduleCompartment = new Compartment(
|
||||
createOrGetGlobalThis(
|
||||
pluginName,
|
||||
// use singleton here to avoid infinite loop
|
||||
createOrGetDynamicImport(baseUrl, pluginName)
|
||||
)
|
||||
);
|
||||
const entryPoint = moduleCompartment.evaluate(code, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
const moduleExportsMap = new Map<string, any>();
|
||||
const setVarProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, p: string): any {
|
||||
return (newValue: any) => {
|
||||
moduleExportsMap.set(p, newValue);
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
currentImportMap.set(filename, moduleExportsMap);
|
||||
entryPoint({
|
||||
imports: createImports(pluginName),
|
||||
liveVar: setVarProxy,
|
||||
onceVar: setVarProxy,
|
||||
});
|
||||
|
||||
for (const [newExport, [originalExport]] of Object.entries(moduleExports)) {
|
||||
if (newExport === originalExport) continue;
|
||||
const value = moduleExportsMap.get(originalExport);
|
||||
moduleExportsMap.set(newExport, value);
|
||||
moduleExportsMap.delete(originalExport);
|
||||
}
|
||||
|
||||
for (const [name, reexports] of Object.entries(moduleReexports)) {
|
||||
const targetExports = currentImportMap.get(filename);
|
||||
const moduleExports = currentImportMap.get(name);
|
||||
assertExists(targetExports);
|
||||
assertExists(moduleExports);
|
||||
for (const [exportedName, localName] of reexports) {
|
||||
const exportedValue: any = moduleExports.get(exportedName);
|
||||
assertExists(exportedValue);
|
||||
targetExports.set(localName, exportedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const PluginProvider = ({ children }: PropsWithChildren) =>
|
||||
createElement(
|
||||
Provider,
|
||||
{
|
||||
store: rootStore,
|
||||
},
|
||||
children
|
||||
);
|
||||
|
||||
const group = new DisposableGroup();
|
||||
const entryLogger = new DebugLogger('plugin:entry');
|
||||
|
||||
export const evaluatePluginEntry = (pluginName: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
|
||||
const pluginExports = currentImportMap.get('index.js');
|
||||
assertExists(pluginExports);
|
||||
const entryFunction = pluginExports.get('entry');
|
||||
const cleanup = entryFunction(<PluginContext>{
|
||||
register: (part, callback) => {
|
||||
entryLogger.info(`Registering ${pluginName} to ${part}`);
|
||||
if (part === 'headerItem') {
|
||||
rootStore.set(headerItemsAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['headerItem'],
|
||||
}));
|
||||
} else if (part === 'editor') {
|
||||
rootStore.set(editorItemsAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['editor'],
|
||||
}));
|
||||
} else if (part === 'window') {
|
||||
rootStore.set(windowItemsAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['window'],
|
||||
}));
|
||||
} else if (part === 'setting') {
|
||||
rootStore.set(settingItemsAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['setting'],
|
||||
}));
|
||||
} else if (part === 'formatBar') {
|
||||
FormatQuickBar.customElements.push((page, getBlockRange) => {
|
||||
const div = document.createElement('div');
|
||||
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
|
||||
return div;
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unknown part: ${part}`);
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
PluginProvider,
|
||||
},
|
||||
});
|
||||
if (typeof cleanup !== 'function') {
|
||||
throw new Error('Plugin entry must return a function');
|
||||
}
|
||||
group.add(cleanup);
|
||||
};
|
||||
@@ -1,190 +1,77 @@
|
||||
/// <reference types="@types/webpack-env" />
|
||||
import 'ses';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
|
||||
|
||||
import * as AFFiNEComponent from '@affine/component';
|
||||
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
|
||||
import * as Icons from '@blocksuite/icons';
|
||||
import type {
|
||||
CallbackMap,
|
||||
PluginContext,
|
||||
} from '@toeverything/plugin-infra/entry';
|
||||
import * as Manager from '@toeverything/plugin-infra/manager';
|
||||
import {
|
||||
editorItemsAtom,
|
||||
headerItemsAtom,
|
||||
registeredPluginAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import * as Jotai from 'jotai';
|
||||
import { Provider } from 'jotai/react';
|
||||
import * as JotaiUtils from 'jotai/utils';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import * as React from 'react';
|
||||
import * as ReactJSXRuntime from 'react/jsx-runtime';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import * as ReactDomClient from 'react-dom/client';
|
||||
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
|
||||
|
||||
if (!process.env.COVERAGE) {
|
||||
lockdown({
|
||||
evalTaming: 'unsafeEval',
|
||||
overrideTaming: 'severe',
|
||||
consoleTaming: 'unsafe',
|
||||
errorTaming: 'unsafe',
|
||||
errorTrapping: 'platform',
|
||||
unhandledRejectionTrapping: 'report',
|
||||
});
|
||||
const builtinPluginUrl = new Set([
|
||||
'/plugins/bookmark',
|
||||
'/plugins/copilot',
|
||||
'/plugins/hello-world',
|
||||
'/plugins/image-preview',
|
||||
]);
|
||||
|
||||
const logger = new DebugLogger('register-plugins');
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __pluginPackageJson__: unknown[];
|
||||
}
|
||||
|
||||
const PluginProvider = ({ children }: PropsWithChildren) =>
|
||||
React.createElement(
|
||||
Provider,
|
||||
{
|
||||
store: rootStore,
|
||||
},
|
||||
children
|
||||
);
|
||||
globalThis.__pluginPackageJson__ = [];
|
||||
|
||||
const customRequire = (id: string) => {
|
||||
if (id === '@toeverything/plugin-infra/manager') {
|
||||
return Manager;
|
||||
}
|
||||
if (id === 'react') {
|
||||
return React;
|
||||
}
|
||||
if (id === 'react/jsx-runtime') {
|
||||
return ReactJSXRuntime;
|
||||
}
|
||||
if (id === 'react-dom') {
|
||||
return ReactDom;
|
||||
}
|
||||
if (id === 'react-dom/client') {
|
||||
return ReactDomClient;
|
||||
}
|
||||
if (id === '@blocksuite/icons') {
|
||||
return Icons;
|
||||
}
|
||||
if (id === '@affine/component') {
|
||||
return AFFiNEComponent;
|
||||
}
|
||||
if (id === '@blocksuite/blocks/std') {
|
||||
return BlockSuiteBlocksStd;
|
||||
}
|
||||
if (id === '@blocksuite/global/utils') {
|
||||
return BlockSuiteGlobalUtils;
|
||||
}
|
||||
if (id === 'jotai') {
|
||||
return Jotai;
|
||||
}
|
||||
if (id === 'jotai/utils') {
|
||||
return JotaiUtils;
|
||||
}
|
||||
if (id === '../plugin.js') {
|
||||
return entryCompartment.evaluate('exports');
|
||||
}
|
||||
throw new Error(`Cannot find module '${id}'`);
|
||||
};
|
||||
|
||||
const createGlobalThis = () => {
|
||||
return {
|
||||
process: Object.freeze({
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
},
|
||||
}),
|
||||
// UNSAFE: React will read `window` and `document`
|
||||
window,
|
||||
document,
|
||||
navigator,
|
||||
userAgent: navigator.userAgent,
|
||||
|
||||
// fixme: use our own db api
|
||||
indexedDB: globalThis.indexedDB,
|
||||
IDBRequest: globalThis.IDBRequest,
|
||||
IDBDatabase: globalThis.IDBDatabase,
|
||||
IDBCursorWithValue: globalThis.IDBCursorWithValue,
|
||||
IDBFactory: globalThis.IDBFactory,
|
||||
IDBKeyRange: globalThis.IDBKeyRange,
|
||||
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
|
||||
IDBTransaction: globalThis.IDBTransaction,
|
||||
IDBObjectStore: globalThis.IDBObjectStore,
|
||||
IDBIndex: globalThis.IDBIndex,
|
||||
IDBCursor: globalThis.IDBCursor,
|
||||
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
|
||||
|
||||
exports: {},
|
||||
console: globalThis.console,
|
||||
require: customRequire,
|
||||
};
|
||||
};
|
||||
|
||||
const group = new DisposableGroup();
|
||||
const pluginList = await (
|
||||
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
|
||||
).json();
|
||||
const builtInPlugins: string[] = pluginList.map((plugin: any) => plugin.name);
|
||||
const pluginGlobalThis = createGlobalThis();
|
||||
const pluginEntry = await fetch('/plugins/plugin.js').then(res => res.text());
|
||||
const entryCompartment = new Compartment(pluginGlobalThis, {});
|
||||
entryCompartment.evaluate(pluginEntry, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
await Promise.all(
|
||||
builtInPlugins.map(plugin => {
|
||||
const pluginCompartment = new Compartment(createGlobalThis(), {});
|
||||
const pluginGlobalThis = pluginCompartment.globalThis;
|
||||
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
|
||||
const packageJsonURL = new URL('package.json', baseURL);
|
||||
return fetch(packageJsonURL).then(async res => {
|
||||
const packageJson = await res.json();
|
||||
const pluginConfig = packageJson['affinePlugin'];
|
||||
if (
|
||||
pluginConfig.release === false &&
|
||||
process.env.NODE_ENV !== 'development'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
|
||||
const coreEntry = new URL(pluginConfig.entry.core, baseURL.toString());
|
||||
const codeText = await fetch(coreEntry).then(res => res.text());
|
||||
pluginCompartment.evaluate(codeText);
|
||||
pluginGlobalThis.__INTERNAL__ENTRY = {
|
||||
register: (part, callback) => {
|
||||
if (part === 'headerItem') {
|
||||
rootStore.set(headerItemsAtom, items => ({
|
||||
...items,
|
||||
[plugin]: callback as CallbackMap['headerItem'],
|
||||
}));
|
||||
} else if (part === 'editor') {
|
||||
rootStore.set(editorItemsAtom, items => ({
|
||||
...items,
|
||||
[plugin]: callback as CallbackMap['editor'],
|
||||
}));
|
||||
} else if (part === 'window') {
|
||||
rootStore.set(windowItemsAtom, items => ({
|
||||
...items,
|
||||
[plugin]: callback as CallbackMap['window'],
|
||||
}));
|
||||
} else {
|
||||
throw new Error(`Unknown part: ${part}`);
|
||||
export const pluginRegisterPromise = Promise.all(
|
||||
[...builtinPluginUrl].map(url => {
|
||||
return fetch(`${url}/package.json`)
|
||||
.then(async res => {
|
||||
const packageJson = await res.json();
|
||||
const {
|
||||
name: pluginName,
|
||||
affinePlugin: {
|
||||
release,
|
||||
entry: { core },
|
||||
assets,
|
||||
},
|
||||
} = packageJson;
|
||||
globalThis.__pluginPackageJson__.push(packageJson);
|
||||
logger.debug(`registering plugin ${pluginName}`);
|
||||
logger.debug(`package.json: ${packageJson}`);
|
||||
if (!release && !runtimeConfig.enablePlugin) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const baseURL = url;
|
||||
const entryURL = `${baseURL}/${core}`;
|
||||
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
|
||||
await setupPluginCode(baseURL, pluginName, core);
|
||||
console.log(`prepareImports for ${pluginName} done`);
|
||||
await fetch(entryURL).then(async () => {
|
||||
if (assets.length > 0) {
|
||||
await Promise.all(
|
||||
assets.map(async (asset: string) => {
|
||||
if (asset.endsWith('.css')) {
|
||||
const res = await fetch(`${baseURL}/${asset}`);
|
||||
if (res.ok) {
|
||||
// todo: how to put css file into sandbox?
|
||||
return res.text().then(text => {
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('plugin-id', pluginName);
|
||||
style.textContent = text;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
PluginProvider,
|
||||
},
|
||||
} satisfies PluginContext;
|
||||
const dispose = pluginCompartment.evaluate(
|
||||
'exports.entry(__INTERNAL__ENTRY)'
|
||||
);
|
||||
if (typeof dispose !== 'function') {
|
||||
throw new Error('Plugin entry must return a function');
|
||||
}
|
||||
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
|
||||
group.add(dispose);
|
||||
});
|
||||
evaluatePluginEntry(pluginName);
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(`error when fetch plugin from ${url}`, e);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
console.log('register plugins finished');
|
||||
).then(() => {
|
||||
console.info('All plugins loaded');
|
||||
});
|
||||
|
||||
180
apps/core/src/bootstrap/setup.ts
Normal file
180
apps/core/src/bootstrap/setup.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
migrateDatabaseBlockTo3,
|
||||
migrateToSubdoc,
|
||||
} from '@affine/env/blocksuite';
|
||||
import { setupGlobal } from '@affine/env/global';
|
||||
import type {
|
||||
LocalIndexedDBDownloadProvider,
|
||||
WorkspaceAdapter,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import {
|
||||
type RootWorkspaceMetadataV2,
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import {
|
||||
migrateLocalBlobStorage,
|
||||
upgradeV1ToV2,
|
||||
} from '@affine/workspace/migration';
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { rootStore } from '@toeverything/infra/atom';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
async function tryMigration() {
|
||||
const value = localStorage.getItem('jotai-workspaces');
|
||||
if (value) {
|
||||
try {
|
||||
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
|
||||
const promises: Promise<void>[] = [];
|
||||
const newMetadata = [...metadata];
|
||||
metadata.forEach(oldMeta => {
|
||||
if (!('version' in oldMeta)) {
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
assertExists(adapter);
|
||||
const upgrade = async () => {
|
||||
const workspace = await adapter.CRUD.get(oldMeta.id);
|
||||
if (!workspace) {
|
||||
console.warn('cannot find workspace', oldMeta.id);
|
||||
return;
|
||||
}
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
const doc = workspace.blockSuiteWorkspace.doc;
|
||||
const provider = createIndexedDBDownloadProvider(
|
||||
workspace.id,
|
||||
doc,
|
||||
{
|
||||
awareness:
|
||||
workspace.blockSuiteWorkspace.awarenessStore.awareness,
|
||||
}
|
||||
) as LocalIndexedDBDownloadProvider;
|
||||
provider.sync();
|
||||
await provider.whenReady;
|
||||
const newDoc = migrateToSubdoc(doc);
|
||||
if (doc === newDoc) {
|
||||
console.log('doc not changed');
|
||||
return;
|
||||
}
|
||||
const newWorkspace = upgradeV1ToV2(workspace);
|
||||
await migrateDatabaseBlockTo3(newWorkspace.blockSuiteWorkspace.doc);
|
||||
|
||||
const newId = await adapter.CRUD.create(
|
||||
newWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
await adapter.CRUD.delete(workspace as any);
|
||||
console.log('migrated', oldMeta.id, newId);
|
||||
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
id: newId,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
};
|
||||
await migrateLocalBlobStorage(workspace.id, newId);
|
||||
console.log('migrate to v2');
|
||||
};
|
||||
|
||||
// create a new workspace and push it to metadata
|
||||
promises.push(upgrade());
|
||||
} else if (oldMeta.version < WorkspaceVersion.DatabaseV3) {
|
||||
console.log('migrate to v3');
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
assertExists(adapter);
|
||||
promises.push(
|
||||
(async () => {
|
||||
const workspace = await adapter.CRUD.get(oldMeta.id);
|
||||
if (workspace) {
|
||||
const provider = createIndexedDBDownloadProvider(
|
||||
workspace.id,
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
{
|
||||
awareness:
|
||||
workspace.blockSuiteWorkspace.awarenessStore.awareness,
|
||||
}
|
||||
) as LocalIndexedDBDownloadProvider;
|
||||
provider.sync();
|
||||
await provider.whenReady;
|
||||
await migrateDatabaseBlockTo3(
|
||||
workspace.blockSuiteWorkspace.doc
|
||||
);
|
||||
}
|
||||
const index = newMetadata.findIndex(
|
||||
meta => meta.id === oldMeta.id
|
||||
);
|
||||
console.log('migrate to v3');
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
};
|
||||
})()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
console.log('migration done');
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('migration failed');
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
|
||||
window.dispatchEvent(new CustomEvent('migration-done'));
|
||||
window.$migrationDone = true;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('error when migrating data', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createFirstAppData() {
|
||||
const createFirst = (): RootWorkspaceMetadataV2[] => {
|
||||
const Plugins = Object.values(WorkspaceAdapters).sort(
|
||||
(a, b) => a.loadPriority - b.loadPriority
|
||||
);
|
||||
|
||||
return Plugins.flatMap(Plugin => {
|
||||
return Plugin.Events['app:init']?.().map(
|
||||
id =>
|
||||
<RootWorkspaceMetadataV2>{
|
||||
id,
|
||||
flavour: Plugin.flavour,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
}
|
||||
);
|
||||
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
|
||||
};
|
||||
if (localStorage.getItem('is-first-open') !== null) {
|
||||
return;
|
||||
}
|
||||
const result = createFirst();
|
||||
console.info('create first workspace', result);
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
rootStore.set(rootWorkspacesMetadataAtom, result);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
rootStore.set(
|
||||
workspaceAdaptersAtom,
|
||||
WorkspaceAdapters as Record<
|
||||
WorkspaceFlavour,
|
||||
WorkspaceAdapter<WorkspaceFlavour>
|
||||
>
|
||||
);
|
||||
|
||||
console.log('setup global');
|
||||
setupGlobal();
|
||||
|
||||
createFirstAppData();
|
||||
await tryMigration();
|
||||
await rootStore.get(rootWorkspacesMetadataAtom);
|
||||
console.log('setup done');
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
rootStore,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
||||
|
||||
@@ -79,7 +79,7 @@ const NameWorkspaceContent = ({
|
||||
<div className={style.buttonGroup}>
|
||||
<Button
|
||||
data-testid="create-workspace-close-button"
|
||||
type="light"
|
||||
type="primary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t.Cancel()}
|
||||
@@ -155,7 +155,7 @@ const SetDBLocationContent = ({
|
||||
<Button
|
||||
disabled={opening}
|
||||
data-testid="create-workspace-customize-button"
|
||||
type="light"
|
||||
type="primary"
|
||||
onClick={handleSelectDBFileLocation}
|
||||
>
|
||||
{t['Customize']()}
|
||||
|
||||
@@ -47,7 +47,7 @@ const LanguageMenuContent: FC<{
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LanguageMenu: FC<{ triggerProps: ButtonProps }> = ({
|
||||
export const LanguageMenu: FC<{ triggerProps?: ButtonProps }> = ({
|
||||
triggerProps,
|
||||
}) => {
|
||||
const i18n = useI18N();
|
||||
|
||||
@@ -92,15 +92,15 @@ export const WorkspaceDeleteModal = ({
|
||||
/>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
<Button onClick={onClose} size="large">
|
||||
{t['Cancel']()}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="delete-workspace-confirm-button"
|
||||
disabled={!allowDelete}
|
||||
onClick={handleDelete}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
size="large"
|
||||
type="error"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Delete']()}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const StyledModalWrapper = styled('div')(() => {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '560px',
|
||||
background: 'var(--affine-white)',
|
||||
background: 'var(--affine-background-overlay-panel-color)',
|
||||
borderRadius: '12px',
|
||||
// height: '312px',
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
type="danger"
|
||||
type="error"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
import { Button, toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { FC } from 'react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
|
||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && isDesktop) {
|
||||
const bs = workspace.blockSuiteWorkspace.blobs;
|
||||
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
|
||||
const blobsInStorage = await bs.list();
|
||||
const blobsToSync = blobsInStorage.filter(
|
||||
blob => !blobsInDb.includes(blob)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
blobsToSync.map(async blobKey => {
|
||||
const blob = await bs.get(blobKey);
|
||||
if (blob) {
|
||||
const bin = new Uint8Array(await blob.arrayBuffer());
|
||||
await window.apis.db.addBlob(workspace.id, blobKey, bin);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ExportPanel: FC<{
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
const workspaceId = workspace.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const onExport = useCallback(async () => {
|
||||
await syncBlobsToSqliteDb(workspace);
|
||||
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
}, [t, workspace, workspaceId]);
|
||||
return (
|
||||
<>
|
||||
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
|
||||
<Button
|
||||
size="small"
|
||||
data-testid="export-affine-backup"
|
||||
onClick={async () => {
|
||||
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button data-testid="export-affine-backup" onClick={onExport}>
|
||||
{t['Export']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
|
||||
@@ -84,13 +84,12 @@ export const ProfilePanel: FC<{
|
||||
/>
|
||||
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
|
||||
<IconButton
|
||||
size="middle"
|
||||
data-testid="save-workspace-name"
|
||||
onClick={() => {
|
||||
handleUpdateWorkspaceName(input);
|
||||
}}
|
||||
active={true}
|
||||
style={{
|
||||
color: 'var(--affine-primary-color)',
|
||||
marginLeft: '12px',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -74,7 +74,7 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Button
|
||||
className={style.urlButton}
|
||||
size="middle"
|
||||
size="large"
|
||||
onClick={useCallback(() => {
|
||||
window.open(shareUrl, '_blank');
|
||||
}, [shareUrl])}
|
||||
@@ -82,7 +82,7 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
>
|
||||
{shareUrl}
|
||||
</Button>
|
||||
<Button size="middle" onClick={copyUrl}>
|
||||
<Button size="large" onClick={copyUrl}>
|
||||
{t['Copy']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
|
||||
@@ -79,14 +79,13 @@ export const StoragePanel: FC<{
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
className={style.urlButton}
|
||||
size="middle"
|
||||
size="large"
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
{secondaryPath}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
data-testid="reveal-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={onRevealDBFile}
|
||||
@@ -96,7 +95,6 @@ export const StoragePanel: FC<{
|
||||
</FlexWrapper>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
data-testid="move-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={handleMoveTo}
|
||||
|
||||
@@ -61,10 +61,7 @@ export const AboutAffine = () => {
|
||||
desc={t['View the AFFiNE Changelog.']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://affine.pro/blog/what-is-new-affine-0717',
|
||||
'_blank'
|
||||
);
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
|
||||
@@ -32,11 +32,7 @@ export const ThemeSettings = () => {
|
||||
[setTheme]
|
||||
)}
|
||||
>
|
||||
<RadioButton
|
||||
bold={true}
|
||||
value="system"
|
||||
data-testid="system-theme-trigger"
|
||||
>
|
||||
<RadioButton value="system" data-testid="system-theme-trigger">
|
||||
{t['system']()}
|
||||
</RadioButton>
|
||||
<RadioButton bold={true} value="light" data-testid="light-theme-trigger">
|
||||
@@ -117,7 +113,7 @@ export const AppearanceSettings = () => {
|
||||
desc={t['Select the language for the interface.']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<LanguageMenu triggerProps={{ size: 'small' }} />
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
</SettingRow>
|
||||
{environment.isDesktop ? (
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { registeredPluginAtom } from '@toeverything/plugin-infra/manager';
|
||||
import {
|
||||
registeredPluginAtom,
|
||||
settingItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { pluginItem } from './style.css';
|
||||
|
||||
const PluginSettingWrapper: FC<{
|
||||
id: string;
|
||||
title?: ReactNode;
|
||||
}> = ({ title, id }) => {
|
||||
const Setting = useAtomValue(settingItemsAtom)[id];
|
||||
const disposeRef = useRef<(() => void) | null>(null);
|
||||
return (
|
||||
<div>
|
||||
{title ? <div className="title">{title}</div> : null}
|
||||
<div
|
||||
ref={ref => {
|
||||
if (ref && Setting) {
|
||||
setTimeout(() => {
|
||||
disposeRef.current = Setting(ref);
|
||||
});
|
||||
} else if (ref === null) {
|
||||
setTimeout(() => {
|
||||
disposeRef.current?.();
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Plugins = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
||||
console.log('allowedPlugins', allowedPlugins);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -18,7 +47,9 @@ export const Plugins = () => {
|
||||
data-testid="plugins-title"
|
||||
/>
|
||||
{allowedPlugins.map(plugin => (
|
||||
<SettingWrapper key={plugin} title={plugin}></SettingWrapper>
|
||||
<div className={pluginItem} key={plugin}>
|
||||
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,3 +7,10 @@ export const settingWrapper = style({
|
||||
minWidth: '150px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
|
||||
export const pluginItem = style({
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
transition: '0.3s',
|
||||
padding: '24px 8px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ScrollableContainer, Tooltip } from '@affine/component';
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
@@ -7,7 +8,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
@@ -19,6 +20,7 @@ import type {
|
||||
GeneralSettingList,
|
||||
} from '../general-setting';
|
||||
import {
|
||||
currentWorkspaceLabel,
|
||||
settingSlideBar,
|
||||
sidebarItemsWrapper,
|
||||
sidebarSelectItem,
|
||||
@@ -74,10 +76,12 @@ export const SettingSidebar: FC<{
|
||||
</div>
|
||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
<ScrollableContainer>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,9 +137,18 @@ const WorkspaceListItem = ({
|
||||
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
|
||||
<span className="setting-name">{workspaceName}</span>
|
||||
{isCurrent ? (
|
||||
<div className="current-label" data-testid="current-workspace-label">
|
||||
Current
|
||||
</div>
|
||||
<Tooltip
|
||||
content="Current"
|
||||
title="Current"
|
||||
offset={[0, -5]}
|
||||
placement="top"
|
||||
disablePortal={false}
|
||||
>
|
||||
<div
|
||||
className={currentWorkspaceLabel}
|
||||
data-testid="current-workspace-label"
|
||||
></div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ export const settingSlideBar = style({
|
||||
width: '25%',
|
||||
maxWidth: '242px',
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
padding: '20px 16px',
|
||||
padding: '20px 0px',
|
||||
height: '100%',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
@@ -15,14 +15,14 @@ export const sidebarTitle = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: '600',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
paddingLeft: '8px',
|
||||
padding: '0px 16px 0px 24px',
|
||||
});
|
||||
|
||||
export const sidebarSubtitle = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
paddingLeft: '8px',
|
||||
padding: '0px 16px 0px 24px',
|
||||
marginTop: '20px',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
@@ -34,7 +34,7 @@ export const sidebarItemsWrapper = style({
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
overflowY: 'hidden',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -42,9 +42,9 @@ export const sidebarItemsWrapper = style({
|
||||
export const sidebarSelectItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
margin: '0px 16px 4px 16px',
|
||||
padding: '0px 8px',
|
||||
height: '30px',
|
||||
marginBottom: '4px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
@@ -75,20 +75,21 @@ globalStyle(`${settingSlideBar} .setting-name`, {
|
||||
whiteSpace: 'nowrap',
|
||||
flexGrow: 1,
|
||||
});
|
||||
globalStyle(`${settingSlideBar} .current-label`, {
|
||||
export const currentWorkspaceLabel = style({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '0 5px',
|
||||
// TODO: use color variable
|
||||
background: '#1E96EB',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: '600',
|
||||
color: 'var(--affine-white)',
|
||||
marginLeft: '10px',
|
||||
flexShrink: 0,
|
||||
selectors: {
|
||||
'&::after': {
|
||||
content: '""',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--affine-blue)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const accountButton = style({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@ import { PageList, PageListTrashView } from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { type PageMeta, type Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { getPagePreviewText } from '@toeverything/hooks/use-block-suite-page-preview';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useBlockSuitePagePreview } from '@toeverything/hooks/use-block-suite-page-preview';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Suspense, useCallback, useMemo } from 'react';
|
||||
|
||||
import { allPageModeSelectAtom } from '../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
@@ -39,17 +41,49 @@ const filter = {
|
||||
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||
};
|
||||
|
||||
const PagePreviewInner = ({
|
||||
workspace,
|
||||
pageId,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
}) => {
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
assertExists(page);
|
||||
const previewAtom = useBlockSuitePagePreview(page);
|
||||
const preview = useAtomValue(previewAtom);
|
||||
return preview;
|
||||
};
|
||||
|
||||
const PagePreview = ({
|
||||
workspace,
|
||||
pageId,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
}) => {
|
||||
return (
|
||||
<Suspense>
|
||||
<PagePreviewInner workspace={workspace} pageId={pageId} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const PageListEmpty = (props: {
|
||||
createPage?: () => void;
|
||||
createPage?: ReturnType<typeof usePageHelper>['createPage'];
|
||||
listType: BlockSuitePageListProps['listType'];
|
||||
}) => {
|
||||
const { listType, createPage } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onCreatePage = useCallback(() => {
|
||||
createPage?.();
|
||||
}, [createPage]);
|
||||
|
||||
const getEmptyDescription = () => {
|
||||
if (listType === 'all') {
|
||||
const CreateNewPageButton = () => (
|
||||
<button className={emptyDescButton} onClick={createPage}>
|
||||
const createNewPageButton = (
|
||||
<button className={emptyDescButton} onClick={onCreatePage}>
|
||||
New Page
|
||||
</button>
|
||||
);
|
||||
@@ -57,7 +91,7 @@ const PageListEmpty = (props: {
|
||||
const shortcut = environment.isMacOs ? '⌘ + N' : 'Ctrl + N';
|
||||
return (
|
||||
<Trans i18nKey="emptyAllPagesClient">
|
||||
Click on the <CreateNewPageButton /> button Or press
|
||||
Click on the {createNewPageButton} button Or press
|
||||
<kbd className={emptyDescKbd}>{{ shortcut } as any}</kbd> to create
|
||||
your first page.
|
||||
</Trans>
|
||||
@@ -66,7 +100,7 @@ const PageListEmpty = (props: {
|
||||
return (
|
||||
<Trans i18nKey="emptyAllPages">
|
||||
Click on the
|
||||
<CreateNewPageButton />
|
||||
{createNewPageButton}
|
||||
button to create your first page.
|
||||
</Trans>
|
||||
);
|
||||
@@ -147,8 +181,6 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
|
||||
if (listType === 'trash') {
|
||||
const pageList: TrashListData[] = list.map(pageMeta => {
|
||||
const page = blockSuiteWorkspace.getPage(pageMeta.id);
|
||||
const preview = page ? getPagePreviewText(page) : undefined;
|
||||
return {
|
||||
icon: isPreferredEdgeless(pageMeta.id) ? (
|
||||
<EdgelessIcon />
|
||||
@@ -157,7 +189,9 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
),
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
preview,
|
||||
preview: (
|
||||
<PagePreview workspace={blockSuiteWorkspace} pageId={pageMeta.id} />
|
||||
),
|
||||
createDate: new Date(pageMeta.createDate),
|
||||
trashDate: pageMeta.trashDate
|
||||
? new Date(pageMeta.trashDate)
|
||||
@@ -186,12 +220,13 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
|
||||
const pageList: ListData[] = list.map(pageMeta => {
|
||||
const page = blockSuiteWorkspace.getPage(pageMeta.id);
|
||||
const preview = page ? getPagePreviewText(page) : undefined;
|
||||
return {
|
||||
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
preview,
|
||||
preview: (
|
||||
<PagePreview workspace={blockSuiteWorkspace} pageId={pageMeta.id} />
|
||||
),
|
||||
tags:
|
||||
page?.meta.tags?.map(id => tagOptionMap[id]).filter(v => v != null) ??
|
||||
[],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -15,15 +16,23 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
|
||||
[pageSettings]
|
||||
);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
const createPageAndOpen = useCallback(() => {
|
||||
const page = createPage();
|
||||
return openPage(blockSuiteWorkspace.id, page.id);
|
||||
}, [blockSuiteWorkspace.id, createPage, openPage]);
|
||||
const createEdgelessAndOpen = useCallback(() => {
|
||||
const page = createPage();
|
||||
setPageMode(page.id, 'edgeless');
|
||||
return openPage(blockSuiteWorkspace.id, page.id);
|
||||
}, [blockSuiteWorkspace.id, createPage, openPage, setPageMode]);
|
||||
const createPageAndOpen = useCallback(
|
||||
(id?: string, mode?: 'page' | 'edgeless') => {
|
||||
const page = createPage(id);
|
||||
initEmptyPage(page); // we don't need to wait it to be loaded right?
|
||||
if (mode) {
|
||||
setPageMode(page.id, mode);
|
||||
}
|
||||
openPage(blockSuiteWorkspace.id, page.id);
|
||||
},
|
||||
[blockSuiteWorkspace.id, createPage, openPage, setPageMode]
|
||||
);
|
||||
const createEdgelessAndOpen = useCallback(
|
||||
(id?: string) => {
|
||||
return createPageAndOpen(id, 'edgeless');
|
||||
},
|
||||
[createPageAndOpen]
|
||||
);
|
||||
const importFileAndOpen = useCallback(async () => {
|
||||
const { showImportModal } = await import('@blocksuite/blocks');
|
||||
showImportModal({ workspace: blockSuiteWorkspace });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,9 @@ export const EditorModeSwitch = ({
|
||||
assertExists(pageMeta);
|
||||
const { trash } = pageMeta;
|
||||
useEffect(() => {
|
||||
if (trash) {
|
||||
return;
|
||||
}
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if (
|
||||
!environment.isServer && environment.isMacOs
|
||||
@@ -64,7 +67,7 @@ export const EditorModeSwitch = ({
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [setSetting, t]);
|
||||
}, [setSetting, t, trash]);
|
||||
|
||||
return (
|
||||
<Tooltip content={<TooltipContent />}>
|
||||
@@ -77,6 +80,7 @@ export const EditorModeSwitch = ({
|
||||
data-testid="switch-page-mode-button"
|
||||
active={currentMode === 'page'}
|
||||
hide={trash && currentMode !== 'page'}
|
||||
trash={trash}
|
||||
onClick={() => {
|
||||
setSetting(setting => {
|
||||
if (setting?.mode !== 'page') {
|
||||
@@ -90,6 +94,7 @@ export const EditorModeSwitch = ({
|
||||
data-testid="switch-edgeless-mode-button"
|
||||
active={currentMode === 'edgeless'}
|
||||
hide={trash && currentMode !== 'edgeless'}
|
||||
trash={trash}
|
||||
onClick={() => {
|
||||
setSetting(setting => {
|
||||
if (setting?.mode !== 'edgeless') {
|
||||
|
||||
@@ -5,14 +5,15 @@ export const StyledEditorModeSwitch = styled('div')<{
|
||||
showAlone?: boolean;
|
||||
}>(({ switchLeft, showAlone }) => {
|
||||
return {
|
||||
width: showAlone ? '40px' : '78px',
|
||||
maxWidth: showAlone ? '40px' : '70px',
|
||||
gap: '8px',
|
||||
height: '32px',
|
||||
background: showAlone
|
||||
? 'transparent'
|
||||
: 'var(--affine-background-secondary-color)',
|
||||
borderRadius: '12px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
padding: '0 8px',
|
||||
padding: '4px 4px',
|
||||
position: 'relative',
|
||||
|
||||
'::after': {
|
||||
@@ -25,7 +26,7 @@ export const StyledEditorModeSwitch = styled('div')<{
|
||||
borderRadius: '8px',
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
transform: `translateX(${switchLeft ? '0' : '38px'})`,
|
||||
transform: `translateX(${switchLeft ? '0' : '32px'})`,
|
||||
transition: 'all .15s',
|
||||
},
|
||||
};
|
||||
@@ -34,14 +35,19 @@ export const StyledEditorModeSwitch = styled('div')<{
|
||||
export const StyledSwitchItem = styled('button')<{
|
||||
active?: boolean;
|
||||
hide?: boolean;
|
||||
}>(({ active = false, hide = false }) => {
|
||||
trash?: boolean;
|
||||
}>(({ active = false, hide = false, trash = false }) => {
|
||||
return {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '8px',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
boxShadow: active ? 'var(--affine-shadow-1)' : 'none',
|
||||
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
|
||||
color: active
|
||||
? trash
|
||||
? 'var(--affine-error-color)'
|
||||
: 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
display: hide ? 'none' : 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -49,7 +55,7 @@ export const StyledSwitchItem = styled('button')<{
|
||||
zIndex: 2,
|
||||
fontSize: '20px',
|
||||
path: {
|
||||
fill: 'currentColor',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,12 +8,14 @@ import { StyledSwitchItem } from './style';
|
||||
type HoverAnimateControllerProps = {
|
||||
active?: boolean;
|
||||
hide?: boolean;
|
||||
trash?: boolean;
|
||||
children: React.ReactElement;
|
||||
} & HTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
const HoverAnimateController = ({
|
||||
active,
|
||||
hide,
|
||||
trash,
|
||||
children,
|
||||
...props
|
||||
}: HoverAnimateControllerProps) => {
|
||||
@@ -22,6 +24,7 @@ const HoverAnimateController = ({
|
||||
<StyledSwitchItem
|
||||
hide={hide}
|
||||
active={active}
|
||||
trash={trash}
|
||||
onMouseEnter={() => {
|
||||
setStartAnimate(true);
|
||||
}}
|
||||
@@ -32,7 +35,7 @@ const HoverAnimateController = ({
|
||||
>
|
||||
{cloneElement(children, {
|
||||
isStopped: !startAnimate,
|
||||
speed: 5,
|
||||
speed: 1,
|
||||
width: 20,
|
||||
height: 20,
|
||||
})}
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { pageSettingFamily } from '../../../../atoms';
|
||||
@@ -24,7 +24,6 @@ import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suit
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { toast } from '../../../../utils';
|
||||
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
|
||||
import * as styles from '../styles.css';
|
||||
import { LanguageMenu } from './language-menu';
|
||||
const CommonMenu = () => {
|
||||
const content = (
|
||||
@@ -45,7 +44,7 @@ const CommonMenu = () => {
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
|
||||
<IconButton data-testid="editor-option-menu">
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
@@ -71,61 +70,56 @@ const PageMenu = () => {
|
||||
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const [openConfirm, setOpenConfirm] = useState(false);
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const handleFavorite = useCallback(() => {
|
||||
setPageMeta(pageId, { favorite: !favorite });
|
||||
toast(favorite ? t['Removed from Favorites']() : t['Added to Favorites']());
|
||||
}, [favorite, pageId, setPageMeta, t]);
|
||||
const handleSwitchMode = useCallback(() => {
|
||||
setSetting(setting => ({
|
||||
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
|
||||
}));
|
||||
toast(
|
||||
mode === 'page'
|
||||
? t['com.affine.edgelessMode']()
|
||||
: t['com.affine.pageMode']()
|
||||
);
|
||||
}, [mode, setSetting, t]);
|
||||
const handleOnConfirm = useCallback(() => {
|
||||
removeToTrash(pageMeta.id);
|
||||
toast(t['Moved to Trash']());
|
||||
setOpenConfirm(false);
|
||||
}, [pageMeta.id, removeToTrash, t]);
|
||||
|
||||
const EditMenu = (
|
||||
<>
|
||||
<>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={() => {
|
||||
setPageMeta(pageId, { favorite: !favorite });
|
||||
toast(
|
||||
favorite
|
||||
? t['Removed from Favorites']()
|
||||
: t['Added to Favorites']()
|
||||
);
|
||||
}}
|
||||
icon={
|
||||
favorite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={() => {
|
||||
setSetting(setting => ({
|
||||
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{t['Convert to ']()}
|
||||
{mode === 'page' ? t['Edgeless']() : t['Page']()}
|
||||
</MenuItem>
|
||||
<Export />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.horizontalDividerContainer}>
|
||||
<div className={styles.horizontalDivider} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={handleFavorite}
|
||||
icon={
|
||||
favorite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuThemeModeSwitch />
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={handleSwitchMode}
|
||||
>
|
||||
{t['Convert to ']()}
|
||||
{mode === 'page' ? t['Edgeless']() : t['Page']()}
|
||||
</MenuItem>
|
||||
<Export />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -138,18 +132,14 @@ const PageMenu = () => {
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
|
||||
<IconButton data-testid="editor-option-menu">
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={openConfirm}
|
||||
title={pageMeta.title}
|
||||
onConfirm={() => {
|
||||
removeToTrash(pageMeta.id);
|
||||
toast(t['Moved to Trash']());
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
onConfirm={handleOnConfirm}
|
||||
onCancel={() => {
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
|
||||
@@ -51,13 +51,13 @@ export const LanguageMenu: React.FC = () => {
|
||||
disablePortal={true}
|
||||
>
|
||||
<StyledButton
|
||||
type="plain"
|
||||
icon={
|
||||
<StyledArrowDownContainer>
|
||||
<ArrowDownSmallIcon />
|
||||
</StyledArrowDownContainer>
|
||||
}
|
||||
iconPosition="end"
|
||||
noBorder={true}
|
||||
data-testid="language-menu-button"
|
||||
>
|
||||
<StyledCurrentLanguage>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const group = style({
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
bottom: '100px',
|
||||
left: '0',
|
||||
display: 'flex',
|
||||
gap: '24px',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export const buttonContainer = style({
|
||||
boxShadow: 'var(--affine-float-button-shadow-2)',
|
||||
borderRadius: '8px',
|
||||
});
|
||||
@@ -3,14 +3,14 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||
|
||||
import { buttonContainer, group } from './styles.css';
|
||||
export const TrashButtonGroup = () => {
|
||||
// fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
@@ -29,32 +29,34 @@ export const TrashButtonGroup = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
bold={true}
|
||||
shape="round"
|
||||
style={{ marginRight: '24px' }}
|
||||
onClick={() => {
|
||||
restoreFromTrash(pageId);
|
||||
}}
|
||||
>
|
||||
{t['Restore it']()}
|
||||
</Button>
|
||||
<Button
|
||||
bold={true}
|
||||
shape="round"
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t['Delete permanently']()}
|
||||
</Button>
|
||||
<div className={group}>
|
||||
<div className={buttonContainer}>
|
||||
<Button
|
||||
type="processing"
|
||||
onClick={() => {
|
||||
restoreFromTrash(pageId);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{t['Restore it']()}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={buttonContainer}>
|
||||
<Button
|
||||
type="error"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{t['Delete permanently']()}
|
||||
</Button>
|
||||
</div>
|
||||
<Confirm
|
||||
title={t['TrashButtonGroupTitle']()}
|
||||
content={t['TrashButtonGroupDescription']()}
|
||||
confirmText={t['Delete']()}
|
||||
confirmType="danger"
|
||||
confirmType="error"
|
||||
open={open}
|
||||
onConfirm={useCallback(() => {
|
||||
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||
@@ -67,7 +69,7 @@ export const TrashButtonGroup = () => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { headerItemsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { headerItemsAtom } from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import DownloadClientTip from './download-tips';
|
||||
import { EditorOptionMenu } from './header-right-items/editor-option-menu';
|
||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||
import * as styles from './styles.css';
|
||||
import { OSWarningMessage, shouldShowWarning } from './utils';
|
||||
|
||||
@@ -38,7 +38,6 @@ export type BaseHeaderProps<
|
||||
|
||||
export enum HeaderRightItemName {
|
||||
EditorOptionMenu = 'editorOptionMenu',
|
||||
TrashButtonGroup = 'trashButtonGroup',
|
||||
// some windows only items
|
||||
WindowsAppControls = 'windowsAppControls',
|
||||
}
|
||||
@@ -56,16 +55,12 @@ type HeaderItem = {
|
||||
};
|
||||
|
||||
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
[HeaderRightItemName.TrashButtonGroup]: {
|
||||
Component: TrashButtonGroup,
|
||||
availableWhen: (_, currentPage) => {
|
||||
return currentPage?.meta.trash === true;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.EditorOptionMenu]: {
|
||||
Component: EditorOptionMenu,
|
||||
availableWhen: (_, currentPage, { isPublic }) => {
|
||||
return !isPublic;
|
||||
return (
|
||||
!isPublic && currentPage?.meta.trash !== true && currentPage !== null
|
||||
);
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.WindowsAppControls]: {
|
||||
@@ -160,10 +155,9 @@ export const Header = forwardRef<
|
||||
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
|
||||
>((props, ref) => {
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const [showDownloadTip, setShowDownloadTip] = useState(true);
|
||||
// const [shouldShowGuideDownloadClientTip] = useAtom(
|
||||
// guideDownloadClientTipAtom
|
||||
// );
|
||||
const [showDownloadTip, setShowDownloadTip] = useAtom(
|
||||
guideDownloadClientTipAtom
|
||||
);
|
||||
useEffect(() => {
|
||||
setShowWarning(shouldShowWarning());
|
||||
}, []);
|
||||
@@ -183,7 +177,10 @@ export const Header = forwardRef<
|
||||
{showDownloadTip ? (
|
||||
<DownloadClientTip
|
||||
show={showDownloadTip}
|
||||
onClose={() => setShowDownloadTip(false)}
|
||||
onClose={() => {
|
||||
setShowDownloadTip(false);
|
||||
localStorage.setItem('affine-is-dt-hide', '1');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<BrowserWarning
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type {
|
||||
FC,
|
||||
@@ -7,7 +11,7 @@ import type {
|
||||
PropsWithChildren,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../../../atoms';
|
||||
import { QuickSearchButton } from '../../pure/quick-search-button';
|
||||
@@ -27,10 +31,25 @@ export const BlockSuiteEditorHeader: FC<
|
||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isEditable) {
|
||||
setIsEditable(!isEditable);
|
||||
const value = inputRef.current?.value;
|
||||
if (value !== pageMeta?.title && currentPage) {
|
||||
pageTitleMeta.setPageTitle(currentPage?.id, value || '');
|
||||
}
|
||||
} else {
|
||||
setIsEditable(!isEditable);
|
||||
}
|
||||
}, [currentPage, isEditable, pageMeta?.title, pageTitleMeta]);
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
assertExists(pageMeta);
|
||||
const title = pageMeta.title;
|
||||
|
||||
const title = pageMeta?.title;
|
||||
return (
|
||||
<Header ref={headerRef} {...props}>
|
||||
{children}
|
||||
@@ -46,8 +65,38 @@ export const BlockSuiteEditorHeader: FC<
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>{title || 'Untitled'}</div>
|
||||
|
||||
<div>
|
||||
{isEditable ? (
|
||||
<div>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className={styles.title}
|
||||
type="text"
|
||||
data-testid="title-content"
|
||||
defaultValue={pageMeta?.title}
|
||||
onBlur={handleClick}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-testid="save-edit-button"
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
data-testid="title-edit-button"
|
||||
onClick={handleClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{title || 'Untitled'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.searchArrowWrapper}>
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import './page-detail-editor.css';
|
||||
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { CallbackMap, LayoutNode } from '@affine/sdk//entry';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import type { CallbackMap } from '@toeverything/plugin-infra/entry';
|
||||
import {
|
||||
affinePluginsAtom,
|
||||
contentLayoutAtom,
|
||||
editorItemsAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import type { AffinePlugin, LayoutNode } from '@toeverything/plugin-infra/type';
|
||||
} from '@toeverything/infra/atom';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { CSSProperties, FC, ReactElement } from 'react';
|
||||
@@ -25,6 +23,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import { TrashButtonGroup } from './blocksuite/workspace-header/header-right-items/trash-button-group';
|
||||
import * as styles from './page-detail-editor.css';
|
||||
import { pluginContainer } from './page-detail-editor.css';
|
||||
|
||||
@@ -67,62 +66,65 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
}, [appSettings.fontStyle]);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
className={clsx(styles.editor, {
|
||||
'full-screen': appSettings.fullWidthLayout,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
'--affine-font-family': value,
|
||||
} as CSSProperties
|
||||
}
|
||||
key={`${workspace.id}-${pageId}`}
|
||||
mode={isPublic ? 'page' : currentMode}
|
||||
page={page}
|
||||
onInit={useCallback(
|
||||
(page: Page, editor: Readonly<EditorContainer>) => {
|
||||
onInit(page, editor);
|
||||
},
|
||||
[onInit]
|
||||
)}
|
||||
setBlockHub={setBlockHub}
|
||||
onLoad={useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
page.workspace.setPageMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
localStorage.setItem('last_page_id', page.id);
|
||||
let dispose = () => {};
|
||||
if (onLoad) {
|
||||
dispose = onLoad(page, editor);
|
||||
}
|
||||
const editorItems = rootStore.get(editorItemsAtom);
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', id);
|
||||
const cleanup = editorItem(div, editor);
|
||||
assertExists(parent);
|
||||
document.body.appendChild(div);
|
||||
return () => {
|
||||
cleanup();
|
||||
document.body.removeChild(div);
|
||||
};
|
||||
<>
|
||||
<Editor
|
||||
className={clsx(styles.editor, {
|
||||
'full-screen': appSettings.fullWidthLayout,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
'--affine-font-family': value,
|
||||
} as CSSProperties
|
||||
}
|
||||
key={`${workspace.id}-${pageId}`}
|
||||
mode={isPublic ? 'page' : currentMode}
|
||||
page={page}
|
||||
onInit={useCallback(
|
||||
(page: Page, editor: Readonly<EditorContainer>) => {
|
||||
onInit(page, editor);
|
||||
},
|
||||
[onInit]
|
||||
)}
|
||||
setBlockHub={setBlockHub}
|
||||
onLoad={useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
page.workspace.setPageMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
localStorage.setItem('last_page_id', page.id);
|
||||
let dispose = () => {};
|
||||
if (onLoad) {
|
||||
dispose = onLoad(page, editor);
|
||||
}
|
||||
const editorItems = rootStore.get(editorItemsAtom);
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', id);
|
||||
const cleanup = editorItem(div, editor);
|
||||
assertExists(parent);
|
||||
document.body.appendChild(div);
|
||||
return () => {
|
||||
cleanup();
|
||||
document.body.removeChild(div);
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
dispose();
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
});
|
||||
};
|
||||
},
|
||||
[onLoad]
|
||||
)}
|
||||
/>
|
||||
return () => {
|
||||
dispose();
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
});
|
||||
};
|
||||
},
|
||||
[onLoad]
|
||||
)}
|
||||
/>
|
||||
{meta.trash && <TrashButtonGroup />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,7 +162,6 @@ const PluginContentAdapter = memo<{
|
||||
type LayoutPanelProps = {
|
||||
node: LayoutNode;
|
||||
editorProps: PageDetailEditorProps;
|
||||
plugins: AffinePlugin<string>[];
|
||||
};
|
||||
|
||||
const LayoutPanel = memo(function LayoutPanel(
|
||||
@@ -185,21 +186,18 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
>
|
||||
<Panel defaultSize={node.splitPercentage}>
|
||||
<Suspense>
|
||||
<LayoutPanel
|
||||
node={node.first}
|
||||
editorProps={props.editorProps}
|
||||
plugins={props.plugins}
|
||||
/>
|
||||
<LayoutPanel node={node.first} editorProps={props.editorProps} />
|
||||
</Suspense>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel defaultSize={100 - node.splitPercentage}>
|
||||
<Panel
|
||||
defaultSize={100 - node.splitPercentage}
|
||||
style={{
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
>
|
||||
<Suspense>
|
||||
<LayoutPanel
|
||||
node={node.second}
|
||||
editorProps={props.editorProps}
|
||||
plugins={props.plugins}
|
||||
/>
|
||||
<LayoutPanel node={node.second} editorProps={props.editorProps} />
|
||||
</Suspense>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
@@ -215,16 +213,11 @@ export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
|
||||
}
|
||||
|
||||
const layout = useAtomValue(contentLayoutAtom);
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
const plugins = useMemo(
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense>
|
||||
<LayoutPanel node={layout} editorProps={props} plugins={plugins} />
|
||||
<LayoutPanel node={layout} editorProps={props} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,8 +14,7 @@ export const Footer: FC = () => {
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
noBorder
|
||||
bold
|
||||
type="plain"
|
||||
icon={
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
@@ -9,23 +10,27 @@ import type { FC, SVGProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
import { pathGenerator } from '../../../shared';
|
||||
|
||||
export const useSwitchToConfig = (
|
||||
workspaceId: string
|
||||
): {
|
||||
title: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
}[] => {
|
||||
export type Config =
|
||||
| {
|
||||
title: string;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
subPath: WorkspaceSubPath;
|
||||
}
|
||||
| {
|
||||
title: string;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const useSwitchToConfig = (workspaceId: string): Config[] => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t['All pages'](),
|
||||
href: pathGenerator.all(workspaceId),
|
||||
subPath: WorkspaceSubPath.ALL,
|
||||
icon: FolderIcon,
|
||||
},
|
||||
{
|
||||
@@ -41,7 +46,7 @@ export const useSwitchToConfig = (
|
||||
},
|
||||
{
|
||||
title: t['Trash'](),
|
||||
href: pathGenerator.trash(workspaceId),
|
||||
subPath: WorkspaceSubPath.TRASH,
|
||||
icon: DeleteTemporarilyIcon,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Command } from 'cmdk';
|
||||
import { startTransition } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { Footer } from './footer';
|
||||
@@ -37,15 +36,7 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
_setQuery(query);
|
||||
});
|
||||
}, []);
|
||||
const location = useLocation();
|
||||
const isPublicWorkspace = useMemo(
|
||||
() => location.pathname.startsWith('/public-workspace'),
|
||||
[location]
|
||||
);
|
||||
const [showCreatePage, setShowCreatePage] = useState(true);
|
||||
const isPublicAndNoQuery = useCallback(() => {
|
||||
return isPublicWorkspace && query.length === 0;
|
||||
}, [isPublicWorkspace, query.length]);
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
@@ -88,7 +79,7 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
width={608}
|
||||
style={{
|
||||
maxHeight: '80vh',
|
||||
minHeight: isPublicAndNoQuery() ? '72px' : '412px',
|
||||
minHeight: '412px',
|
||||
top: '80px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
@@ -134,13 +125,9 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
: 'Ctrl + K'}
|
||||
</StyledShortcut>
|
||||
</StyledModalHeader>
|
||||
<StyledModalDivider
|
||||
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
|
||||
/>
|
||||
<StyledModalDivider />
|
||||
<Command.List>
|
||||
<StyledContent
|
||||
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
|
||||
>
|
||||
<StyledContent>
|
||||
<Results
|
||||
query={query}
|
||||
onClose={handleClose}
|
||||
@@ -148,7 +135,7 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
</StyledContent>
|
||||
{isPublicWorkspace ? null : showCreatePage ? (
|
||||
{showCreatePage ? (
|
||||
<>
|
||||
<StyledModalDivider />
|
||||
<StyledModalFooter>
|
||||
|
||||
@@ -7,8 +7,6 @@ import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suit
|
||||
import { Command } from 'cmdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { recentPageSettingsAtom } from '../../../atoms';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
@@ -32,12 +30,11 @@ export const Results: FC<ResultsProps> = ({
|
||||
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
assertExists(blockSuiteWorkspace.id);
|
||||
const List = useSwitchToConfig(workspace.id);
|
||||
const list = useSwitchToConfig(workspace.id);
|
||||
|
||||
const recentPageSetting = useAtomValue(recentPageSettingsAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
const navigate = useNavigate();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const { jumpToPage, jumpToSubPath } = useNavigateHelper();
|
||||
const results = blockSuiteWorkspace.search({ query });
|
||||
|
||||
// remove `space:` prefix
|
||||
@@ -55,10 +52,7 @@ export const Results: FC<ResultsProps> = ({
|
||||
return page.trash !== true;
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
setShowCreatePage(!resultsPageMeta.length);
|
||||
//Determine whether to display the ‘+ New page’
|
||||
}, [resultsPageMeta.length, setShowCreatePage]);
|
||||
setShowCreatePage(resultsPageMeta.length === 0);
|
||||
if (!query) {
|
||||
return (
|
||||
<>
|
||||
@@ -90,15 +84,20 @@ export const Results: FC<ResultsProps> = ({
|
||||
</Command.Group>
|
||||
)}
|
||||
<Command.Group heading={t['Jump to']()}>
|
||||
{List.map(link => {
|
||||
{list.map(link => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={link.title}
|
||||
value={link.title}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
link.href && navigate(link.href);
|
||||
link.onClick?.();
|
||||
if ('subPath' in link) {
|
||||
jumpToSubPath(blockSuiteWorkspace.id, link.subPath);
|
||||
} else if ('onClick' in link) {
|
||||
link.onClick();
|
||||
} else {
|
||||
throw new Error('Invalid link');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
|
||||
@@ -50,8 +50,6 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
size={[24, 24]}
|
||||
iconSize={[15, 15]}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import {
|
||||
EditCollectionModel,
|
||||
useCollectionManager,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { uuidv4 } from '@blocksuite/store';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
|
||||
|
||||
type AddCollectionButtonProps = {
|
||||
workspace: Workspace;
|
||||
};
|
||||
|
||||
export const AddCollectionButton = ({
|
||||
workspace,
|
||||
}: AddCollectionButtonProps) => {
|
||||
const getPageInfo = useGetPageInfoById(workspace);
|
||||
const setting = useCollectionManager(workspace.id);
|
||||
const t = useAFFiNEI18N();
|
||||
const [show, showUpdateCollection] = useState(false);
|
||||
const [defaultCollection, setDefaultCollection] = useState<Collection>();
|
||||
const handleClick = useCallback(() => {
|
||||
showUpdateCollection(true);
|
||||
setDefaultCollection({
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
pinned: true,
|
||||
filterList: [],
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}, [showUpdateCollection, workspace.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
data-testid="slider-bar-add-collection-button"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
|
||||
<EditCollectionModel
|
||||
propertiesMeta={workspace.meta.properties}
|
||||
getPageInfo={getPageInfo}
|
||||
onConfirm={setting.saveCollection}
|
||||
open={show}
|
||||
onClose={() => showUpdateCollection(false)}
|
||||
title={t['Save As New Collection']()}
|
||||
init={defaultCollection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteIcon,
|
||||
FilterIcon,
|
||||
InformationIcon,
|
||||
MoreHorizontalIcon,
|
||||
UnpinIcon,
|
||||
ViewLayersIcon,
|
||||
@@ -192,6 +193,7 @@ const CollectionRenderer = ({
|
||||
/>
|
||||
<MenuItem
|
||||
data-testid="collection-item"
|
||||
data-type="collection-list-item"
|
||||
ref={setNodeRef}
|
||||
onCollapsedChange={setCollapsed}
|
||||
active={isOver}
|
||||
@@ -251,21 +253,34 @@ export const CollectionsList = ({ workspace }: CollectionsListProps) => {
|
||||
const metas = useBlockSuitePageMeta(workspace);
|
||||
const { savedCollections } = useSavedCollections(workspace.id);
|
||||
const getPageInfo = useGetPageInfoById(workspace);
|
||||
const pinedCollections = useMemo(
|
||||
() => savedCollections.filter(v => v.pinned),
|
||||
[savedCollections]
|
||||
);
|
||||
if (pinedCollections.length === 0) {
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="slider-bar-collection-null-description"
|
||||
icon={<InformationIcon />}
|
||||
disabled
|
||||
>
|
||||
<span>Create a collection</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div data-testid="collections" className={styles.wrapper}>
|
||||
{savedCollections
|
||||
.filter(v => v.pinned)
|
||||
.map(view => {
|
||||
return (
|
||||
<CollectionRenderer
|
||||
getPageInfo={getPageInfo}
|
||||
key={view.id}
|
||||
collection={view}
|
||||
pages={metas}
|
||||
workspace={workspace}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pinedCollections.map(view => {
|
||||
return (
|
||||
<CollectionRenderer
|
||||
getPageInfo={getPageInfo}
|
||||
key={view.id}
|
||||
collection={view}
|
||||
pages={metas}
|
||||
workspace={workspace}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,6 +150,7 @@ export const Page = ({
|
||||
<Collapsible.Root open={!collapsed}>
|
||||
<MenuItem
|
||||
data-testid="collection-page"
|
||||
data-type="collection-list-item"
|
||||
icon={icon}
|
||||
onClick={clickPage}
|
||||
className={styles.title}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useHistoryAtom } from '../../atoms/history';
|
||||
import { useAppSetting } from '../../atoms/settings';
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
||||
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
||||
import ImportPage from './import-page';
|
||||
@@ -190,7 +191,9 @@ export const RootAppSidebar = ({
|
||||
<SidebarScrollableContainer>
|
||||
<CategoryDivider label={t['Favorites']()} />
|
||||
<FavoriteList workspace={blockSuiteWorkspace} />
|
||||
<CategoryDivider label={t['Collections']()} />
|
||||
<CategoryDivider label={t['Collections']()}>
|
||||
<AddCollectionButton workspace={blockSuiteWorkspace} />
|
||||
</CategoryDivider>
|
||||
<CollectionsList workspace={blockSuiteWorkspace} />
|
||||
<CategoryDivider label={t['others']()} />
|
||||
<RouteMenuLinkItem
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { WorkspaceHeaderProps } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { uuidv4 } from '@blocksuite/store';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -71,13 +70,9 @@ export function WorkspaceHeader({
|
||||
currentWorkspace.blockSuiteWorkspace.meta.properties
|
||||
}
|
||||
getPageInfo={getPageInfoById}
|
||||
init={{
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
filterList: setting.currentCollection.filterList,
|
||||
workspaceId: currentWorkspaceId,
|
||||
}}
|
||||
onConfirm={saveToCollection}
|
||||
filterList={setting.currentCollection.filterList}
|
||||
workspaceId={currentWorkspaceId}
|
||||
></SaveCollectionButton>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@ import { useReferenceLinkHelper } from './use-reference-link-helper';
|
||||
export function useBlockSuiteMetaHelper(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
) {
|
||||
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { setPageMeta, getPageMeta, setPageReadonly } =
|
||||
usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
||||
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
|
||||
@@ -56,8 +57,9 @@ export function useBlockSuiteMetaHelper(
|
||||
trashDate: +new Date(),
|
||||
trashRelate: isRoot ? parentMeta?.id : undefined,
|
||||
});
|
||||
setPageReadonly(pageId, true);
|
||||
},
|
||||
[getPageMeta, metas, setPageMeta]
|
||||
[getPageMeta, metas, setPageMeta, setPageReadonly]
|
||||
);
|
||||
|
||||
const restoreFromTrash = useCallback(
|
||||
@@ -73,11 +75,12 @@ export function useBlockSuiteMetaHelper(
|
||||
trashDate: undefined,
|
||||
trashRelate: undefined,
|
||||
});
|
||||
setPageReadonly(pageId, false);
|
||||
subpageIds.forEach(id => {
|
||||
restoreFromTrash(id);
|
||||
});
|
||||
},
|
||||
[addReferenceLink, getPageMeta, setPageMeta]
|
||||
[addReferenceLink, getPageMeta, setPageMeta, setPageReadonly]
|
||||
);
|
||||
|
||||
const permanentlyDeletePage = useCallback(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export enum RouteLogic {
|
||||
@@ -10,17 +16,22 @@ export enum RouteLogic {
|
||||
export function useNavigateHelper() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const setWorkspaceId = useSetAtom(currentWorkspaceIdAtom);
|
||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
||||
|
||||
const jumpToPage = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
setWorkspaceId(workspaceId);
|
||||
setCurrentPageId(pageId);
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
[navigate, setCurrentPageId, setWorkspaceId]
|
||||
);
|
||||
const jumpToPublicWorkspacePage = useCallback(
|
||||
(
|
||||
@@ -28,11 +39,13 @@ export function useNavigateHelper() {
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
setWorkspaceId(workspaceId);
|
||||
setCurrentPageId(pageId);
|
||||
return navigate(`/public-workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
[navigate, setCurrentPageId, setWorkspaceId]
|
||||
);
|
||||
const jumpToSubPath = useCallback(
|
||||
(
|
||||
@@ -40,14 +53,18 @@ export function useNavigateHelper() {
|
||||
subPath: WorkspaceSubPath,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
setWorkspaceId(workspaceId);
|
||||
setCurrentPageId(null);
|
||||
return navigate(`/workspace/${workspaceId}/${subPath}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
[navigate, setCurrentPageId, setWorkspaceId]
|
||||
);
|
||||
const openPage = useCallback(
|
||||
(workspaceId: string, pageId: string) => {
|
||||
setWorkspaceId(workspaceId);
|
||||
setCurrentPageId(pageId);
|
||||
const isPublicWorkspace =
|
||||
location.pathname.indexOf('/public-workspace') === 0;
|
||||
if (isPublicWorkspace) {
|
||||
@@ -56,25 +73,35 @@ export function useNavigateHelper() {
|
||||
return jumpToPage(workspaceId, pageId);
|
||||
}
|
||||
},
|
||||
[jumpToPage, jumpToPublicWorkspacePage, location.pathname]
|
||||
[
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
location.pathname,
|
||||
setCurrentPageId,
|
||||
setWorkspaceId,
|
||||
]
|
||||
);
|
||||
|
||||
const jumpToIndex = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
setWorkspaceId(null);
|
||||
setCurrentPageId(null);
|
||||
return navigate('/', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
[navigate, setCurrentPageId, setWorkspaceId]
|
||||
);
|
||||
|
||||
const jumpTo404 = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
setWorkspaceId(null);
|
||||
setCurrentPageId(null);
|
||||
return navigate('/404', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
[navigate, setCurrentPageId, setWorkspaceId]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,12 +25,12 @@ export function useTransformWorkspace() {
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace as any);
|
||||
await set(workspaces => {
|
||||
set(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
});
|
||||
return [...workspaces];
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
|
||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -27,12 +27,12 @@ export function useAppHelper() {
|
||||
async (workspaceId: string): Promise<string> => {
|
||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
|
||||
saveWorkspaceToLocalStorage(workspaceId);
|
||||
await set(workspaces => [
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
logger.debug('imported local workspace', workspaceId);
|
||||
@@ -72,12 +72,12 @@ export function useAppHelper() {
|
||||
jumpOnce: true,
|
||||
});
|
||||
}
|
||||
await set(workspaces => [
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
logger.debug('created local workspace', id);
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { StrictMode } from 'react';
|
||||
import { StrictMode, Suspense } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
async function main() {
|
||||
await import('./bootstrap/before-app');
|
||||
const { setup } = await import('./bootstrap/setup');
|
||||
await setup();
|
||||
const { App } = await import('./app');
|
||||
const root = document.getElementById('app');
|
||||
assertExists(root);
|
||||
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Suspense fallback={<WorkspaceFallback key="AppLoading" />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Content, displayFlex } from '@affine/component';
|
||||
import { AffineWatermark } from '@affine/component/affine-watermark';
|
||||
import { appSidebarResizingAtom } from '@affine/component/app-sidebar';
|
||||
import { BlockHubWrapper } from '@affine/component/block-hub';
|
||||
import { NotificationCenter } from '@affine/component/notification-center';
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
ToolContainer,
|
||||
WorkspaceFallback,
|
||||
} from '@affine/component/workspace';
|
||||
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
rootBlockHubAtom,
|
||||
@@ -28,16 +26,12 @@ import {
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
|
||||
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { lazy, Suspense, useCallback, useMemo } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import {
|
||||
@@ -47,6 +41,7 @@ import {
|
||||
} from '../atoms';
|
||||
import { useAppSetting } from '../atoms/settings';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import type { IslandItemNames } from '../components/pure/help-island';
|
||||
import { HelpIsland } from '../components/pure/help-island';
|
||||
import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections';
|
||||
@@ -111,20 +106,6 @@ export const CurrentWorkspaceContext = ({
|
||||
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const exist = metadata.find(m => m.id === workspaceId);
|
||||
const navigate = useNavigate();
|
||||
// fixme(himself65): this is not a good way to handle this,
|
||||
// need a better way to check whether this workspace really exist.
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => {
|
||||
if (!exist) {
|
||||
navigate('/');
|
||||
globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
|
||||
}
|
||||
}, globalThis.HALTING_PROBLEM_TIMEOUT);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}, [exist, metadata.length, navigate]);
|
||||
if (metadata.length === 0) {
|
||||
return <WorkspaceFallback key="no-workspace" />;
|
||||
}
|
||||
@@ -171,32 +152,21 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
||||
|
||||
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
|
||||
const { jumpToPage, openPage } = useNavigateHelper();
|
||||
const { openPage } = useNavigateHelper();
|
||||
|
||||
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
|
||||
|
||||
useEffect(() => {
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(
|
||||
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
|
||||
);
|
||||
if (page && page.meta.jumpOnce) {
|
||||
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
setCurrentPageId(currentPageId);
|
||||
jumpToPage(currentWorkspace.id, page.id);
|
||||
}
|
||||
}, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
|
||||
|
||||
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
|
||||
const helper = useBlockSuiteWorkspaceHelper(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||
|
||||
const handleCreatePage = useCallback(() => {
|
||||
return helper.createPage(nanoid());
|
||||
}, [helper]);
|
||||
const id = nanoid();
|
||||
helper.createPage(id);
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(id);
|
||||
assertExists(page);
|
||||
return page;
|
||||
}, [currentWorkspace.blockSuiteWorkspace, helper]);
|
||||
|
||||
const handleOpenWorkspaceListModal = useCallback(() => {
|
||||
setOpenWorkspacesModal(true);
|
||||
}, [setOpenWorkspacesModal]);
|
||||
@@ -289,7 +259,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
|
||||
<HelpIsland showList={pageId ? undefined : showList} />
|
||||
</ToolContainer>
|
||||
<AffineWatermark />
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
<PageListTitleCellDragOverlay />
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { lazy, useEffect, useRef } from 'react';
|
||||
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { useWorkspace } from '../hooks/use-workspace';
|
||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||
import { rootStore } from '@toeverything/infra/atom';
|
||||
import { lazy } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
const AllWorkspaceModals = lazy(() =>
|
||||
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
||||
@@ -14,81 +12,35 @@ const AllWorkspaceModals = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
type WorkspaceLoaderProps = {
|
||||
id: string;
|
||||
};
|
||||
const logger = new DebugLogger('index-page');
|
||||
|
||||
const WorkspaceLoader = (props: WorkspaceLoaderProps): null => {
|
||||
useWorkspace(props.id);
|
||||
export const loader: LoaderFunction = async () => {
|
||||
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
|
||||
const lastId = localStorage.getItem('last_workspace_id');
|
||||
const lastPageId = localStorage.getItem('last_page_id');
|
||||
const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
|
||||
if (target) {
|
||||
const targetWorkspace = getWorkspace(target.id);
|
||||
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
|
||||
({ trash }) => !trash
|
||||
);
|
||||
const pageId =
|
||||
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
|
||||
nonTrashPages.at(0)?.id;
|
||||
if (pageId) {
|
||||
logger.debug('Found target workspace. Jump to page', pageId);
|
||||
return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
|
||||
} else {
|
||||
logger.debug('Found target workspace. Jump to all page');
|
||||
return redirect(`/workspace/${targetWorkspace.id}/all`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const logger = new DebugLogger('index-page');
|
||||
|
||||
export const Component = () => {
|
||||
const meta = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const jumpOnceRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (jumpOnceRef.current) {
|
||||
return;
|
||||
}
|
||||
const lastId = localStorage.getItem('last_workspace_id');
|
||||
const lastPageId = localStorage.getItem('last_page_id');
|
||||
const target =
|
||||
(lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
|
||||
if (target) {
|
||||
const targetWorkspace = getWorkspace(target.id);
|
||||
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
|
||||
({ trash }) => !trash
|
||||
);
|
||||
const pageId =
|
||||
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
|
||||
nonTrashPages.at(0)?.id;
|
||||
if (pageId) {
|
||||
logger.debug('Found target workspace. Jump to page', pageId);
|
||||
navigateHelper.jumpToPage(
|
||||
targetWorkspace.id,
|
||||
pageId,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
jumpOnceRef.current = true;
|
||||
} else {
|
||||
const clearId = setTimeout(() => {
|
||||
dispose.dispose();
|
||||
logger.debug('Found target workspace. Jump to all pages');
|
||||
navigateHelper.jumpToSubPath(
|
||||
targetWorkspace.id,
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
jumpOnceRef.current = true;
|
||||
}, 1000);
|
||||
const dispose = targetWorkspace.slots.pageAdded.once(pageId => {
|
||||
clearTimeout(clearId);
|
||||
navigateHelper.jumpToPage(
|
||||
targetWorkspace.id,
|
||||
pageId,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
jumpOnceRef.current = true;
|
||||
});
|
||||
return () => {
|
||||
clearTimeout(clearId);
|
||||
dispose.dispose();
|
||||
jumpOnceRef.current = false;
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.warn('No workspace found');
|
||||
}
|
||||
return;
|
||||
}, [meta, navigateHelper]);
|
||||
return (
|
||||
<>
|
||||
{meta.map(({ id }) => (
|
||||
<WorkspaceLoader id={id} key={id} />
|
||||
))}
|
||||
<AllWorkspaceModals />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
import { useCollectionManager } from '@affine/component/page-list';
|
||||
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
|
||||
const AllPage = () => {
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const workspaceId = args.params.workspaceId;
|
||||
assertExists(workspaceId);
|
||||
const workspaceAtom = getActiveBlockSuiteWorkspaceAtom(workspaceId);
|
||||
const workspace = await rootStore.get(workspaceAtom);
|
||||
const page = workspace.getPage(
|
||||
`${workspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
|
||||
);
|
||||
if (page && page.meta.jumpOnce) {
|
||||
workspace.meta.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
return redirect(`/workspace/${workspace.id}/${page.id}`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const AllPage = () => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const setting = useCollectionManager(currentWorkspace.id);
|
||||
const onClickPage = useCallback(
|
||||
@@ -23,6 +46,18 @@ const AllPage = () => {
|
||||
},
|
||||
[currentWorkspace, jumpToPage]
|
||||
);
|
||||
useEffect(() => {
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(
|
||||
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
|
||||
);
|
||||
if (page && page.meta.jumpOnce) {
|
||||
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
setCurrentPageId(currentPageId);
|
||||
jumpToPage(currentWorkspace.id, page.id);
|
||||
}
|
||||
}, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
|
||||
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
|
||||
return (
|
||||
<>
|
||||
@@ -42,9 +77,5 @@ const AllPage = () => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<WorkspaceLayout>
|
||||
<AllPage />
|
||||
</WorkspaceLayout>
|
||||
);
|
||||
return <AllPage />;
|
||||
};
|
||||
|
||||
@@ -7,18 +7,18 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import { type ReactElement, useCallback, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
|
||||
const WorkspaceDetailPageImpl = (): ReactElement => {
|
||||
const DetailPageImpl = (): ReactElement => {
|
||||
const { openPage, jumpToSubPath } = useNavigateHelper();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
@@ -27,7 +27,7 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
|
||||
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||
const collectionManager = useCollectionManager(currentWorkspace.id);
|
||||
const onLoad = useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
(_: Page, editor: EditorContainer) => {
|
||||
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
||||
return openPage(blockSuiteWorkspace.id, pageId);
|
||||
});
|
||||
@@ -68,10 +68,10 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceDetailPage = (): ReactElement => {
|
||||
export const DetailPage = (): ReactElement => {
|
||||
const { workspaceId, pageId } = useParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { jumpTo404 } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
|
||||
const page = currentPageId
|
||||
@@ -91,7 +91,14 @@ const WorkspaceDetailPage = (): ReactElement => {
|
||||
const page =
|
||||
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
navigate('/404');
|
||||
jumpTo404();
|
||||
} else {
|
||||
// fixme: cleanup jumpOnce in the right time
|
||||
if (page.meta.jumpOnce) {
|
||||
currentWorkspace.blockSuiteWorkspace.setPageMeta(currentPageId, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,8 +106,8 @@ const WorkspaceDetailPage = (): ReactElement => {
|
||||
currentPageId,
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
currentWorkspace.id,
|
||||
jumpTo404,
|
||||
location.pathname,
|
||||
navigate,
|
||||
pageId,
|
||||
setCurrentPageId,
|
||||
workspaceId,
|
||||
@@ -110,13 +117,17 @@ const WorkspaceDetailPage = (): ReactElement => {
|
||||
if (!currentPageId || !page) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
return <WorkspaceDetailPageImpl />;
|
||||
return <DetailPageImpl />;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = args => {
|
||||
if (args.params.pageId) {
|
||||
localStorage.setItem('last_page_id', args.params.pageId);
|
||||
rootStore.set(currentPageIdAtom, args.params.pageId);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<WorkspaceLayout>
|
||||
<WorkspaceDetailPage />
|
||||
</WorkspaceLayout>
|
||||
);
|
||||
return <DetailPage />;
|
||||
};
|
||||
|
||||
26
apps/core/src/pages/workspace/index.tsx
Normal file
26
apps/core/src/pages/workspace/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { currentWorkspaceIdAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import type { ReactElement } from 'react';
|
||||
import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
|
||||
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
|
||||
if (!meta.some(({ id }) => id === args.params.workspaceId)) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (args.params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', args.params.workspaceId);
|
||||
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
return (
|
||||
<WorkspaceLayout>
|
||||
<Outlet />
|
||||
</WorkspaceLayout>
|
||||
);
|
||||
};
|
||||
@@ -6,9 +6,8 @@ import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
|
||||
const TrashPage = () => {
|
||||
export const TrashPage = () => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const onClickPage = useCallback(
|
||||
@@ -44,9 +43,5 @@ const TrashPage = () => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<WorkspaceLayout>
|
||||
<TrashPage />
|
||||
</WorkspaceLayout>
|
||||
);
|
||||
return <TrashPage />;
|
||||
};
|
||||
|
||||
19
apps/core/src/polyfill/ses.ts
Normal file
19
apps/core/src/polyfill/ses.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'ses';
|
||||
|
||||
if (!process.env.COVERAGE) {
|
||||
lockdown({
|
||||
evalTaming: 'unsafeEval',
|
||||
overrideTaming: 'severe',
|
||||
consoleTaming: 'unsafe',
|
||||
errorTaming: 'unsafe',
|
||||
errorTrapping: 'platform',
|
||||
unhandledRejectionTrapping: 'report',
|
||||
});
|
||||
|
||||
console.log('SES lockdown complete');
|
||||
} else {
|
||||
Object.defineProperty(globalThis, 'harden', {
|
||||
value: (x: any) => Object.freeze(x),
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { arrayMove } from '@dnd-kit/sortable';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useTransition } from 'react';
|
||||
@@ -124,7 +124,7 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
currentWorkspaceIdAtom
|
||||
);
|
||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
||||
const [transitioning, transition] = useTransition();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
|
||||
|
||||
const handleOpenSettingModal = useCallback(
|
||||
@@ -143,7 +143,7 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
<>
|
||||
<Suspense>
|
||||
<WorkspaceListModal
|
||||
disabled={transitioning}
|
||||
disabled={isPending}
|
||||
workspaces={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
open={
|
||||
@@ -157,10 +157,10 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
(activeId, overId) => {
|
||||
const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
||||
const newIndex = workspaces.findIndex(w => w.id === overId);
|
||||
transition(() => {
|
||||
startTransition(() => {
|
||||
setWorkspaces(workspaces =>
|
||||
arrayMove(workspaces, oldIndex, newIndex)
|
||||
).catch(console.error);
|
||||
);
|
||||
});
|
||||
},
|
||||
[setWorkspaces, workspaces]
|
||||
@@ -195,11 +195,13 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
setOpenCreateWorkspaceModal(false);
|
||||
}, [setOpenCreateWorkspaceModal])}
|
||||
onCreate={useCallback(
|
||||
async id => {
|
||||
setOpenCreateWorkspaceModal(false);
|
||||
setOpenWorkspacesModal(false);
|
||||
setCurrentWorkspaceId(id);
|
||||
return jumpToSubPath(id, WorkspaceSubPath.ALL);
|
||||
id => {
|
||||
startTransition(() => {
|
||||
setOpenCreateWorkspaceModal(false);
|
||||
setOpenWorkspacesModal(false);
|
||||
setCurrentWorkspaceId(id);
|
||||
jumpToSubPath(id, WorkspaceSubPath.ALL);
|
||||
});
|
||||
},
|
||||
[
|
||||
jumpToSubPath,
|
||||
|
||||
37
apps/core/src/router.ts
Normal file
37
apps/core/src/router.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
|
||||
export const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
path: '/',
|
||||
lazy: () => import('./pages/index'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId',
|
||||
lazy: () => import('./pages/workspace/index'),
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
lazy: () => import('./pages/workspace/all-page'),
|
||||
},
|
||||
{
|
||||
path: 'trash',
|
||||
lazy: () => import('./pages/workspace/trash-page'),
|
||||
},
|
||||
{
|
||||
path: ':pageId',
|
||||
lazy: () => import('./pages/workspace/detail-page'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
],
|
||||
{
|
||||
future: {
|
||||
v7_normalizeFormMethod: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -16,7 +16,7 @@
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": ["webpack-env"]
|
||||
"types": ["webpack-env", "ses"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/docs",
|
||||
"version": "0.7.0-canary.51",
|
||||
"version": "0.8.0-canary.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -10,12 +10,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"express": "^4.18.2",
|
||||
"jotai": "^2.2.2",
|
||||
"react": "18.3.0-canary-1fdacbefd-20230630",
|
||||
@@ -24,12 +24,12 @@
|
||||
"waku": "0.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react": "^18.2.17",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vanilla-extract/css": "^1.12.0",
|
||||
"@vanilla-extract/vite-plugin": "^3.8.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,10 @@ test('app theme', async ({ page, electronApp }) => {
|
||||
}
|
||||
|
||||
{
|
||||
await page.getByTestId('editor-option-menu').click();
|
||||
await page.getByTestId('change-theme-dark').click();
|
||||
await page.getByTestId('settings-modal-trigger').click();
|
||||
await page.getByTestId('appearance-panel-trigger').click();
|
||||
await page.waitForTimeout(50);
|
||||
await page.getByTestId('dark-theme-trigger').click();
|
||||
const themeMode = await root.evaluate(element =>
|
||||
element.getAttribute('data-theme')
|
||||
);
|
||||
|
||||
@@ -47,6 +47,9 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
|
||||
test('export then add', async ({ page, appInfo, workspace }) => {
|
||||
const w = await workspace.current();
|
||||
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.fill('.affine-default-page-block-title', 'test1');
|
||||
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
@@ -108,4 +111,11 @@ test('export then add', async ({ page, appInfo, workspace }) => {
|
||||
expect(newWorkspace.id).not.toBe(originalId);
|
||||
// check its name is correct
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText(newWorkspaceName);
|
||||
|
||||
// find button which has the title "test1"
|
||||
const test1PageButton = await page.waitForSelector(`text="test1"`);
|
||||
await test1PageButton.click();
|
||||
|
||||
const title = page.locator('[data-block-is-title] >> text="test1"');
|
||||
await expect(title).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -26,8 +26,82 @@ const arch =
|
||||
? process.argv[process.argv.indexOf('--arch') + 1]
|
||||
: process.arch;
|
||||
|
||||
const platform =
|
||||
process.argv.indexOf('--platform') > 0
|
||||
? process.argv[process.argv.indexOf('--platform') + 1]
|
||||
: process.platform;
|
||||
|
||||
const windowsIconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`;
|
||||
|
||||
const makers = [
|
||||
!process.env.SKIP_BUNDLE &&
|
||||
platform === 'darwin' && {
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
format: 'ULFO',
|
||||
icon: icnsPath,
|
||||
name: 'AFFiNE',
|
||||
'icon-size': 128,
|
||||
background: path.resolve(
|
||||
__dirname,
|
||||
'./resources/icons/dmg-background.png'
|
||||
),
|
||||
contents: [
|
||||
{
|
||||
x: 176,
|
||||
y: 192,
|
||||
type: 'file',
|
||||
path: path.resolve(
|
||||
__dirname,
|
||||
'out',
|
||||
buildType,
|
||||
`${productName}-darwin-${arch}`,
|
||||
`${productName}.app`
|
||||
),
|
||||
},
|
||||
{ x: 432, y: 192, type: 'link', path: '/Applications' },
|
||||
],
|
||||
file: path.resolve(
|
||||
__dirname,
|
||||
'out',
|
||||
buildType,
|
||||
`${productName}-darwin-${arch}`,
|
||||
`${productName}.app`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
config: {
|
||||
name: 'affine',
|
||||
iconUrl: icoPath,
|
||||
setupIcon: icoPath,
|
||||
platforms: ['darwin', 'linux', 'win32'],
|
||||
},
|
||||
},
|
||||
!process.env.SKIP_BUNDLE && {
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {
|
||||
name: 'AFFiNE',
|
||||
setupIcon: icoPath,
|
||||
iconUrl: windowsIconUrl,
|
||||
loadingGif: './resources/icons/affine_installing.gif',
|
||||
},
|
||||
},
|
||||
!process.env.SKIP_BUNDLE && {
|
||||
name: '@reforged/maker-appimage',
|
||||
config: {
|
||||
name: 'AFFiNE',
|
||||
iconUrl: icoPath,
|
||||
setupIcon: icoPath,
|
||||
platforms: ['linux'],
|
||||
options: {
|
||||
bin: productName,
|
||||
},
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
/**
|
||||
* @type {import('@electron-forge/shared-types').ForgeConfig}
|
||||
*/
|
||||
@@ -57,51 +131,7 @@ module.exports = {
|
||||
// We need the following line for updater
|
||||
extraResource: ['./resources/app-update.yml'],
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
format: 'ULFO',
|
||||
icon: icnsPath,
|
||||
name: 'AFFiNE',
|
||||
'icon-size': 128,
|
||||
background: './resources/icons/dmg-background.png',
|
||||
contents: [
|
||||
{
|
||||
x: 176,
|
||||
y: 192,
|
||||
type: 'file',
|
||||
path: path.resolve(
|
||||
__dirname,
|
||||
'out',
|
||||
buildType,
|
||||
`${productName}-darwin-${arch}`,
|
||||
`${productName}.app`
|
||||
),
|
||||
},
|
||||
{ x: 432, y: 192, type: 'link', path: '/Applications' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
config: {
|
||||
name: 'affine',
|
||||
iconUrl: icoPath,
|
||||
setupIcon: icoPath,
|
||||
platforms: ['darwin', 'linux', 'win32'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {
|
||||
name: 'AFFiNE',
|
||||
setupIcon: icoPath,
|
||||
iconUrl: windowsIconUrl,
|
||||
loadingGif: './resources/icons/affine_installing.gif',
|
||||
},
|
||||
},
|
||||
],
|
||||
makers,
|
||||
hooks: {
|
||||
readPackageJson: async (_, packageJson) => {
|
||||
// we want different package name for canary build
|
||||
@@ -112,17 +142,28 @@ module.exports = {
|
||||
const { rm, cp } = require('node:fs/promises');
|
||||
const { resolve } = require('node:path');
|
||||
|
||||
await rm(
|
||||
resolve(__dirname, './node_modules/@toeverything/plugin-infra'),
|
||||
await rm(resolve(__dirname, './node_modules/@toeverything/infra'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
await cp(
|
||||
resolve(__dirname, '../../packages/infra'),
|
||||
resolve(__dirname, './node_modules/@toeverything/infra'),
|
||||
{
|
||||
recursive: true,
|
||||
force: true,
|
||||
}
|
||||
);
|
||||
|
||||
await rm(resolve(__dirname, './node_modules/@affine/sdk'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
await cp(
|
||||
resolve(__dirname, '../../packages/plugin-infra'),
|
||||
resolve(__dirname, './node_modules/@toeverything/plugin-infra'),
|
||||
resolve(__dirname, '../../packages/sdk'),
|
||||
resolve(__dirname, './node_modules/@affine/sdk'),
|
||||
{
|
||||
recursive: true,
|
||||
force: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.51",
|
||||
"version": "0.8.0-canary.10",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -10,6 +10,7 @@
|
||||
"description": "AFFiNE App",
|
||||
"homepage": "https://github.com/toeverything/AFFiNE",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
|
||||
"dev:prod": "yarn node scripts/dev.mjs",
|
||||
"build": "NODE_ENV=production zx scripts/build-layers.mjs",
|
||||
@@ -26,38 +27,39 @@
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly",
|
||||
"@affine/sdk": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@electron-forge/cli": "^6.2.1",
|
||||
"@electron-forge/core": "^6.2.1",
|
||||
"@electron-forge/core-utils": "^6.2.1",
|
||||
"@electron-forge/maker-deb": "^6.2.1",
|
||||
"@electron-forge/maker-dmg": "^6.2.1",
|
||||
"@electron-forge/maker-squirrel": "^6.2.1",
|
||||
"@electron-forge/maker-zip": "^6.2.1",
|
||||
"@electron-forge/shared-types": "^6.2.1",
|
||||
"@electron/remote": "2.0.10",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@reforged/maker-appimage": "^3.3.1",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "^25.2.0",
|
||||
"electron": "^25.3.1",
|
||||
"electron-log": "^5.0.0-beta.24",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.18.11",
|
||||
"esbuild": "^0.18.15",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jotai": "^2.2.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"undici": "^5.22.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vitest": "^0.33.0",
|
||||
"which": "^3.0.1",
|
||||
"zx": "^7.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"electron-updater": "^6.0.0",
|
||||
"link-preview-js": "^3.0.4",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": ["@affine/bookmark-block"],
|
||||
"projects": ["tag:plugin"],
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ if (process.platform === 'win32') {
|
||||
|
||||
async function buildLayers() {
|
||||
const common = config();
|
||||
await esbuild.build(common.workers);
|
||||
await esbuild.build({
|
||||
...common.layers,
|
||||
define: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user