Compare commits

..

108 Commits

Author SHA1 Message Date
Alex Yang
36bd0b02d0 v0.8.0-canary.11 2023-08-04 17:34:31 -07:00
JimmFly
3a92c4f798 feat: modify sidebar floating logic and header responsive style (#3550) 2023-08-05 00:15:17 +00:00
Alex Yang
97de0ef5b4 build: use tsconfig bundler (#3581) 2023-08-05 00:00:36 +00:00
Alex Yang
bbf5f0efe0 chore: bump version (#3567) 2023-08-04 23:55:28 +00:00
Alex Yang
ea76936508 feat: update 404 page (#3580)
Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com>
2023-08-04 23:11:30 +00:00
Alex Yang
f076cb0ead docs: fix all-contributors count 2023-08-04 13:48:43 -07:00
fossabot
0f230253a8 docs: add license scan report and status (#3576)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-04 13:44:15 -07:00
Alex Yang
13d2dde501 fix: only run migration in local workspace (#3570) 2023-08-04 16:09:55 +00:00
Garfield Lee
65fc0ed59c refactor: remove React.FC for component package (#3575) 2023-08-04 15:00:28 +00:00
Chi Zhang
7ec4b8fb8c Update README.md (#3578) 2023-08-04 14:46:10 +00:00
Peng Xiao
6415f0093b fix: optimize types for infra/electron (#3574) 2023-08-04 09:23:56 +00:00
LongYinan
5795020403 style: add no-misused-promises rule (#3547)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-08-04 08:08:10 +00:00
Alex Yang
f8e49ee3be v0.8.0-canary.10 2023-08-03 20:08:52 -07:00
Pratik Kumar
1012807c65 fix: added scrollbar at the correct position (#3506)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-08-04 02:50:22 +00:00
Alex Yang
1c7c27712e ci: update cancel.yml 2023-08-03 19:11:32 -07:00
Alex Yang
7f28c78d8c ci: skip build in the non-darwin platform 2023-08-03 18:12:00 -07:00
Alex Yang
4bb874756d refactor: lazy download macos maker (#3564) 2023-08-03 17:58:42 -07:00
Alex Yang
0882d47dc9 v0.8.0-canary.9 2023-08-03 16:23:02 -07:00
Alex Yang
0c16eb1189 build: improve webpack config (#3561) 2023-08-03 23:05:46 +00:00
Alex Yang
f2ac4c7eda fix(core): editor wrapper css (#3563) 2023-08-03 21:40:43 +00:00
Alex Yang
0d531782ca ci: add dependabot.yml (#3562) 2023-08-03 12:24:23 -07:00
Alex Yang
47ff376195 ci: improve download @sentry/cli (#3560) 2023-08-03 17:26:30 +00:00
Alex Yang
33cc9d25a1 fix(core): use download atom (#3558) 2023-08-03 17:13:44 +00:00
Alex Yang
58ceeb9c5f chore: ignore output files (#3557) 2023-08-03 16:24:58 +00:00
Camol
3c00b69805 chore: remove repeated inreferences (#3551) 2023-08-03 16:11:51 +00:00
Chi Zhang
2678ca9330 feat: should hide downloadtip when it had been closed (#3555) 2023-08-03 23:50:08 +08:00
Peng Xiao
6f488d963b fix: a possible double connect issue (#3552) 2023-08-03 13:45:00 +00:00
JimmFly
8face25bdf fix: scrollbar position offset (#3538) 2023-08-03 08:52:04 +00:00
Alex Yang
0e32803247 refactor: merge plugin-infra into infra (#3540) 2023-08-03 08:48:59 +00:00
Pratik Kumar
3282344d4a fix: padding in the Switch button of Page/Edgeless (#3542) 2023-08-03 08:38:00 +00:00
Garfield Lee
78d23d86f5 feat: add tooltips for collection bar action buttons (#3545)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-03 08:26:17 +00:00
Garfield Lee
3a0797955c fix: editor-mode-switch animation should only run once (#3543)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-03 08:24:10 +00:00
Alex Yang
9449e66396 ci: fix upload artifact 2023-08-03 01:11:32 -07:00
Alex Yang
b6200ab56d ci: build storage 2023-08-03 00:32:07 -07:00
Alex Yang
ff23561e21 ci: fix needs 2023-08-03 00:25:31 -07:00
Alex Yang
e718428d50 ci: fix server build (#3541) 2023-08-03 07:10:02 +00:00
Alex Yang
ea34d66e14 feat: add @affine/sdk (#3536) 2023-08-03 04:47:05 +00:00
Alex Yang
d3c719d89a test: add test case for plugin bootstrap (#3529) 2023-08-03 01:48:35 +00:00
Alex Yang
dcd070b3e7 v0.8.0-canary.8 2023-08-02 16:51:34 -07:00
Alex Yang
32c08e49c5 feat: migrate to database v3 (#3528) 2023-08-02 16:50:10 -07:00
Garfield Lee
28a496bc67 feat: update editor mode switch icons (#3526) 2023-08-02 10:08:45 -07:00
Alex Yang
4386894e8a v0.8.0-canary.7 2023-08-02 10:06:05 -07:00
Alex Yang
33613c7041 fix(electron): check bundle (#3527) 2023-08-02 15:56:00 +00:00
Garfield Lee
db1b4d48b8 docs: update docs for build plugins (#3525) 2023-08-02 08:32:22 -07:00
Alex Yang
f007e2cecb ci: fix setup maker (#3519) 2023-08-02 05:03:05 +00:00
Peng Xiao
7e4df4c3d1 fix: stackoverflow issue in empty page (#3518) 2023-08-02 04:29:49 +00:00
Alex Yang
03b98b433b fix: drag workspace (#3513) 2023-08-01 23:29:17 +00:00
Alex Yang
1b17743ed3 feat: custom maker dmg (#3501) 2023-08-01 19:20:29 +00:00
Alex Yang
03f12f6aa4 feat: add filter schema (#3479) 2023-08-01 19:13:24 +00:00
Alex Yang
0176d66a94 v0.8.0-canary.6 2023-08-01 11:50:32 -07:00
Alex Yang
f078154b9b chore: bump version (#3489) 2023-08-01 11:30:56 -07:00
Peng Xiao
35a4c63c27 fix: flaky tests (#3507) 2023-08-01 14:13:04 +00:00
JimmFly
70f3508005 feat: brand new version of icons (#3496) 2023-08-01 05:51:30 +00:00
Alex Yang
16e22e614b feat: init @affine/worker (#3495) 2023-08-01 05:39:37 +00:00
Chi Zhang
c8b2728e27 feat: add placeholder for OPENAI_API_KEY input (#3486) 2023-07-31 17:35:53 +00:00
Alex Yang
452c780d40 refactor(i18n): language setup (#3484) 2023-07-31 09:21:12 +00:00
JimmFly
9567471e7f chore: adjustment options menu (#3455) 2023-07-31 07:56:51 +00:00
Alex Yang
4d4923cd37 v0.8.0-canary.5 2023-07-31 00:54:03 -07:00
Alex Yang
e85404a9c5 build: enable plugin system in production (#3480) 2023-07-31 06:52:11 +00:00
Alex Yang
1d43e46f99 chore: add noUnusedLocals and noUnusedParameters rules (#3476)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-07-31 05:47:37 +00:00
Peng Xiao
5a8c1dcb57 fix: flaky test (#3478) 2023-07-31 05:47:13 +00:00
Alex Yang
9ffc523443 v0.8.0-canary.4 2023-07-30 20:33:52 -07:00
Alex Yang
39693a19bd chore: bump version (#3471) 2023-07-31 02:59:54 +00:00
Alex Yang
18fcaff5ee feat(plugin-cli): add cli af (#3465) 2023-07-30 18:10:45 +00:00
Alex Yang
568d5e4cdf fix(core): lockdown twice 2023-07-30 08:09:05 -07:00
fourdim
99c24b5cd8 chore: add the missing d.ts file for y-indexeddb (#3467) 2023-07-30 13:19:59 +00:00
Alex Yang
a3087d14d8 chore: remove unused files (#3466) 2023-07-30 06:35:00 +00:00
Alex Yang
cc7de52caf build: improve webpack config (#3463) 2023-07-30 06:34:52 +00:00
Alex Yang
05865d51c6 docs: update plugins section 2023-07-29 19:49:58 -07:00
Alex Yang
45089e176f v0.8.0-canary.3 2023-07-29 19:40:22 -07:00
Alex Yang
00a41b95b9 feat(plugin-infra): esm simulation in browser (#3464) 2023-07-30 02:23:00 +00:00
Alex Yang
765efd19da v0.8.0-canary.2 2023-07-29 14:33:38 -07:00
Alex Yang
ac59e28fcd feat(plugin-infra): support worker thread in server side (#3462) 2023-07-29 20:57:23 +00:00
Alex Yang
77dab70ff7 feat(plugin-infra): init permission control (#3461) 2023-07-29 20:10:50 +00:00
Alex Yang
0b66e911b1 feat(plugin-infra): support esm bundler (#3460) 2023-07-29 19:07:32 +00:00
JimmFly
6388a798c9 style: adjust active slider bar collection item active style (#3458)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-29 15:22:21 +00:00
Alex Yang
c45149b664 v0.8.0-canary.1 2023-07-29 08:23:38 -07:00
Alex Yang
ce0c1c39e2 feat: improve copilot plugin (#3459) 2023-07-29 07:37:01 +00:00
Alex Yang
52809a2783 refactor: image preview plugin (#3457) 2023-07-29 00:18:28 -07:00
Alex Yang
be3909370e refactor(plugin-infra): split functions (#3451) 2023-07-28 22:28:10 -07:00
Alex Yang
f79733e5df feat(plugin-infra): add package.json schema (#3456) 2023-07-29 05:07:25 +00:00
Alex Yang
2d95de06d6 docs: update rustc version 2023-07-28 21:36:43 -07:00
Alex Yang
97502231a3 v0.8.0-canary.0 2023-07-28 20:18:00 -07:00
Alex Yang
d20a6d2677 chore: bump version (#3449) 2023-07-29 02:53:29 +00:00
Alex Yang
9f43c0ddc8 refactor: plugin loading logic (#3448) 2023-07-29 02:43:52 +00:00
Peng Xiao
4cb1bf6a9f test: add test for sub doc (#3444) 2023-07-28 15:15:32 +00:00
JimmFly
d96263fde9 feat: add read only mode for page in trash (#3440) 2023-07-28 15:01:10 +00:00
JimmFly
ed8b2d9927 chore: update change log link (#3435) 2023-07-28 15:00:03 +00:00
Alex Yang
7b3be389d4 v0.7.0-canary.59 2023-07-27 22:03:23 -07:00
JimmFly
68755f4303 fix: bring back the lost WorkspaceDeleteModal style (#3434) 2023-07-27 21:32:46 -07:00
Alex Yang
0e1f712dcc v0.7.0-canary.58 2023-07-27 20:33:14 -07:00
Alex Yang
0ab1cfdeb6 chore: split vitest (#3426) 2023-07-28 03:06:50 +00:00
Alex Yang
8185ee991b fix: serial build plugins (#3431) 2023-07-28 03:06:37 +00:00
Alex Yang
1001d7462a v0.7.0-canary.57 2023-07-27 17:58:21 -07:00
Alex Yang
f9929ebd61 fix: copilot not working (#3425) 2023-07-28 00:28:21 +00:00
Alex Yang
aa69a7cad2 v0.7.0-canary.56 2023-07-27 14:42:44 -07:00
JimmFly
4de063de98 style: adjust collection modal style (#3407) 2023-07-27 20:37:34 +00:00
Alex Yang
d765d0350d ci: add timeout (#3423) 2023-07-27 20:08:47 +00:00
Alex Yang
d2459a5837 fix(electron): plugin cannot found (#3418) 2023-07-27 19:55:19 +00:00
JimmFly
e1f604d857 refactor: create collection (#3406) 2023-07-27 19:55:04 +00:00
xiaodong zuo
af4e860176 fix: the exported pdf has part white background in dark mode (#3408) 2023-07-27 19:50:20 +00:00
Alex Yang
a3d665503f fix(core): delete page (#3419) 2023-07-27 18:12:11 +00:00
Alex Yang
b47fbde479 fix: improve navigate (#3420) 2023-07-27 18:06:30 +00:00
Pratik Kumar
115f46a4fa test: improve e2e coverage on page deletion (#3416) 2023-07-27 17:42:16 +00:00
Alex Yang
b0f8486ef2 docs: update plugin description 2023-07-27 10:48:45 -07:00
fourdim
57c27e6a4b fix: undefined allDb in firefox (#3417) 2023-07-27 16:30:09 +00:00
Subhadip Sarkar
f591939a6a docs: fix the Linux download button on the readme page (#3413) 2023-07-27 10:08:04 -07:00
Peng Xiao
2d41cce90f fix: sqlite db apply (#3409) 2023-07-27 07:06:06 -07:00
305 changed files with 8487 additions and 4224 deletions

View File

@@ -24,7 +24,8 @@
"debug",
"storage",
"infra",
"plugin-infra"
"plugin-cli",
"sdk"
]
]
}

View File

@@ -9,3 +9,6 @@ lib
.eslintrc.js
packages/i18n/src/i18n-generated.ts
e2e-dist-*
static
web-static
public

View File

@@ -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
View 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'

View File

@@ -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
View 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
View File

@@ -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/**/*'

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
versioning-strategy: increase
commit-message:
prefix: 'chore'

View File

@@ -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:

View File

@@ -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:

View File

@@ -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 -->
[![AFFiNE Web](<https://img.shields.io/badge/-Try%20It%20Online%20%E2%86%92-rgb(84,56,255)?style=flat-square&logoColor=white&logo=affine>)](https://app.affine.pro)
[![AFFiNE macOS M1/M2 Chip](https://img.shields.io/badge/-macOS_M_Chip%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://affine.pro/download)
[![AFFiNE macOS x64](https://img.shields.io/badge/-macOS_x86%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://affine.pro/download)
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://affine.pro/download)
[![AFFiNE Linux](https://img.shidelds.io/badge/-Linux%20%E2%86%92-yellow?style=flat-square&logo=linux&logoColor=white)](https://affine.pro/download)
[![AFFiNE Linux](https://img.shields.io/badge/-Linux%20%E2%86%92-yellow?style=flat-square&logo=linux&logoColor=white)](https://affine.pro/download)
[![Releases](https://img.shields.io/github/downloads/toeverything/AFFiNE/total)](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/)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE.svg?type=shield)](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
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE?ref=badge_large)

View File

@@ -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,

View File

@@ -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'),
},
],
}),
],

View File

@@ -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,

View File

@@ -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',
}),
],
});
}

View File

@@ -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",

View File

@@ -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"]
}
}
}

View File

@@ -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' });
});

View 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();

View File

@@ -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,

View File

@@ -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>

View File

@@ -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';

View File

@@ -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 });
}
);

View File

@@ -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';

View File

@@ -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);

View 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;
}

View 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());
},
};
}

View 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);
};

View File

@@ -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');
});

View 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');
}

View File

@@ -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';

View File

@@ -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();

View File

@@ -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']()}

View File

@@ -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',
};

View File

@@ -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']());

View File

@@ -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]());
}
})

View File

@@ -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 />

View File

@@ -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>
))}
</>
);

View File

@@ -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)',
});

View File

@@ -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>

View File

@@ -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',

View File

@@ -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';

View File

@@ -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>
);

View File

@@ -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',
},
};
});

View File

@@ -35,7 +35,7 @@ const HoverAnimateController = ({
>
{cloneElement(children, {
isStopped: !startAnimate,
speed: 5,
speed: 1,
width: 20,
height: 20,
})}

View File

@@ -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);
}}

View File

@@ -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';

View File

@@ -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>
);

View File

@@ -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>
)}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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,
},
],

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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}

View File

@@ -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>

View File

@@ -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(

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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];
});

View File

@@ -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';

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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 />

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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';

View 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,
});
}

View File

@@ -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,

View File

@@ -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"],

View File

@@ -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",

View File

@@ -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:

View File

@@ -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.

View File

@@ -10,7 +10,8 @@
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"include": ["src", "entries.ts"],
"references": [

View File

@@ -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')
);

View File

@@ -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();
});

View File

@@ -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,

View File

@@ -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",

View File

@@ -9,7 +9,7 @@
"executor": "nx:run-script",
"dependsOn": [
{
"projects": ["@affine/bookmark-block"],
"projects": ["tag:plugin"],
"target": "build",
"params": "ignore"
},

View File

@@ -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'),

View File

@@ -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(

View File

@@ -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

View File

@@ -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 => {

View File

@@ -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.
*

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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');

View File

@@ -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

View File

@@ -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() {

View File

@@ -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
*/

View File

@@ -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