mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-10 19:38:39 +00:00
Compare commits
108 Commits
v0.7.0-can
...
v0.8.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36bd0b02d0 | ||
|
|
3a92c4f798 | ||
|
|
97de0ef5b4 | ||
|
|
bbf5f0efe0 | ||
|
|
ea76936508 | ||
|
|
f076cb0ead | ||
|
|
0f230253a8 | ||
|
|
13d2dde501 | ||
|
|
65fc0ed59c | ||
|
|
7ec4b8fb8c | ||
|
|
6415f0093b | ||
|
|
5795020403 | ||
|
|
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 |
@@ -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
|
||||
|
||||
17
.eslintrc.js
17
.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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -203,6 +214,7 @@ const config = {
|
||||
ignoreIIFE: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-promises': ['error'],
|
||||
},
|
||||
})),
|
||||
{
|
||||
@@ -228,6 +240,7 @@ const config = {
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': 0,
|
||||
'@typescript-eslint/no-misused-promises': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
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'
|
||||
6
.github/workflows/nightly-build.yml
vendored
6
.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:
|
||||
|
||||
6
.github/workflows/release-desktop-app.yml
vendored
6
.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:
|
||||
|
||||
26
README.md
26
README.md
@@ -14,21 +14,12 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<!--
|
||||
Make New Badge Pattern badges inline
|
||||
See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
-->
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-66-orange.svg?style=flat-square
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[?style=flat-square&logoColor=white&logo=affine>)](https://app.affine.pro)
|
||||
[](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)
|
||||
@@ -39,6 +30,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
[![React-version-icon]](https://reactjs.org/)
|
||||
[![blocksuite-icon]](https://github.com/toeverything/blocksuite)
|
||||
[![Rust-version-icon]](https://www.rust-lang.org/)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE?ref=badge_shield)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -126,10 +118,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
|
||||
|
||||
@@ -190,6 +183,7 @@ See [docs/contributing/tutorial.md](./docs/contributing/tutorial.md) for details
|
||||
|
||||
See [LICENSE] for details.
|
||||
|
||||
[all-contributors-badge]: https://img.shields.io/github/all-contributors/toeverything/AFFiNE/master?color=orange
|
||||
[license]: ./LICENSE
|
||||
[building.md]: ./docs/BUILDING.md
|
||||
[these people]: https://twitter.com/AffineOfficial/followers
|
||||
@@ -197,10 +191,12 @@ 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
|
||||
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
|
||||
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/react?filename=apps%2Fcore%2Fpackage.json&color=rgb(97%2C228%2C251)
|
||||
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fcore%2Fpackage.json&label=blocksuite
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE?ref=badge_large)
|
||||
|
||||
@@ -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,
|
||||
@@ -130,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,
|
||||
},
|
||||
@@ -137,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,
|
||||
},
|
||||
@@ -252,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({}),
|
||||
@@ -269,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,
|
||||
|
||||
@@ -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.55",
|
||||
"version": "0.8.0-canary.11",
|
||||
"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,13 +18,13 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/icons": "^2.1.27",
|
||||
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/icons": "^2.1.29",
|
||||
"@blocksuite/lit": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
@@ -34,6 +33,7 @@
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.14.2",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.6",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
@@ -70,6 +70,7 @@
|
||||
"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,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
import '@toeverything/components/style.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 { use } from 'foxact/use';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, memo, Suspense, useEffect } from 'react';
|
||||
import { lazy, memo, Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { router } from './router';
|
||||
import createEmotionCache from './utils/create-emotion-cache';
|
||||
|
||||
const i18n = createI18n();
|
||||
const cache = createEmotionCache();
|
||||
|
||||
const DevTools = lazy(() =>
|
||||
@@ -32,14 +32,19 @@ const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
export const App = memo(function App() {
|
||||
useEffect(() => {
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAtom } from 'jotai';
|
||||
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';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { PrimitiveAtom } from 'jotai';
|
||||
import { atom } from 'jotai';
|
||||
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
||||
import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
|
||||
|
||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||
import type { SettingProps } from '../components/affine/setting-modal';
|
||||
@@ -59,19 +61,16 @@ const defaultPageSetting = {
|
||||
mode: 'page',
|
||||
} satisfies PageLocalSetting;
|
||||
|
||||
export const pageSettingFamily = atomFamily((pageId: string) =>
|
||||
export const pageSettingFamily: AtomFamily<
|
||||
string,
|
||||
PrimitiveAtom<PageLocalSetting>
|
||||
> = atomFamily((pageId: string) =>
|
||||
atom(
|
||||
get =>
|
||||
get(pageSettingsBaseAtom)[pageId] ?? {
|
||||
...defaultPageSetting,
|
||||
},
|
||||
(
|
||||
get,
|
||||
set,
|
||||
patch:
|
||||
| Partial<PageLocalSetting>
|
||||
| ((prevSetting: PageLocalSetting | undefined) => void)
|
||||
) => {
|
||||
(get, set, patch) => {
|
||||
set(recentPageSettingsBaseAtom, ids => {
|
||||
// pick 3 recent page ids
|
||||
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
||||
@@ -93,7 +92,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/atom';
|
||||
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/atom';
|
||||
|
||||
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,200 +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 * as Atom from '@toeverything/plugin-infra/atom';
|
||||
import {
|
||||
editorItemsAtom,
|
||||
headerItemsAtom,
|
||||
registeredPluginAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/plugin-infra/atom';
|
||||
import type {
|
||||
CallbackMap,
|
||||
PluginContext,
|
||||
} from '@toeverything/plugin-infra/entry';
|
||||
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/atom') {
|
||||
return Atom;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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,
|
||||
// todo: permission control
|
||||
fetch: globalThis.fetch,
|
||||
|
||||
// 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()) as { name: string; assets: string[]; release: boolean }[];
|
||||
|
||||
await Promise.all(
|
||||
pluginList.map(({ name: plugin, release, assets }) => {
|
||||
if (!release && process.env.NODE_ENV !== 'development') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const pluginCompartment = new Compartment(createGlobalThis(), {});
|
||||
const pluginGlobalThis = pluginCompartment.globalThis;
|
||||
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
|
||||
const entryURL = new URL('index.js', baseURL);
|
||||
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
|
||||
return fetch(entryURL).then(async res => {
|
||||
if (assets.length > 0) {
|
||||
await Promise.all(
|
||||
assets.map(async asset => {
|
||||
if (asset.endsWith('.css')) {
|
||||
const res = await fetch(new URL(asset, baseURL));
|
||||
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', plugin);
|
||||
style.textContent = text;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
const codeText = await res.text();
|
||||
pluginCompartment.evaluate(codeText, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
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');
|
||||
});
|
||||
|
||||
183
apps/core/src/bootstrap/setup.ts
Normal file
183
apps/core/src/bootstrap/setup.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
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 () => {
|
||||
if (oldMeta.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
const workspace = await adapter.CRUD.get(oldMeta.id);
|
||||
if (!workspace) {
|
||||
console.warn('cannot find workspace', oldMeta.id);
|
||||
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) {
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
assertExists(adapter);
|
||||
promises.push(
|
||||
(async () => {
|
||||
if (oldMeta.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
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
|
||||
);
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
};
|
||||
console.log('migrate to v3');
|
||||
})()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
console.log('migration done');
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('migration failed', e);
|
||||
})
|
||||
.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/atom';
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
||||
|
||||
@@ -139,7 +139,6 @@ const SetDBLocationContent = ({
|
||||
if (result?.filePath) {
|
||||
onConfirmLocation(result.filePath);
|
||||
} else if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic so the type is unknown
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})().catch(err => {
|
||||
@@ -273,7 +272,6 @@ export const CreateWorkspaceModal = ({
|
||||
setStep('set-syncing-mode');
|
||||
} else if (result.error || result.canceled) {
|
||||
if (result.error) {
|
||||
// @ts-expect-error: result.error is dynamic so the type is unknown
|
||||
toast(t[result.error]());
|
||||
}
|
||||
onClose();
|
||||
|
||||
@@ -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}
|
||||
size="large"
|
||||
type="error"
|
||||
shape="circle"
|
||||
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',
|
||||
};
|
||||
|
||||
@@ -36,7 +36,6 @@ export const ExportPanel: FC<{
|
||||
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']());
|
||||
|
||||
@@ -55,7 +55,6 @@ export const StoragePanel: FC<{
|
||||
if (!result?.error && !result?.canceled) {
|
||||
toast(t['Move folder success']());
|
||||
} else if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
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/atom';
|
||||
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();
|
||||
@@ -17,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,4 +1,4 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { ScrollableContainer, Tooltip } from '@affine/component';
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
@@ -8,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';
|
||||
@@ -76,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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useBlockSuitePagePreview } from '@toeverything/hooks/use-block-suite-pa
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type React from 'react';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
import { Suspense, useCallback, useMemo } from 'react';
|
||||
|
||||
import { allPageModeSelectAtom } from '../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
@@ -70,16 +70,20 @@ const PagePreview = ({
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -87,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>
|
||||
@@ -96,7 +100,7 @@ const PageListEmpty = (props: {
|
||||
return (
|
||||
<Trans i18nKey="emptyAllPages">
|
||||
Click on the
|
||||
<CreateNewPageButton />
|
||||
{createNewPageButton}
|
||||
button to create your first page.
|
||||
</Trans>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -54,7 +55,7 @@ export const StyledSwitchItem = styled('button')<{
|
||||
zIndex: 2,
|
||||
fontSize: '20px',
|
||||
path: {
|
||||
fill: 'currentColor',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,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/atom';
|
||||
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 = (
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -145,11 +139,7 @@ const PageMenu = () => {
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={openConfirm}
|
||||
title={pageMeta.title}
|
||||
onConfirm={() => {
|
||||
removeToTrash(pageMeta.id);
|
||||
toast(t['Moved to Trash']());
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
onConfirm={handleOnConfirm}
|
||||
onCancel={() => {
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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/atom';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ 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/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { headerItemsAtom } from '@toeverything/infra/atom';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import DownloadClientTip from './download-tips';
|
||||
@@ -37,8 +39,6 @@ export type BaseHeaderProps<
|
||||
|
||||
export enum HeaderRightItemName {
|
||||
EditorOptionMenu = 'editorOptionMenu',
|
||||
// some windows only items
|
||||
WindowsAppControls = 'windowsAppControls',
|
||||
}
|
||||
|
||||
type HeaderItem = {
|
||||
@@ -57,62 +57,59 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
[HeaderRightItemName.EditorOptionMenu]: {
|
||||
Component: EditorOptionMenu,
|
||||
availableWhen: (_, currentPage, { isPublic }) => {
|
||||
return !isPublic && currentPage?.meta.trash !== true;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.WindowsAppControls]: {
|
||||
Component: () => {
|
||||
const handleMinimizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMinimizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleMaximizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMaximizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleCloseApp = useCallback(() => {
|
||||
window.apis?.ui.handleCloseApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
data-platform-target="win32"
|
||||
className={styles.windowAppControlsWrapper}
|
||||
>
|
||||
<button
|
||||
data-type="minimize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleMinimizeApp}
|
||||
>
|
||||
<MinusIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="maximize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleMaximizeApp}
|
||||
>
|
||||
<RoundedRectangleIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="close"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleCloseApp}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
!isPublic && currentPage?.meta.trash !== true && currentPage !== null
|
||||
);
|
||||
},
|
||||
availableWhen: () => {
|
||||
return isDesktop && globalThis.platform === 'win32';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type HeaderProps = BaseHeaderProps;
|
||||
const WindowsAppControls = () => {
|
||||
const handleMinimizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMinimizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleMaximizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMaximizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleCloseApp = useCallback(() => {
|
||||
window.apis?.ui.handleCloseApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
data-platform-target="win32"
|
||||
className={styles.windowAppControlsWrapper}
|
||||
>
|
||||
<button
|
||||
data-type="minimize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleMinimizeApp}
|
||||
>
|
||||
<MinusIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="maximize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleMaximizeApp}
|
||||
>
|
||||
<RoundedRectangleIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="close"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleCloseApp}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PluginHeader = () => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
@@ -152,10 +149,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());
|
||||
}, []);
|
||||
@@ -163,7 +159,7 @@ export const Header = forwardRef<
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
|
||||
const mode = useAtomValue(currentModeAtom);
|
||||
|
||||
const isWindowsDesktop = globalThis.platform === 'win32' && isDesktop;
|
||||
return (
|
||||
<div
|
||||
className={styles.headerContainer}
|
||||
@@ -175,7 +171,10 @@ export const Header = forwardRef<
|
||||
{showDownloadTip ? (
|
||||
<DownloadClientTip
|
||||
show={showDownloadTip}
|
||||
onClose={() => setShowDownloadTip(false)}
|
||||
onClose={() => {
|
||||
setShowDownloadTip(false);
|
||||
localStorage.setItem('affine-is-dt-hide', '1');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<BrowserWarning
|
||||
@@ -191,14 +190,25 @@ export const Header = forwardRef<
|
||||
data-has-warning={showWarning}
|
||||
data-testid="editor-header-items"
|
||||
data-is-edgeless={mode === 'edgeless'}
|
||||
data-is-page-list={props.currentPage === null}
|
||||
>
|
||||
<div className={styles.headerLeftSide}>
|
||||
{!open && <SidebarSwitch />}
|
||||
{props.leftSlot}
|
||||
<div>{!open && <SidebarSwitch />}</div>
|
||||
<div
|
||||
className={clsx(styles.headerLeftSideItem, {
|
||||
[styles.headerLeftSideOpen]: open,
|
||||
})}
|
||||
>
|
||||
{props.leftSlot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{props.children}
|
||||
<div className={styles.headerRightSide}>
|
||||
<div
|
||||
className={clsx(styles.headerRightSide, {
|
||||
[styles.headerRightSideWindow]: isWindowsDesktop,
|
||||
})}
|
||||
>
|
||||
<PluginHeader />
|
||||
{useMemo(() => {
|
||||
return Object.entries(HeaderRightItems).map(
|
||||
@@ -222,6 +232,7 @@ export const Header = forwardRef<
|
||||
);
|
||||
}, [props])}
|
||||
</div>
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -65,7 +65,7 @@ export const BlockSuiteEditorHeader: FC<
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.pageTitle}>
|
||||
{isEditable ? (
|
||||
<div>
|
||||
<input
|
||||
@@ -88,11 +88,7 @@ export const BlockSuiteEditorHeader: FC<
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
data-testid="title-edit-button"
|
||||
onClick={handleClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<span data-testid="title-edit-button" onClick={handleClick}>
|
||||
{title || 'Untitled'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ComplexStyleRule } from '@vanilla-extract/css';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { createContainer, style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerVanillaContainer = createContainer();
|
||||
|
||||
export const headerContainer = style({
|
||||
height: 'auto',
|
||||
@@ -27,35 +29,45 @@ export const headerContainer = style({
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const header = style({
|
||||
containerName: headerVanillaContainer,
|
||||
containerType: 'inline-size',
|
||||
flexShrink: 0,
|
||||
height: '52px',
|
||||
minHeight: '52px',
|
||||
width: '100%',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 20px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto 1fr',
|
||||
alignItems: 'center',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 99,
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&[data-is-edgeless="true"]': {
|
||||
'&[data-is-page-list="true"], &[data-is-edgeless="true"]': {
|
||||
borderBottom: `1px solid var(--affine-border-color)`,
|
||||
},
|
||||
},
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
alignItems: 'start',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const titleContainer = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
margin: 'auto',
|
||||
position: 'absolute',
|
||||
inset: 'auto auto auto 50%',
|
||||
transform: 'translate(-50%, 0px)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignContent: 'unset',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
alignItems: 'start',
|
||||
paddingTop: '2px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
@@ -75,9 +87,26 @@ export const title = style({
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const pageTitle = style({
|
||||
maxWidth: '600px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'width .15s',
|
||||
cursor: 'pointer',
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 1920px)`]: {
|
||||
maxWidth: '800px',
|
||||
},
|
||||
[`${headerVanillaContainer} (max-width: 1300px)`]: {
|
||||
maxWidth: '400px',
|
||||
},
|
||||
[`${headerVanillaContainer} (max-width: 768px)`]: {
|
||||
maxWidth: '220px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const titleWrapper = style({
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -86,10 +115,28 @@ export const titleWrapper = style({
|
||||
export const headerLeftSide = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '150px',
|
||||
'@media': {
|
||||
'(max-width: 900px)': {
|
||||
width: 'auto',
|
||||
transition: 'all .15s',
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
height: '68px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const headerLeftSideItem = style({
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
bottom: '8px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const headerLeftSideOpen = style({
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
marginLeft: '20px',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -99,7 +146,21 @@ export const headerRightSide = style({
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
zIndex: 1,
|
||||
marginLeft: '20px',
|
||||
justifyContent: 'flex-end',
|
||||
transition: 'all .15s',
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
position: 'absolute',
|
||||
height: 'auto',
|
||||
right: '0',
|
||||
bottom: '8px',
|
||||
marginRight: '18px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const headerRightSideWindow = style({
|
||||
marginRight: '140px',
|
||||
});
|
||||
|
||||
export const browserWarning = style({
|
||||
@@ -131,22 +192,12 @@ export const closeButton = style({
|
||||
});
|
||||
|
||||
export const switchWrapper = style({
|
||||
position: 'absolute',
|
||||
right: '100%',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const searchArrowWrapper = style({
|
||||
position: 'absolute',
|
||||
left: 'calc(100% + 4px)',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
@@ -164,16 +215,13 @@ export const allPageListTitleWrapper = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
margin: '0 1px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
alignItems: 'flex-start',
|
||||
marginTop: '8px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const pageListTitleIcon = style({
|
||||
@@ -220,30 +268,36 @@ export const windowAppControlsWrapper = style({
|
||||
gap: '2px',
|
||||
transform: 'translateX(8px)',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
right: '14px',
|
||||
});
|
||||
|
||||
export const windowAppControl = style({
|
||||
WebkitAppRegion: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
width: '42px',
|
||||
height: 'calc(100% - 10px)',
|
||||
paddingTop: '10px',
|
||||
width: '51px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '0',
|
||||
selectors: {
|
||||
'&[data-type="close"]': {
|
||||
width: '56px',
|
||||
paddingRight: '14px',
|
||||
marginRight: '-14px',
|
||||
paddingRight: '5px',
|
||||
marginRight: '-12px',
|
||||
},
|
||||
'&[data-type="close"]:hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
color: '#FFFFFF',
|
||||
background: 'var(--affine-windows-close-button)',
|
||||
color: 'var(--affine-pure-white)',
|
||||
},
|
||||
'&:hover': {
|
||||
background: 'var(--affine-background-tertiary-color)',
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
'@container': {
|
||||
[`${headerVanillaContainer} (max-width: 900px)`]: {
|
||||
height: '50px',
|
||||
paddingTop: '0',
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -12,9 +13,7 @@ import {
|
||||
editorItemsAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/plugin-infra/atom';
|
||||
import type { CallbackMap } from '@toeverything/plugin-infra/entry';
|
||||
import type { 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';
|
||||
@@ -24,7 +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 { 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';
|
||||
|
||||
@@ -191,7 +190,12 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
</Suspense>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel defaultSize={100 - node.splitPercentage}>
|
||||
<Panel
|
||||
defaultSize={100 - node.splitPercentage}
|
||||
style={{
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
>
|
||||
<Suspense>
|
||||
<LayoutPanel node={node.second} editorProps={props.editorProps} />
|
||||
</Suspense>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,11 +3,12 @@ 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 { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
|
||||
|
||||
@@ -22,18 +23,23 @@ export const AddCollectionButton = ({
|
||||
const setting = useCollectionManager(workspace.id);
|
||||
const t = useAFFiNEI18N();
|
||||
const [show, showUpdateCollection] = useState(false);
|
||||
const defaultCollection = {
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
pinned: true,
|
||||
filterList: [],
|
||||
workspaceId: workspace.id,
|
||||
};
|
||||
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={() => showUpdateCollection(true)}
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
>
|
||||
<PlusIcon />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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/atom';
|
||||
} 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/atom';
|
||||
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';
|
||||
@@ -27,8 +26,8 @@ import {
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import { currentWorkspaceIdAtom } from '@toeverything/plugin-infra/atom';
|
||||
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, useMemo } from 'react';
|
||||
@@ -260,7 +259,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
|
||||
<HelpIsland showList={pageId ? undefined : showList} />
|
||||
</ToolContainer>
|
||||
<AffineWatermark />
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
<PageListTitleCellDragOverlay />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, displayFlex, styled } from '@affine/component';
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
|
||||
import { rootStore } from '@toeverything/plugin-infra/atom';
|
||||
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';
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/plugin-infra/__internal__/workspace';
|
||||
import { currentPageIdAtom, rootStore } from '@toeverything/plugin-infra/atom';
|
||||
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';
|
||||
|
||||
@@ -7,12 +7,12 @@ 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, rootStore } from '@toeverything/plugin-infra/atom';
|
||||
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import { type ReactElement, useCallback, useEffect } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
@@ -27,7 +27,7 @@ const DetailPageImpl = (): 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);
|
||||
});
|
||||
@@ -71,7 +71,7 @@ const DetailPageImpl = (): 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,7 @@ export const DetailPage = (): ReactElement => {
|
||||
const page =
|
||||
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
navigate('/404');
|
||||
jumpTo404();
|
||||
} else {
|
||||
// fixme: cleanup jumpOnce in the right time
|
||||
if (page.meta.jumpOnce) {
|
||||
@@ -106,8 +106,8 @@ export const DetailPage = (): ReactElement => {
|
||||
currentPageId,
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
currentWorkspace.id,
|
||||
jumpTo404,
|
||||
location.pathname,
|
||||
navigate,
|
||||
pageId,
|
||||
setCurrentPageId,
|
||||
workspaceId,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
currentWorkspaceIdAtom,
|
||||
rootStore,
|
||||
} from '@toeverything/plugin-infra/atom';
|
||||
import { currentWorkspaceIdAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import type { ReactElement } from 'react';
|
||||
import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
|
||||
|
||||
|
||||
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/atom';
|
||||
} 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,
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": ["webpack-env"]
|
||||
"outDir": "lib",
|
||||
"typeRoots": ["../../node_modules", "../../node_modules/@types"],
|
||||
"types": ["webpack-env", "ses", "affine__env"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/docs",
|
||||
"version": "0.7.0-canary.55",
|
||||
"version": "0.8.0-canary.11",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -10,18 +10,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"express": "^4.18.2",
|
||||
"jotai": "^2.2.2",
|
||||
"react": "18.3.0-canary-1fdacbefd-20230630",
|
||||
"react-dom": "18.3.0-canary-1fdacbefd-20230630",
|
||||
"react-server-dom-webpack": "18.3.0-canary-1fdacbefd-20230630",
|
||||
"waku": "0.12.1"
|
||||
"waku": "0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.17",
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineRouter(
|
||||
async id => {
|
||||
switch (id) {
|
||||
case 'index': {
|
||||
const { default: AppCreator } = await import('./src/app.js');
|
||||
const { default: AppCreator } = await import('./app.js');
|
||||
return AppCreator(id);
|
||||
}
|
||||
default:
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<!--/placeholder1-->
|
||||
<script src="./src/index.tsx" defer type="module"></script>
|
||||
<script src="./index.tsx" defer type="module"></script>
|
||||
<!--placeholder2-->
|
||||
<!--/placeholder2-->
|
||||
</body>
|
||||
Binary file not shown.
@@ -10,7 +10,8 @@
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
},
|
||||
"include": ["src", "entries.ts"],
|
||||
"references": [
|
||||
|
||||
@@ -27,8 +27,8 @@ if (platform() === 'darwin') {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForSelector('v-line');
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', 'test1', {
|
||||
await page.focus('.affine-doc-page-block-title');
|
||||
await page.type('.affine-doc-page-block-title', 'test1', {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
@@ -36,8 +36,8 @@ if (platform() === 'darwin') {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForSelector('v-line');
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', 'test2', {
|
||||
await page.focus('.affine-doc-page-block-title');
|
||||
await page.type('.affine-doc-page-block-title', 'test2', {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
@@ -45,14 +45,14 @@ if (platform() === 'darwin') {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForSelector('v-line');
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', 'test3', {
|
||||
await page.focus('.affine-doc-page-block-title');
|
||||
await page.type('.affine-doc-page-block-title', 'test3', {
|
||||
delay: 100,
|
||||
});
|
||||
}
|
||||
{
|
||||
const title = (await page
|
||||
.locator('.affine-default-page-block-title')
|
||||
.locator('.affine-doc-page-block-title')
|
||||
.textContent()) as string;
|
||||
expect(title.trim()).toBe('test3');
|
||||
}
|
||||
@@ -63,7 +63,7 @@ if (platform() === 'darwin') {
|
||||
await page.waitForTimeout(1000);
|
||||
{
|
||||
const title = (await page
|
||||
.locator('.affine-default-page-block-title')
|
||||
.locator('.affine-doc-page-block-title')
|
||||
.textContent()) as string;
|
||||
expect(title.trim()).toBe('test1');
|
||||
}
|
||||
@@ -73,7 +73,7 @@ if (platform() === 'darwin') {
|
||||
await page.waitForTimeout(1000);
|
||||
{
|
||||
const title = (await page
|
||||
.locator('.affine-default-page-block-title')
|
||||
.locator('.affine-doc-page-block-title')
|
||||
.textContent()) as string;
|
||||
expect(title.trim()).toBe('test3');
|
||||
}
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
@@ -29,7 +29,6 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
|
||||
|
||||
// move db file to tmp folder
|
||||
await page.evaluate(tmpPath => {
|
||||
// @ts-expect-error
|
||||
window.apis?.dialog.setFakeDialogResult({
|
||||
filePath: tmpPath,
|
||||
});
|
||||
@@ -47,6 +46,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-doc-page-block-title');
|
||||
await page.fill('.affine-doc-page-block-title', 'test1');
|
||||
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
@@ -69,7 +71,6 @@ test('export then add', async ({ page, appInfo, workspace }) => {
|
||||
|
||||
// export db file to tmp folder
|
||||
await page.evaluate(tmpPath => {
|
||||
// @ts-expect-error
|
||||
window.apis?.dialog.setFakeDialogResult({
|
||||
filePath: tmpPath,
|
||||
});
|
||||
@@ -89,7 +90,6 @@ test('export then add', async ({ page, appInfo, workspace }) => {
|
||||
await page.getByTestId('add-or-new-workspace').click();
|
||||
|
||||
await page.evaluate(tmpPath => {
|
||||
// @ts-expect-error
|
||||
window.apis?.dialog.setFakeDialogResult({
|
||||
filePath: tmpPath,
|
||||
});
|
||||
@@ -108,4 +108,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,63 +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',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@reforged/maker-appimage',
|
||||
config: {
|
||||
name: 'AFFiNE',
|
||||
iconUrl: icoPath,
|
||||
setupIcon: icoPath,
|
||||
platforms: ['linux'],
|
||||
options: {
|
||||
bin: productName,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
makers,
|
||||
hooks: {
|
||||
readPackageJson: async (_, packageJson) => {
|
||||
// we want different package name for canary build
|
||||
@@ -124,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.55",
|
||||
"version": "0.8.0-canary.11",
|
||||
"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,21 +27,20 @@
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
|
||||
"@affine/sdk": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230804190636-37f66904-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230804190636-37f66904-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",
|
||||
"@reforged/maker-appimage": "^3.3.1",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -54,11 +54,12 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@ export const config = () => {
|
||||
resolve(electronDir, './src/main/index.ts'),
|
||||
resolve(electronDir, './src/preload/index.ts'),
|
||||
resolve(electronDir, './src/helper/index.ts'),
|
||||
resolve(electronDir, './src/worker/plugin.ts'),
|
||||
],
|
||||
entryNames: '[dir]',
|
||||
outdir: resolve(electronDir, './dist'),
|
||||
|
||||
@@ -3,10 +3,11 @@ import { readdir } from 'node:fs/promises';
|
||||
|
||||
const outputRoot = fileURLToPath(
|
||||
new URL(
|
||||
'../zip-out/AFFiNE-canary.app/Contents/Resources/app',
|
||||
'../out/canary/AFFiNE-canary-darwin-arm64/AFFiNE-canary.app/Contents/Resources/app',
|
||||
import.meta.url
|
||||
)
|
||||
);
|
||||
|
||||
const outputList = [
|
||||
[
|
||||
'dist',
|
||||
@@ -19,7 +20,7 @@ const outputList = [
|
||||
],
|
||||
],
|
||||
['dist/plugins', ['bookmark']],
|
||||
['dist/plugins/bookmark', ['index.js']],
|
||||
['dist/plugins/bookmark', ['index.cjs']],
|
||||
] as [entry: string, expected: string[]][];
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set the directory
|
||||
dir="./out/canary/make/zip/darwin/arm64"
|
||||
|
||||
# Get the first file
|
||||
file=$(ls -1 $dir | head -n 1)
|
||||
|
||||
# Check if file exists and is a zip file
|
||||
if [ -f "$dir/$file" ] && [ ${file: -4} == ".zip" ]
|
||||
then
|
||||
# Unzip the file
|
||||
unzip "$dir/$file" -d "zip-out"
|
||||
else
|
||||
echo "No zip file found"
|
||||
fi
|
||||
@@ -115,7 +115,11 @@ function startPollingSecondaryDB(db: WorkspaceSQLiteDB) {
|
||||
const secondaryDB = new SecondaryWorkspaceSQLiteDB(path, db);
|
||||
return new Observable<SecondaryWorkspaceSQLiteDB>(subscriber => {
|
||||
subscriber.next(secondaryDB);
|
||||
return () => secondaryDB.destroy();
|
||||
return () => {
|
||||
secondaryDB.destroy().catch(err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
};
|
||||
});
|
||||
}),
|
||||
switchMap(secondaryDB => {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { ValidationResult } from '@affine/native';
|
||||
import type {
|
||||
FakeDialogResult,
|
||||
LoadDBFileResult,
|
||||
MoveDBFileResult,
|
||||
SaveDBFileResult,
|
||||
SelectDBFileLocationResult,
|
||||
} from '@toeverything/infra/type';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
@@ -28,13 +35,6 @@ export async function revealDBFile(workspaceId: string) {
|
||||
await mainRPC.showItemInFolder(meta.secondaryDBPath ?? meta.mainDBPath);
|
||||
}
|
||||
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
export interface FakeDialogResult {
|
||||
canceled?: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
}
|
||||
|
||||
// result will be used in the next call to showOpenDialog
|
||||
// if it is being read once, it will be reset to undefined
|
||||
let fakeDialogResult: FakeDialogResult | undefined = undefined;
|
||||
@@ -53,23 +53,6 @@ export function setFakeDialogResult(result: FakeDialogResult | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorMessages = [
|
||||
'DB_FILE_ALREADY_LOADED',
|
||||
'DB_FILE_PATH_INVALID',
|
||||
'DB_FILE_INVALID',
|
||||
'DB_FILE_MIGRATION_FAILED',
|
||||
'FILE_ALREADY_EXISTS',
|
||||
'UNKNOWN_ERROR',
|
||||
] as const;
|
||||
|
||||
type ErrorMessage = (typeof ErrorMessages)[number];
|
||||
|
||||
export interface SaveDBFileResult {
|
||||
filePath?: string;
|
||||
canceled?: boolean;
|
||||
error?: ErrorMessage;
|
||||
}
|
||||
|
||||
const extension = 'affine';
|
||||
|
||||
function getDefaultDBFileName(name: string, id: string) {
|
||||
@@ -125,12 +108,6 @@ export async function saveDBFileAs(
|
||||
}
|
||||
}
|
||||
|
||||
export interface SelectDBFileLocationResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export async function selectDBFileLocation(): Promise<SelectDBFileLocationResult> {
|
||||
try {
|
||||
const ret =
|
||||
@@ -157,12 +134,6 @@ export async function selectDBFileLocation(): Promise<SelectDBFileLocationResult
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadDBFileResult {
|
||||
workspaceId?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the user clicks the "Load" button in the "Load Workspace" dialog.
|
||||
*
|
||||
@@ -255,12 +226,6 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface MoveDBFileResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the user clicks the "Move" button in the "Move Workspace Storage" setting.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { app, Menu } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { revealLogFile } from '../logger';
|
||||
import { checkForUpdates } from '../updater';
|
||||
import { isMacOS } from '../utils';
|
||||
import { applicationMenuSubjects } from './subject';
|
||||
|
||||
// Unique id for menuitems
|
||||
|
||||
@@ -40,16 +40,15 @@ export async function savePDFFileAs(
|
||||
|
||||
await BrowserWindow.getFocusedWindow()
|
||||
?.webContents.printToPDF({
|
||||
margins: {
|
||||
marginType: 'custom',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
pageSize: 'A4',
|
||||
margins: {
|
||||
bottom: 0.5,
|
||||
},
|
||||
printBackground: true,
|
||||
landscape: false,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: getFootTemple(),
|
||||
})
|
||||
.then(data => {
|
||||
fs.writeFile(filePath, data, error => {
|
||||
@@ -65,3 +64,27 @@ export async function savePDFFileAs(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFootTemple(): string {
|
||||
const logo = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="53" height="12" viewBox="0 0 53 12" fill="none">
|
||||
<path d="M18.9256 0.709372C18.8749 0.504937 18.6912 0.361572 18.4807 0.361572H17.77C17.5595 0.361572 17.3758 0.504937 17.3252 0.709372L14.9153 10.4283C14.8438 10.7172 15.0621 10.9965 15.3601 10.9965H15.6052C15.8183 10.9965 16.0033 10.8497 16.0513 10.6423L16.5646 8.43721C16.6127 8.22974 16.7976 8.08291 17.0107 8.08291H19.2396C19.4527 8.08291 19.6376 8.22974 19.6857 8.43721L20.199 10.6423C20.247 10.8497 20.432 10.9965 20.6451 10.9965H20.8902C21.1878 10.9965 21.4065 10.7172 21.335 10.4283L18.9251 0.709372H18.9256ZM18.7891 7.0629H17.4616C17.1666 7.0629 16.9483 6.7883 17.0155 6.50113L17.9025 2.23181C17.9575 1.99576 18.2936 1.99576 18.3486 2.23181L19.2357 6.50113C19.3024 6.7883 19.0845 7.0629 18.7896 7.0629H18.7891Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M36.2654 5.00861H30.766C30.5131 5.00861 30.3078 4.8033 30.3078 4.55036V2.25132C30.3078 1.77055 30.6976 1.38074 31.1783 1.38074H33.8997C34.1526 1.38074 34.3579 1.17544 34.3579 0.922494V0.818977C34.3579 0.566031 34.1526 0.36073 33.8997 0.36073H30.8539C29.8924 0.36073 29.1132 1.14036 29.1132 2.10146V5.00774H24.2171C23.9642 5.00774 23.7589 4.80244 23.7589 4.54949V2.25046C23.7589 1.76969 24.1487 1.37988 24.6295 1.37988H27.3508C27.6038 1.37988 27.8091 1.17457 27.8091 0.921628V0.818111C27.8091 0.565165 27.6038 0.359863 27.3508 0.359863H24.3051C23.3435 0.359863 22.5643 1.13949 22.5643 2.1006V10.5366C22.5643 10.7895 22.7696 10.9948 23.0226 10.9948H23.3011C23.554 10.9948 23.7593 10.7895 23.7593 10.5366V6.48513C23.7593 6.23219 23.9646 6.02689 24.2176 6.02689H29.1136V10.5366C29.1136 10.7895 29.3189 10.9948 29.5719 10.9948H29.8504C30.1033 10.9948 30.3086 10.7895 30.3086 10.5366V6.48513C30.3086 6.23219 30.5139 6.02689 30.7669 6.02689H35.9713C36.4521 6.02689 36.8419 6.4167 36.8419 6.89747V10.5418C36.8419 10.7947 37.0472 11 37.3001 11H37.5492C37.8021 11 38.0074 10.7947 38.0074 10.5418V6.74804C38.0074 5.7865 37.2278 5.00731 36.2667 5.00731L36.2654 5.00861Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M45.2871 0.361517H45.0363C44.7838 0.361517 44.5789 0.565953 44.5781 0.818032L44.5504 9.53946L42.0512 0.695024C41.9954 0.497519 41.8156 0.361517 41.6103 0.361517H40.521C40.268 0.361517 40.0627 0.566819 40.0627 0.819765V10.5387C40.0627 10.7916 40.268 10.9969 40.521 10.9969H40.7718C41.0243 10.9969 41.2292 10.7925 41.23 10.5404L41.2577 1.81899L43.7569 10.6634C43.8128 10.8609 43.9925 10.9969 44.1978 10.9969H45.2871C45.5401 10.9969 45.7454 10.7916 45.7454 10.5387V0.819331C45.7454 0.566386 45.5401 0.361084 45.2871 0.361084V0.361517Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M49.2307 1.3811H51.8212C52.0741 1.3811 52.2794 1.17579 52.2794 0.922849V0.819331C52.2794 0.566386 52.0741 0.361084 51.8212 0.361084H48.9214C47.9599 0.361084 47.1807 1.14071 47.1807 2.10182V9.25489C47.1807 10.2164 47.9603 10.9956 48.9214 10.9956H51.8212C52.0741 10.9956 52.2794 10.7903 52.2794 10.5374V10.4339C52.2794 10.1809 52.0741 9.97562 51.8212 9.97562H49.2307C48.7499 9.97562 48.3601 9.5858 48.3601 9.10503V6.33996C48.3601 6.08701 48.5654 5.88171 48.8183 5.88171H51.6752C51.9282 5.88171 52.1335 5.67641 52.1335 5.42346V5.31994C52.1335 5.067 51.9282 4.8617 51.6752 4.8617H48.8183C48.5654 4.8617 48.3601 4.65639 48.3601 4.40345V2.24995C48.3601 1.76918 48.7499 1.37936 49.2307 1.37936V1.3811Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M37.3088 1.65787C37.1052 1.4543 36.7583 1.54742 36.6838 1.82549L36.3473 3.08199C36.2728 3.35962 36.527 3.61387 36.8051 3.5398L38.0616 3.20326C38.3396 3.12876 38.4323 2.7814 38.2292 2.57826L37.3097 1.65873L37.3088 1.65787Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M11.9043 9.92891C11.8624 9.85125 11.3139 8.91339 11.2674 8.82602C9.92288 6.49855 7.98407 3.13718 6.64195 0.814557C6.37775 0.29657 5.6448 0.286862 5.36882 0.795141C3.92685 3.2932 1.71414 7.12436 0.274242 9.6193C0.214607 9.72505 0.118915 9.88037 0.0617076 9.99687C-0.035025 10.2063 -0.0163025 10.4677 0.10574 10.6608C0.238877 10.8858 0.502032 11.0165 0.759985 11.0027H0.8269C2.06986 11.0009 9.86983 11.0048 11.2844 11.0027C11.8329 11.0041 12.1779 10.4022 11.904 9.92926L11.9043 9.92891ZM6.09068 1.66053C6.91793 3.09661 7.8967 4.79099 8.85535 6.45036C8.47016 6.21355 8.05792 6.02875 7.6169 5.91711C7.49763 5.89007 7.04136 5.83529 6.94289 5.83113C6.9207 5.82939 6.89851 5.82835 6.87598 5.82835H5.12474C4.97288 5.82835 4.82622 5.86753 4.69655 5.93757C4.53325 5.14845 4.68199 4.26468 4.8914 3.51683C4.90978 3.45269 4.92954 3.38889 4.9493 3.3251C5.29636 2.7239 5.62366 2.15737 5.91073 1.65984C5.95095 1.5905 6.0508 1.59084 6.09068 1.65984V1.66053ZM6.15412 8.25707C6.15412 8.25707 6.12327 8.30492 6.09692 8.34583C6.07161 8.37079 6.03728 8.38535 5.99984 8.38535C5.94956 8.38535 5.90484 8.35935 5.87988 8.31601L5.03841 6.85809C5.03841 6.85809 5.0124 6.80747 4.98987 6.76413C4.98085 6.72946 4.98571 6.69271 5.00408 6.66046C5.02905 6.61712 5.07412 6.59112 5.12404 6.59112H6.80733C6.80733 6.59112 6.86419 6.59389 6.91308 6.59632C6.9474 6.60568 6.97722 6.62822 6.99559 6.66046C7.02056 6.7038 7.02056 6.75581 6.99559 6.79915L6.15378 8.25707H6.15412ZM1.12681 9.94521C1.30502 9.63733 1.53766 9.23653 1.5914 9.14084C2.1971 8.09169 3.04585 6.62163 3.89252 5.15539C3.88004 5.60715 3.92616 6.05684 4.04993 6.49473C4.08599 6.61158 4.26697 7.03422 4.31274 7.12159C4.32591 7.14967 4.34256 7.17914 4.35851 7.20619L5.21939 8.69739C5.29532 8.8288 5.40245 8.93628 5.52796 9.01359C4.92607 9.54961 4.08634 9.86269 3.33397 10.0551C3.27191 10.0707 3.20916 10.0849 3.14675 10.0988C2.34827 10.0988 1.66837 10.0995 1.21695 10.1009C1.13651 10.1009 1.08659 10.0142 1.12681 9.94486V9.94521ZM10.7834 10.1016C9.65661 10.1026 7.37212 10.1005 5.25475 10.0995C5.65139 9.88453 6.01683 9.62034 6.33337 9.29478C6.41624 9.20498 6.69222 8.83712 6.74492 8.75391C6.7626 8.72825 6.77994 8.69913 6.79519 8.67208L7.65608 7.18088C7.73201 7.04947 7.77153 6.90281 7.77569 6.75546C8.54089 7.00856 9.23188 7.57925 9.77449 8.13468C9.82129 8.18322 9.86706 8.23245 9.91248 8.28203C10.2464 8.86035 10.5695 9.41994 10.8729 9.9459C10.9127 10.0152 10.8632 10.1019 10.7831 10.1019L10.7834 10.1016Z" fill="black" fill-opacity="0.1"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const footerTemp = `
|
||||
<div style="font-size: 14px; width: 100%; display: flex; justify-content: flex-end; margin-right: 40px;">
|
||||
<a href="https://affine.pro" style="display: flex; text-decoration: none; color: rgba(0, 0, 0, 0.1);">
|
||||
<span>Created with</span>
|
||||
<div style="display: flex; align-items: center;">${logo}</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return footerTemp;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
type WebContents,
|
||||
} from 'electron';
|
||||
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
import { logger } from './logger';
|
||||
import { MessageEventChannel } from './utils';
|
||||
|
||||
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ app.on('second-instance', () => {
|
||||
);
|
||||
});
|
||||
|
||||
app.on('open-url', async (_, _url) => {
|
||||
app.on('open-url', (_, _url) => {
|
||||
// todo: handle `affine://...` urls
|
||||
});
|
||||
|
||||
@@ -54,7 +54,11 @@ app.on('window-all-closed', () => {
|
||||
/**
|
||||
* @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate'
|
||||
*/
|
||||
app.on('activate', restoreOrCreateWindow);
|
||||
app.on('activate', () => {
|
||||
restoreOrCreateWindow().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create app window when background process will be ready
|
||||
|
||||
@@ -2,6 +2,7 @@ import { shell } from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
export const logger = log.scope('main');
|
||||
export const pluginLogger = log.scope('plugin');
|
||||
log.initialize();
|
||||
|
||||
export function getLogFilePath() {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { isMacOS, isWindows } from '../shared/utils';
|
||||
import { getExposedMeta } from './exposed';
|
||||
import { ensureHelperProcess } from './helper-process';
|
||||
import { logger } from './logger';
|
||||
import { isMacOS, isWindows } from './utils';
|
||||
|
||||
const IS_DEV: boolean =
|
||||
process.env.NODE_ENV === 'development' && !process.env.CI;
|
||||
@@ -114,6 +114,7 @@ async function createWindow() {
|
||||
|
||||
// singleton
|
||||
let browserWindow: Electron.BrowserWindow | undefined;
|
||||
|
||||
/**
|
||||
* Restore existing BrowserWindow or Create new BrowserWindow
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { join, resolve } from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import { logger } from '@affine/electron/main/logger';
|
||||
import { logger, pluginLogger } from '@affine/electron/main/logger';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
import { ipcMain } from 'electron';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
|
||||
const builtInPlugins = ['bookmark'];
|
||||
|
||||
declare global {
|
||||
// fixme(himself65):
|
||||
@@ -10,26 +17,41 @@ declare global {
|
||||
var asyncCall: Record<string, (...args: any) => PromiseLike<any>>;
|
||||
}
|
||||
|
||||
export function registerPlugin() {
|
||||
export async function registerPlugin() {
|
||||
logger.info('import plugin manager');
|
||||
globalThis.asyncCall = {};
|
||||
const bookmarkPluginPath = join(
|
||||
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
|
||||
'./bookmark/index.js'
|
||||
const asyncCall = AsyncCall<
|
||||
Record<string, (...args: any) => PromiseLike<any>>
|
||||
>(
|
||||
{
|
||||
log: (...args: any[]) => {
|
||||
pluginLogger.log(...args);
|
||||
},
|
||||
},
|
||||
{
|
||||
channel: new MessageEventChannel(
|
||||
new Worker(resolve(__dirname, './worker.js'), {})
|
||||
),
|
||||
}
|
||||
);
|
||||
globalThis.asyncCall = asyncCall;
|
||||
await Promise.all(
|
||||
builtInPlugins.map(async plugin => {
|
||||
const pluginPackageJsonPath = join(
|
||||
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
|
||||
`./${plugin}/package.json`
|
||||
);
|
||||
logger.info(`${plugin} plugin path:`, pluginPackageJsonPath);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(pluginPackageJsonPath, 'utf-8')
|
||||
);
|
||||
console.log('packageJson', packageJson);
|
||||
const serverCommand: string[] = packageJson.affinePlugin.serverCommand;
|
||||
serverCommand.forEach(command => {
|
||||
ipcMain.handle(command, async (_, ...args) => {
|
||||
logger.info(`plugin ${plugin} called`);
|
||||
return asyncCall[command](...args);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
logger.info('bookmark plugin path:', bookmarkPluginPath);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { entry } = require(bookmarkPluginPath);
|
||||
|
||||
entry({
|
||||
registerCommand: (command: string, handler: (...args: any[]) => any) => {
|
||||
logger.info('register plugin command', command);
|
||||
ipcMain.handle(command, (event, ...args) => handler(...args));
|
||||
globalThis.asyncCall[command] = handler;
|
||||
},
|
||||
registerCommands: (command: string) => {
|
||||
ipcMain.removeHandler(command);
|
||||
delete globalThis.asyncCall[command];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user