Compare commits

..

132 Commits

Author SHA1 Message Date
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
Alex Yang
3b1aff1db1 v0.7.0-canary.55 2023-07-27 07:03:07 -07:00
Alex Yang
3a64b43032 fix(cli): create empty plugin directory 2023-07-27 07:02:06 -07:00
Alex Yang
59f53760d1 v0.7.0-canary.54 2023-07-27 05:58:20 -07:00
Alex Yang
2980c1afac fix: plugin not found (#3415) 2023-07-27 05:56:59 -07:00
Alex Yang
39054a7c3d v0.7.0-canary.53 2023-07-27 05:19:20 -07:00
Alex Yang
4b7e47e265 chore: bump blocksuite (#3404)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-07-27 05:37:38 +00:00
Peng Xiao
4e7824583d build: add AppImage build (#3401) 2023-07-26 22:38:01 -07:00
JimmFly
ba53c74130 fix: unable to add a second collection (#3405) 2023-07-26 22:37:42 -07:00
Qi
bc263e7afb feat: modify current workspace label to a dot (#3399) 2023-07-26 22:37:31 -07:00
JimmFly
bc27412425 feat: support gif toast (#3389) 2023-07-26 22:37:18 -07:00
Qi
fa8086d525 fix: button style error (#3396) 2023-07-26 22:37:00 -07:00
JimmFly
04534c2008 chore: adjust sidebar padding (#3397) 2023-07-26 22:36:45 -07:00
Alex Yang
780fffb88f fix: plugin infra (#3398) 2023-07-26 22:36:29 -07:00
Alex Yang
1e72d3c270 chore: bump version (#3394) 2023-07-27 04:02:18 +00:00
xiaodong zuo
1e38d36161 fix: inconsistent database content in exported PDF (#3385) 2023-07-26 21:26:53 +00:00
JimmFly
bb9908e1fa fix: filter button conflicts with electron header drag event (#3380) 2023-07-26 09:58:40 +00:00
liuyi
6bafa83cef fix(workspace): should avoid sending providers' update back (#3384) 2023-07-26 09:47:24 +00:00
JimmFly
2c249781a2 feat: add new collection button to slider bar (#3369) 2023-07-26 04:32:55 +00:00
Alex Yang
8334ac031b Revert "chore(cli): build infra (#3375)"
This reverts commit 635ca081e4.
2023-07-25 22:04:58 -07:00
Alex Yang
635ca081e4 chore(cli): build infra (#3375) 2023-07-25 23:33:25 +00:00
Alex Yang
10f879f29a refactor(electron): server side plugin (#3360) 2023-07-25 21:32:34 +00:00
Alex Yang
521e505a01 build: update cli (#3374) 2023-07-25 21:32:18 +00:00
Alex Yang
f968587f6f v0.7.0-canary.52 2023-07-25 12:36:30 -07:00
Whitewater
e70f8e74ec chore: allow custom editor spec presets (#3362)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-25 18:57:42 +00:00
Alex Yang
32fd01ed33 build: fix ci (#3373) 2023-07-25 18:41:32 +00:00
TinsFox
00718f8c9a chore: update version label (#3368) 2023-07-25 11:18:02 -07:00
Peng Xiao
20ee9d485d perf: use lazy load provider for IDB and SQLITE (#3351) 2023-07-25 16:56:48 +00:00
JimmFly
e3f66d7e22 style: move trash button group to page bottom (#3352) 2023-07-25 05:21:16 +00:00
JimmFly
be81e63eed chore: update icon size (#3350) 2023-07-24 23:35:10 +00:00
Alex Yang
2cf4e8ebce fix(y-indexeddb): un-track doc when destroy (#3358) 2023-07-24 15:23:16 +00:00
Alex Yang
e6e98975ed fix(core): avoid page full refresh (#3341)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-07-24 09:02:35 +00:00
Peng Xiao
ccb0df10e4 fix: temp workaround for missing blobs in export (#3347) 2023-07-23 10:45:01 +00:00
Alex Yang
dd31d1e8c6 feat(plugin-infra): add plugin cli (#3344) 2023-07-22 17:17:40 +00:00
Alex Yang
a494bad543 chore: bump version (#3346) 2023-07-22 13:10:20 +00:00
danielchim
363699a175 feat: title editing on workspace title (#3139)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-22 13:03:18 +00:00
Qi
439ef1ba90 feat: refactor button with new design (#3343) 2023-07-21 11:07:28 +00:00
349 changed files with 11836 additions and 8066 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'],
},
],
},
],

View File

@@ -21,8 +21,8 @@ body:
label: Distribution version
description: What version of AFFiNE are you using?
options:
- macOS x64
- macOS ARM 64
- macOS x64 (Intel)
- macOS ARM 64 (Apple Silicon)
- Windows x64
- Linux
- Web (app.affine.pro)

16
.github/actions/setup-maker/action.yml vendored Normal file
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:
@@ -120,7 +126,7 @@ jobs:
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
@@ -159,6 +165,7 @@ jobs:
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
mv apps/electron/out/*/make/AppImage/x64/*.AppImage ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.AppImage
- name: Upload Artifact
uses: actions/upload-artifact@v3

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:
@@ -159,6 +165,7 @@ jobs:
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
mv apps/electron/out/*/make/AppImage/x64/*.AppImage ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.AppImage
- name: Upload Artifact
uses: actions/upload-artifact@v3

View File

@@ -28,7 +28,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
[![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)
@@ -126,10 +126,11 @@ If you have questions, you are welcome to contact us. One of the best places to
>
> (Currently, plugins are under heavy development, and the SDK is not yet available.)
| Name | |
| ------------------------------------------------ | ----------------------------------------- |
| [@affine/bookmark-block](plugins/bookmark-block) | A block for bookmarking a website |
| [@affine/copilot](plugins/copilot) | AI Copilot that help you document writing |
| Official Plugin | Description |
| ----------------------------------------------------- | ----------------------------------------- |
| [@affine/bookmark-plugin](plugins/bookmark) | A block for bookmarking a website |
| [@affine/copilot-plugin](plugins/copilot) | AI Copilot that help you document writing |
| [@affine/image-preview-plugin](plugins/image-preview) | Component for previewing an image |
## Thanks
@@ -197,7 +198,7 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[rust-version-icon]: https://img.shields.io/badge/Rust-1.71.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.1-success

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,
@@ -106,11 +110,12 @@ export const createConfiguration: (
devtool:
buildFlags.mode === 'production'
? buildFlags.distribution === 'desktop'
? 'inline-cheap-source-map'
? 'nosources-source-map'
: 'source-map'
: 'eval-cheap-module-source-map',
resolve: {
symlinks: true,
extensionAlias: {
'.js': ['.js', '.tsx', '.ts'],
'.mjs': ['.mjs', '.mts'],
@@ -129,6 +134,10 @@ export const createConfiguration: (
module: {
parser: {
javascript: {
// Do not mock Node.js globals
node: false,
requireJs: false,
import: true,
// Treat as missing export as error
strictExportPresence: true,
},
@@ -136,6 +145,20 @@ export const createConfiguration: (
rules: [
{
test: /\.m?js?$/,
enforce: 'pre',
use: [
{
loader: require.resolve('source-map-loader'),
options: {
filterSourceMappingUrl: (
_url: string,
resourcePath: string
) => {
return resourcePath.includes('@blocksuite');
},
},
},
],
resolve: {
fullySpecified: false,
},
@@ -251,14 +274,6 @@ export const createConfiguration: (
ignoreOrder: true,
}),
]),
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'defer',
minify: false,
chunks: ['index', 'plugin'],
filename: 'index.html',
}),
new VanillaExtractPlugin(),
new webpack.DefinePlugin({
'process.env': JSON.stringify({}),
@@ -268,7 +283,10 @@ export const createConfiguration: (
}),
new CopyPlugin({
patterns: [
{ from: resolve(rootPath, 'public'), to: resolve(rootPath, 'dist') },
{
from: resolve(rootPath, 'public'),
to: resolve(rootPath, 'dist'),
},
],
}),
],

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

@@ -38,7 +38,6 @@
rel="shortcut icon"
href="https://affine.pro/favicon.ico"
/>
<link rel="stylesheet" href="/plugins/style.css" />
</head>
<body>
<div id="app"></div>

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.51",
"version": "0.8.0-canary.10",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
@@ -11,7 +11,6 @@
"dependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/component": "workspace:*",
"@affine/copilot": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/graphql": "workspace:*",
@@ -19,20 +18,20 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/blocks": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/editor": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/global": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/icons": "^2.1.26",
"@blocksuite/lit": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/icons": "^2.1.29",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.1",
"@mui/material": "^5.14.2",
"@react-hookz/web": "^23.1.0",
"async-call-rpc": "^6.3.1",
"cmdk": "^0.2.0",
@@ -49,7 +48,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-is": "18.2.0",
"react-resizable-panels": "^0.0.53",
"react-resizable-panels": "^0.0.54",
"react-router-dom": "^6.14.2",
"rxjs": "^7.8.1",
"ses": "^0.18.5",
@@ -61,15 +60,16 @@
"devDependencies": {
"@perfsee/webpack": "^1.8.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@sentry/webpack-plugin": "^2.4.0",
"@sentry/webpack-plugin": "^2.5.0",
"@svgr/webpack": "^8.0.1",
"@swc/core": "^1.3.70",
"@swc/core": "^1.3.71",
"@types/webpack-env": "^1.18.1",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"express": "^4.18.2",
"html-webpack-plugin": "^5.5.3",
"raw-loader": "^4.0.2",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.3",
"swc-loader": "^0.2.3",
"swc-plugin-coverage-instrument": "^0.0.19",

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

@@ -3,120 +3,15 @@ import '@affine/component/theme/theme.css';
import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import type { RouterState } from '@remix-run/router';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/manager';
import { use } from 'foxact/use';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense, useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, memo, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { historyBaseAtom, MAX_HISTORY } from './atoms/history';
import { router } from './router';
import createEmotionCache from './utils/create-emotion-cache';
const router = createBrowserRouter([
{
path: '/',
lazy: () => import('./pages/index'),
},
{
path: '/404',
lazy: () => import('./pages/404'),
},
{
path: '/workspace/:workspaceId/all',
lazy: () => import('./pages/workspace/all-page'),
},
{
path: '/workspace/:workspaceId/trash',
lazy: () => import('./pages/workspace/trash-page'),
},
{
path: '/workspace/:workspaceId/:pageId',
lazy: () => import('./pages/workspace/detail-page'),
},
]);
//#region atoms bootstrap
currentWorkspaceIdAtom.onMount = set => {
const callback = (state: RouterState) => {
const value = state.location.pathname.split('/')[2];
if (value) {
set(value);
localStorage.setItem('last_workspace_id', value);
}
};
callback(router.state);
const unsubscribe = router.subscribe(callback);
return () => {
unsubscribe();
};
};
currentPageIdAtom.onMount = set => {
const callback = (state: RouterState) => {
const value = state.location.pathname.split('/')[3];
if (value) {
set(value);
}
};
callback(router.state);
const unsubscribe = router.subscribe(callback);
return () => {
unsubscribe();
};
};
historyBaseAtom.onMount = set => {
const unsubscribe = router.subscribe(state => {
set(prev => {
const url = state.location.pathname;
console.log('push', url, prev.skip, prev.stack.length, prev.current);
if (prev.skip) {
return {
stack: [...prev.stack],
current: prev.current,
skip: false,
};
} else {
if (prev.current < prev.stack.length - 1) {
const newStack = prev.stack.slice(0, prev.current);
newStack.push(url);
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
} else {
const newStack = [...prev.stack, url];
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
}
}
});
});
return () => {
unsubscribe();
};
};
//#endregion
const i18n = createI18n();
const cache = createEmotionCache();
const DevTools = lazy(() =>
@@ -132,21 +27,32 @@ const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
);
};
export const App = memo(function App() {
useEffect(() => {
const future = {
v7_startTransition: true,
} as const;
async function loadLanguage() {
if (environment.isBrowser) {
const { createI18n, setUpLanguage } = await import('@affine/i18n');
const i18n = createI18n();
document.documentElement.lang = i18n.language;
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n)?.catch(error => {
console.error(error);
});
}, []);
await setUpLanguage(i18n);
}
}
const languageLoadingPromise = loadLanguage().catch(console.error);
export const App = memo(function App() {
use(languageLoadingPromise);
return (
<CacheProvider value={cache}>
<AffineContext>
<DebugProvider>
<Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}>
<RouterProvider router={router} />
</Suspense>
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
future={future}
/>
</DebugProvider>
</AffineContext>
</CacheProvider>

View File

@@ -1,8 +1,11 @@
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { useCallback } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useNavigate } from 'react-router-dom';
import { router } from '../router';
export type History = {
stack: string[];
current: number;
@@ -11,11 +14,62 @@ export type History = {
export const MAX_HISTORY = 50;
export const historyBaseAtom = atomWithStorage<History>('router-history', {
stack: [],
current: 0,
skip: false,
});
const historyBaseAtom = atomWithStorage<History>(
'router-history',
{
stack: [],
current: 0,
skip: false,
},
createJSONStorage(() => sessionStorage)
);
historyBaseAtom.onMount = set => {
const unsubscribe = router.subscribe(state => {
set(prev => {
const url = state.location.pathname;
// if stack top is the same as current, skip
if (prev.stack[prev.current] === url) {
return prev;
}
if (prev.skip) {
return {
stack: [...prev.stack],
current: prev.current,
skip: false,
};
} else {
if (prev.current < prev.stack.length - 1) {
const newStack = prev.stack.slice(0, prev.current);
newStack.push(url);
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
} else {
const newStack = [...prev.stack, url];
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
}
}
});
});
return () => {
unsubscribe();
};
};
export function useHistoryAtom() {
const navigate = useNavigate();

View File

@@ -93,7 +93,7 @@ export const pageSettingFamily = atomFamily((pageId: string) =>
export const setPageModeAtom = atom(
void 0,
(get, set, pageId: string, mode: PageMode) => {
(_, set, pageId: string, mode: PageMode) => {
set(pageSettingFamily(pageId), { mode });
}
);

View File

@@ -1,4 +1,4 @@
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { atom } from 'jotai/vanilla';
import { pageSettingFamily } from './index';

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/manager';
import { WorkspaceAdapters } from '../adapters/workspace';
console.log('setup global');
setupGlobal();
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(workspace.id, doc, {
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness,
}) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
console.log('migrated', oldMeta.id, newId);
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.SubDoc,
};
await migrateLocalBlobStorage(workspace.id, newId);
};
// create a new workspace and push it to metadata
promises.push(upgrade());
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
// new workspace should all support sub-doc feature
version: WorkspaceVersion.SubDoc,
}) satisfies RootWorkspaceMetadataV2
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
await rootStore
.get(rootWorkspacesMetadataAtom)
.then(meta => {
if (meta.length === 0 && localStorage.getItem('is-first-open') === null) {
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result).catch(console.error);
}
})
.catch(console.error);

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,190 +1,77 @@
/// <reference types="@types/webpack-env" />
import 'ses';
import { DebugLogger } from '@affine/debug';
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
import * as AFFiNEComponent from '@affine/component';
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
import { DisposableGroup } from '@blocksuite/global/utils';
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
import * as Icons from '@blocksuite/icons';
import type {
CallbackMap,
PluginContext,
} from '@toeverything/plugin-infra/entry';
import * as Manager from '@toeverything/plugin-infra/manager';
import {
editorItemsAtom,
headerItemsAtom,
registeredPluginAtom,
rootStore,
windowItemsAtom,
} from '@toeverything/plugin-infra/manager';
import * as Jotai from 'jotai';
import { Provider } from 'jotai/react';
import * as JotaiUtils from 'jotai/utils';
import type { PropsWithChildren } from 'react';
import * as React from 'react';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import * as ReactDom from 'react-dom';
import * as ReactDomClient from 'react-dom/client';
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
if (!process.env.COVERAGE) {
lockdown({
evalTaming: 'unsafeEval',
overrideTaming: 'severe',
consoleTaming: 'unsafe',
errorTaming: 'unsafe',
errorTrapping: 'platform',
unhandledRejectionTrapping: 'report',
});
const builtinPluginUrl = new Set([
'/plugins/bookmark',
'/plugins/copilot',
'/plugins/hello-world',
'/plugins/image-preview',
]);
const logger = new DebugLogger('register-plugins');
declare global {
// eslint-disable-next-line no-var
var __pluginPackageJson__: unknown[];
}
const PluginProvider = ({ children }: PropsWithChildren) =>
React.createElement(
Provider,
{
store: rootStore,
},
children
);
globalThis.__pluginPackageJson__ = [];
const customRequire = (id: string) => {
if (id === '@toeverything/plugin-infra/manager') {
return Manager;
}
if (id === 'react') {
return React;
}
if (id === 'react/jsx-runtime') {
return ReactJSXRuntime;
}
if (id === 'react-dom') {
return ReactDom;
}
if (id === 'react-dom/client') {
return ReactDomClient;
}
if (id === '@blocksuite/icons') {
return Icons;
}
if (id === '@affine/component') {
return AFFiNEComponent;
}
if (id === '@blocksuite/blocks/std') {
return BlockSuiteBlocksStd;
}
if (id === '@blocksuite/global/utils') {
return BlockSuiteGlobalUtils;
}
if (id === 'jotai') {
return Jotai;
}
if (id === 'jotai/utils') {
return JotaiUtils;
}
if (id === '../plugin.js') {
return entryCompartment.evaluate('exports');
}
throw new Error(`Cannot find module '${id}'`);
};
const createGlobalThis = () => {
return {
process: Object.freeze({
env: {
NODE_ENV: process.env.NODE_ENV,
},
}),
// UNSAFE: React will read `window` and `document`
window,
document,
navigator,
userAgent: navigator.userAgent,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
IDBRequest: globalThis.IDBRequest,
IDBDatabase: globalThis.IDBDatabase,
IDBCursorWithValue: globalThis.IDBCursorWithValue,
IDBFactory: globalThis.IDBFactory,
IDBKeyRange: globalThis.IDBKeyRange,
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
IDBTransaction: globalThis.IDBTransaction,
IDBObjectStore: globalThis.IDBObjectStore,
IDBIndex: globalThis.IDBIndex,
IDBCursor: globalThis.IDBCursor,
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
exports: {},
console: globalThis.console,
require: customRequire,
};
};
const group = new DisposableGroup();
const pluginList = await (
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
).json();
const builtInPlugins: string[] = pluginList.map((plugin: any) => plugin.name);
const pluginGlobalThis = createGlobalThis();
const pluginEntry = await fetch('/plugins/plugin.js').then(res => res.text());
const entryCompartment = new Compartment(pluginGlobalThis, {});
entryCompartment.evaluate(pluginEntry, {
__evadeHtmlCommentTest__: true,
});
await Promise.all(
builtInPlugins.map(plugin => {
const pluginCompartment = new Compartment(createGlobalThis(), {});
const pluginGlobalThis = pluginCompartment.globalThis;
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
const packageJsonURL = new URL('package.json', baseURL);
return fetch(packageJsonURL).then(async res => {
const packageJson = await res.json();
const pluginConfig = packageJson['affinePlugin'];
if (
pluginConfig.release === false &&
process.env.NODE_ENV !== 'development'
) {
return;
}
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
const coreEntry = new URL(pluginConfig.entry.core, baseURL.toString());
const codeText = await fetch(coreEntry).then(res => res.text());
pluginCompartment.evaluate(codeText);
pluginGlobalThis.__INTERNAL__ENTRY = {
register: (part, callback) => {
if (part === 'headerItem') {
rootStore.set(headerItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['headerItem'],
}));
} else if (part === 'editor') {
rootStore.set(editorItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['editor'],
}));
} else if (part === 'window') {
rootStore.set(windowItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['window'],
}));
} else {
throw new Error(`Unknown part: ${part}`);
export const pluginRegisterPromise = Promise.all(
[...builtinPluginUrl].map(url => {
return fetch(`${url}/package.json`)
.then(async res => {
const packageJson = await res.json();
const {
name: pluginName,
affinePlugin: {
release,
entry: { core },
assets,
},
} = packageJson;
globalThis.__pluginPackageJson__.push(packageJson);
logger.debug(`registering plugin ${pluginName}`);
logger.debug(`package.json: ${packageJson}`);
if (!release && !runtimeConfig.enablePlugin) {
return Promise.resolve();
}
const baseURL = url;
const entryURL = `${baseURL}/${core}`;
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
await setupPluginCode(baseURL, pluginName, core);
console.log(`prepareImports for ${pluginName} done`);
await fetch(entryURL).then(async () => {
if (assets.length > 0) {
await Promise.all(
assets.map(async (asset: string) => {
if (asset.endsWith('.css')) {
const res = await fetch(`${baseURL}/${asset}`);
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', pluginName);
style.textContent = text;
document.head.appendChild(style);
});
}
return null;
} else {
return Promise.resolve();
}
})
);
}
},
utils: {
PluginProvider,
},
} satisfies PluginContext;
const dispose = pluginCompartment.evaluate(
'exports.entry(__INTERNAL__ENTRY)'
);
if (typeof dispose !== 'function') {
throw new Error('Plugin entry must return a function');
}
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
group.add(dispose);
});
evaluatePluginEntry(pluginName);
});
})
.catch(e => {
console.error(`error when fetch plugin from ${url}`, e);
});
})
);
console.log('register plugins finished');
).then(() => {
console.info('All plugins loaded');
});

View File

@@ -0,0 +1,180 @@
import {
migrateDatabaseBlockTo3,
migrateToSubdoc,
} from '@affine/env/blocksuite';
import { setupGlobal } from '@affine/env/global';
import type {
LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/infra/atom';
import { WorkspaceAdapters } from '../adapters/workspace';
async function tryMigration() {
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(
workspace.id,
doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
await migrateDatabaseBlockTo3(newWorkspace.blockSuiteWorkspace.doc);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
console.log('migrated', oldMeta.id, newId);
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.DatabaseV3,
};
await migrateLocalBlobStorage(workspace.id, newId);
console.log('migrate to v2');
};
// create a new workspace and push it to metadata
promises.push(upgrade());
} else if (oldMeta.version < WorkspaceVersion.DatabaseV3) {
console.log('migrate to v3');
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
promises.push(
(async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (workspace) {
const provider = createIndexedDBDownloadProvider(
workspace.id,
workspace.blockSuiteWorkspace.doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
await migrateDatabaseBlockTo3(
workspace.blockSuiteWorkspace.doc
);
}
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
console.log('migrate to v3');
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.DatabaseV3,
};
})()
);
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
}
function createFirstAppData() {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
version: WorkspaceVersion.DatabaseV3,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
if (localStorage.getItem('is-first-open') !== null) {
return;
}
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result);
}
export async function setup() {
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
console.log('setup global');
setupGlobal();
createFirstAppData();
await tryMigration();
await rootStore.get(rootWorkspacesMetadataAtom);
console.log('setup done');
}

View File

@@ -9,7 +9,7 @@ import {
currentPageIdAtom,
currentWorkspaceIdAtom,
rootStore,
} from '@toeverything/plugin-infra/manager';
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';

View File

@@ -79,7 +79,7 @@ const NameWorkspaceContent = ({
<div className={style.buttonGroup}>
<Button
data-testid="create-workspace-close-button"
type="light"
type="primary"
onClick={onClose}
>
{t.Cancel()}
@@ -155,7 +155,7 @@ const SetDBLocationContent = ({
<Button
disabled={opening}
data-testid="create-workspace-customize-button"
type="light"
type="primary"
onClick={handleSelectDBFileLocation}
>
{t['Customize']()}

View File

@@ -47,7 +47,7 @@ const LanguageMenuContent: FC<{
</>
);
};
export const LanguageMenu: FC<{ triggerProps: ButtonProps }> = ({
export const LanguageMenu: FC<{ triggerProps?: ButtonProps }> = ({
triggerProps,
}) => {
const i18n = useI18N();

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}
type="danger"
shape="circle"
size="large"
type="error"
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

@@ -37,7 +37,7 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
</Button>
<Button
onClick={handleLeave}
type="danger"
type="error"
shape="circle"
style={{ marginLeft: '24px' }}
>

View File

@@ -1,31 +1,51 @@
import { Button, toast } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { isDesktop } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { FC } from 'react';
import { type FC, useCallback } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared';
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && isDesktop) {
const bs = workspace.blockSuiteWorkspace.blobs;
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
const blobsInStorage = await bs.list();
const blobsToSync = blobsInStorage.filter(
blob => !blobsInDb.includes(blob)
);
await Promise.all(
blobsToSync.map(async blobKey => {
const blob = await bs.get(blobKey);
if (blob) {
const bin = new Uint8Array(await blob.arrayBuffer());
await window.apis.db.addBlob(workspace.id, blobKey, bin);
}
})
);
}
}
export const ExportPanel: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const workspaceId = workspace.id;
const t = useAFFiNEI18N();
const onExport = useCallback(async () => {
await syncBlobsToSqliteDb(workspace);
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
}, [t, workspace, workspaceId]);
return (
<>
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
<Button
size="small"
data-testid="export-affine-backup"
onClick={async () => {
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
}}
>
<Button data-testid="export-affine-backup" onClick={onExport}>
{t['Export']()}
</Button>
</SettingRow>

View File

@@ -84,13 +84,12 @@ export const ProfilePanel: FC<{
/>
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
<IconButton
size="middle"
data-testid="save-workspace-name"
onClick={() => {
handleUpdateWorkspaceName(input);
}}
active={true}
style={{
color: 'var(--affine-primary-color)',
marginLeft: '12px',
}}
>

View File

@@ -74,7 +74,7 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
<FlexWrapper justifyContent="space-between">
<Button
className={style.urlButton}
size="middle"
size="large"
onClick={useCallback(() => {
window.open(shareUrl, '_blank');
}, [shareUrl])}
@@ -82,7 +82,7 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
>
{shareUrl}
</Button>
<Button size="middle" onClick={copyUrl}>
<Button size="large" onClick={copyUrl}>
{t['Copy']()}
</Button>
</FlexWrapper>

View File

@@ -79,14 +79,13 @@ export const StoragePanel: FC<{
<Button
data-testid="move-folder"
className={style.urlButton}
size="middle"
size="large"
onClick={handleMoveTo}
>
{secondaryPath}
</Button>
</Tooltip>
<Button
size="small"
data-testid="reveal-folder"
data-disabled={moveToInProgress}
onClick={onRevealDBFile}
@@ -96,7 +95,6 @@ export const StoragePanel: FC<{
</FlexWrapper>
) : (
<Button
size="small"
data-testid="move-folder"
data-disabled={moveToInProgress}
onClick={handleMoveTo}

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

@@ -32,11 +32,7 @@ export const ThemeSettings = () => {
[setTheme]
)}
>
<RadioButton
bold={true}
value="system"
data-testid="system-theme-trigger"
>
<RadioButton value="system" data-testid="system-theme-trigger">
{t['system']()}
</RadioButton>
<RadioButton bold={true} value="light" data-testid="light-theme-trigger">
@@ -117,7 +113,7 @@ export const AppearanceSettings = () => {
desc={t['Select the language for the interface.']()}
>
<div className={settingWrapper}>
<LanguageMenu triggerProps={{ size: 'small' }} />
<LanguageMenu />
</div>
</SettingRow>
{environment.isDesktop ? (

View File

@@ -1,15 +1,44 @@
import {
SettingHeader,
SettingWrapper,
} from '@affine/component/setting-components';
import { SettingHeader } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { registeredPluginAtom } from '@toeverything/plugin-infra/manager';
import {
registeredPluginAtom,
settingItemsAtom,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import type { FC, ReactNode } from 'react';
import { useRef } from 'react';
import { pluginItem } from './style.css';
const PluginSettingWrapper: FC<{
id: string;
title?: ReactNode;
}> = ({ title, id }) => {
const Setting = useAtomValue(settingItemsAtom)[id];
const disposeRef = useRef<(() => void) | null>(null);
return (
<div>
{title ? <div className="title">{title}</div> : null}
<div
ref={ref => {
if (ref && Setting) {
setTimeout(() => {
disposeRef.current = Setting(ref);
});
} else if (ref === null) {
setTimeout(() => {
disposeRef.current?.();
});
}
}}
/>
</div>
);
};
export const Plugins = () => {
const t = useAFFiNEI18N();
const allowedPlugins = useAtomValue(registeredPluginAtom);
console.log('allowedPlugins', allowedPlugins);
return (
<>
<SettingHeader
@@ -18,7 +47,9 @@ export const Plugins = () => {
data-testid="plugins-title"
/>
{allowedPlugins.map(plugin => (
<SettingWrapper key={plugin} title={plugin}></SettingWrapper>
<div className={pluginItem} key={plugin}>
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
</div>
))}
</>
);

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,3 +1,4 @@
import { ScrollableContainer, Tooltip } from '@affine/component';
import {
WorkspaceListItemSkeleton,
WorkspaceListSkeleton,
@@ -7,7 +8,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import clsx from 'clsx';
import { useAtomValue } from 'jotai';
import type { FC } from 'react';
@@ -19,6 +20,7 @@ import type {
GeneralSettingList,
} from '../general-setting';
import {
currentWorkspaceLabel,
settingSlideBar,
sidebarItemsWrapper,
sidebarSelectItem,
@@ -74,10 +76,12 @@ export const SettingSidebar: FC<{
</div>
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
<Suspense fallback={<WorkspaceListSkeleton />}>
<WorkspaceList
onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedWorkspaceId={selectedWorkspaceId}
/>
<ScrollableContainer>
<WorkspaceList
onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedWorkspaceId={selectedWorkspaceId}
/>
</ScrollableContainer>
</Suspense>
</div>
</div>
@@ -133,9 +137,18 @@ const WorkspaceListItem = ({
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
<span className="setting-name">{workspaceName}</span>
{isCurrent ? (
<div className="current-label" data-testid="current-workspace-label">
Current
</div>
<Tooltip
content="Current"
title="Current"
offset={[0, -5]}
placement="top"
disablePortal={false}
>
<div
className={currentWorkspaceLabel}
data-testid="current-workspace-label"
></div>
</Tooltip>
) : null}
</div>
);

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',
@@ -75,20 +75,21 @@ globalStyle(`${settingSlideBar} .setting-name`, {
whiteSpace: 'nowrap',
flexGrow: 1,
});
globalStyle(`${settingSlideBar} .current-label`, {
export const currentWorkspaceLabel = style({
width: '20px',
height: '20px',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '0 5px',
// TODO: use color variable
background: '#1E96EB',
fontSize: 'var(--affine-font-xs)',
fontWeight: '600',
color: 'var(--affine-white)',
marginLeft: '10px',
flexShrink: 0,
selectors: {
'&::after': {
content: '""',
width: '8px',
height: '8px',
borderRadius: '50%',
background: 'var(--affine-blue)',
},
},
});
export const accountButton = style({

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

@@ -4,13 +4,15 @@ import { PageList, PageListTrashView } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { type PageMeta, type Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { getPagePreviewText } from '@toeverything/hooks/use-block-suite-page-preview';
import { useAtom } from 'jotai';
import { useBlockSuitePagePreview } from '@toeverything/hooks/use-block-suite-page-preview';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useAtom, useAtomValue } from 'jotai';
import type React from 'react';
import { useMemo } from 'react';
import { Suspense, useCallback, useMemo } from 'react';
import { allPageModeSelectAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
@@ -39,17 +41,49 @@ const filter = {
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
const PagePreviewInner = ({
workspace,
pageId,
}: {
workspace: Workspace;
pageId: string;
}) => {
const page = useBlockSuiteWorkspacePage(workspace, pageId);
assertExists(page);
const previewAtom = useBlockSuitePagePreview(page);
const preview = useAtomValue(previewAtom);
return preview;
};
const PagePreview = ({
workspace,
pageId,
}: {
workspace: Workspace;
pageId: string;
}) => {
return (
<Suspense>
<PagePreviewInner workspace={workspace} pageId={pageId} />
</Suspense>
);
};
const PageListEmpty = (props: {
createPage?: () => void;
createPage?: ReturnType<typeof usePageHelper>['createPage'];
listType: BlockSuitePageListProps['listType'];
}) => {
const { listType, createPage } = props;
const t = useAFFiNEI18N();
const onCreatePage = useCallback(() => {
createPage?.();
}, [createPage]);
const getEmptyDescription = () => {
if (listType === 'all') {
const CreateNewPageButton = () => (
<button className={emptyDescButton} onClick={createPage}>
const createNewPageButton = (
<button className={emptyDescButton} onClick={onCreatePage}>
New Page
</button>
);
@@ -57,7 +91,7 @@ const PageListEmpty = (props: {
const shortcut = environment.isMacOs ? '⌘ + N' : 'Ctrl + N';
return (
<Trans i18nKey="emptyAllPagesClient">
Click on the <CreateNewPageButton /> button Or press
Click on the {createNewPageButton} button Or press
<kbd className={emptyDescKbd}>{{ shortcut } as any}</kbd> to create
your first page.
</Trans>
@@ -66,7 +100,7 @@ const PageListEmpty = (props: {
return (
<Trans i18nKey="emptyAllPages">
Click on the
<CreateNewPageButton />
{createNewPageButton}
button to create your first page.
</Trans>
);
@@ -147,8 +181,6 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
if (listType === 'trash') {
const pageList: TrashListData[] = list.map(pageMeta => {
const page = blockSuiteWorkspace.getPage(pageMeta.id);
const preview = page ? getPagePreviewText(page) : undefined;
return {
icon: isPreferredEdgeless(pageMeta.id) ? (
<EdgelessIcon />
@@ -157,7 +189,9 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
),
pageId: pageMeta.id,
title: pageMeta.title,
preview,
preview: (
<PagePreview workspace={blockSuiteWorkspace} pageId={pageMeta.id} />
),
createDate: new Date(pageMeta.createDate),
trashDate: pageMeta.trashDate
? new Date(pageMeta.trashDate)
@@ -186,12 +220,13 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
const pageList: ListData[] = list.map(pageMeta => {
const page = blockSuiteWorkspace.getPage(pageMeta.id);
const preview = page ? getPagePreviewText(page) : undefined;
return {
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id,
title: pageMeta.title,
preview,
preview: (
<PagePreview workspace={blockSuiteWorkspace} pageId={pageMeta.id} />
),
tags:
page?.meta.tags?.map(id => tagOptionMap[id]).filter(v => v != null) ??
[],

View File

@@ -1,3 +1,4 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
@@ -15,15 +16,23 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
[pageSettings]
);
const setPageMode = useSetAtom(setPageModeAtom);
const createPageAndOpen = useCallback(() => {
const page = createPage();
return openPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace.id, createPage, openPage]);
const createEdgelessAndOpen = useCallback(() => {
const page = createPage();
setPageMode(page.id, 'edgeless');
return openPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace.id, createPage, openPage, setPageMode]);
const createPageAndOpen = useCallback(
(id?: string, mode?: 'page' | 'edgeless') => {
const page = createPage(id);
initEmptyPage(page); // we don't need to wait it to be loaded right?
if (mode) {
setPageMode(page.id, mode);
}
openPage(blockSuiteWorkspace.id, page.id);
},
[blockSuiteWorkspace.id, createPage, openPage, setPageMode]
);
const createEdgelessAndOpen = useCallback(
(id?: string) => {
return createPageAndOpen(id, 'edgeless');
},
[createPageAndOpen]
);
const importFileAndOpen = useCallback(async () => {
const { showImportModal } = await import('@blocksuite/blocks');
showImportModal({ workspace: blockSuiteWorkspace });

View File

@@ -43,6 +43,9 @@ export const EditorModeSwitch = ({
assertExists(pageMeta);
const { trash } = pageMeta;
useEffect(() => {
if (trash) {
return;
}
const keydown = (e: KeyboardEvent) => {
if (
!environment.isServer && environment.isMacOs
@@ -64,7 +67,7 @@ export const EditorModeSwitch = ({
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [setSetting, t]);
}, [setSetting, t, trash]);
return (
<Tooltip content={<TooltipContent />}>
@@ -77,6 +80,7 @@ export const EditorModeSwitch = ({
data-testid="switch-page-mode-button"
active={currentMode === 'page'}
hide={trash && currentMode !== 'page'}
trash={trash}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'page') {
@@ -90,6 +94,7 @@ export const EditorModeSwitch = ({
data-testid="switch-edgeless-mode-button"
active={currentMode === 'edgeless'}
hide={trash && currentMode !== 'edgeless'}
trash={trash}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'edgeless') {

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',
},
};
@@ -34,14 +35,19 @@ export const StyledEditorModeSwitch = styled('div')<{
export const StyledSwitchItem = styled('button')<{
active?: boolean;
hide?: boolean;
}>(({ active = false, hide = false }) => {
trash?: boolean;
}>(({ active = false, hide = false, trash = false }) => {
return {
width: '24px',
height: '24px',
borderRadius: '8px',
WebkitAppRegion: 'no-drag',
boxShadow: active ? 'var(--affine-shadow-1)' : 'none',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
color: active
? trash
? 'var(--affine-error-color)'
: 'var(--affine-primary-color)'
: 'var(--affine-icon-color)',
display: hide ? 'none' : 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
@@ -49,7 +55,7 @@ export const StyledSwitchItem = styled('button')<{
zIndex: 2,
fontSize: '20px',
path: {
fill: 'currentColor',
stroke: 'currentColor',
},
};
});

View File

@@ -8,12 +8,14 @@ import { StyledSwitchItem } from './style';
type HoverAnimateControllerProps = {
active?: boolean;
hide?: boolean;
trash?: boolean;
children: React.ReactElement;
} & HTMLAttributes<HTMLButtonElement>;
const HoverAnimateController = ({
active,
hide,
trash,
children,
...props
}: HoverAnimateControllerProps) => {
@@ -22,6 +24,7 @@ const HoverAnimateController = ({
<StyledSwitchItem
hide={hide}
active={active}
trash={trash}
onMouseEnter={() => {
setStartAnimate(true);
}}
@@ -32,7 +35,7 @@ const HoverAnimateController = ({
>
{cloneElement(children, {
isStopped: !startAnimate,
speed: 5,
speed: 1,
width: 20,
height: 20,
})}

View File

@@ -14,9 +14,9 @@ import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue } from 'jotai';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import { pageSettingFamily } from '../../../../atoms';
@@ -24,7 +24,6 @@ import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suit
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { toast } from '../../../../utils';
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
import * as styles from '../styles.css';
import { LanguageMenu } from './language-menu';
const CommonMenu = () => {
const content = (
@@ -45,7 +44,7 @@ const CommonMenu = () => {
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<IconButton data-testid="editor-option-menu">
<MoreVerticalIcon />
</IconButton>
</Menu>
@@ -71,61 +70,56 @@ const PageMenu = () => {
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const handleFavorite = useCallback(() => {
setPageMeta(pageId, { favorite: !favorite });
toast(favorite ? t['Removed from Favorites']() : t['Added to Favorites']());
}, [favorite, pageId, setPageMeta, t]);
const handleSwitchMode = useCallback(() => {
setSetting(setting => ({
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
}));
toast(
mode === 'page'
? t['com.affine.edgelessMode']()
: t['com.affine.pageMode']()
);
}, [mode, setSetting, t]);
const handleOnConfirm = useCallback(() => {
removeToTrash(pageMeta.id);
toast(t['Moved to Trash']());
setOpenConfirm(false);
}, [pageMeta.id, removeToTrash, t]);
const EditMenu = (
<>
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite
? t['Removed from Favorites']()
: t['Added to Favorites']()
);
}}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
setSetting(setting => ({
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
}));
}}
>
{t['Convert to ']()}
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
<div className={styles.horizontalDividerContainer}>
<div className={styles.horizontalDivider} />
</div>
</>
<div
onClick={e => {
e.stopPropagation();
}}
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={handleFavorite}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
<MenuThemeModeSwitch />
<LanguageMenu />
</div>
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={handleSwitchMode}
>
{t['Convert to ']()}
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
</>
);
@@ -138,18 +132,14 @@ const PageMenu = () => {
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<IconButton data-testid="editor-option-menu">
<MoreVerticalIcon />
</IconButton>
</Menu>
<MoveToTrash.ConfirmModal
open={openConfirm}
title={pageMeta.title}
onConfirm={() => {
removeToTrash(pageMeta.id);
toast(t['Moved to Trash']());
setOpenConfirm(false);
}}
onConfirm={handleOnConfirm}
onCancel={() => {
setOpenConfirm(false);
}}

View File

@@ -51,13 +51,13 @@ export const LanguageMenu: React.FC = () => {
disablePortal={true}
>
<StyledButton
type="plain"
icon={
<StyledArrowDownContainer>
<ArrowDownSmallIcon />
</StyledArrowDownContainer>
}
iconPosition="end"
noBorder={true}
data-testid="language-menu-button"
>
<StyledCurrentLanguage>

View File

@@ -0,0 +1,15 @@
import { style } from '@vanilla-extract/css';
export const group = style({
width: '100%',
position: 'absolute',
bottom: '100px',
left: '0',
display: 'flex',
gap: '24px',
justifyContent: 'center',
});
export const buttonContainer = style({
boxShadow: 'var(--affine-float-button-shadow-2)',
borderRadius: '8px',
});

View File

@@ -3,14 +3,14 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
import { buttonContainer, group } from './styles.css';
export const TrashButtonGroup = () => {
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
@@ -29,32 +29,34 @@ export const TrashButtonGroup = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button
bold={true}
shape="round"
style={{ marginRight: '24px' }}
onClick={() => {
restoreFromTrash(pageId);
}}
>
{t['Restore it']()}
</Button>
<Button
bold={true}
shape="round"
type="danger"
onClick={() => {
setOpen(true);
}}
>
{t['Delete permanently']()}
</Button>
<div className={group}>
<div className={buttonContainer}>
<Button
type="processing"
onClick={() => {
restoreFromTrash(pageId);
}}
size="large"
>
{t['Restore it']()}
</Button>
</div>
<div className={buttonContainer}>
<Button
type="error"
onClick={() => {
setOpen(true);
}}
size="large"
>
{t['Delete permanently']()}
</Button>
</div>
<Confirm
title={t['TrashButtonGroupTitle']()}
content={t['TrashButtonGroupDescription']()}
confirmText={t['Delete']()}
confirmType="danger"
confirmType="error"
open={open}
onConfirm={useCallback(() => {
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
@@ -67,7 +69,7 @@ export const TrashButtonGroup = () => {
setOpen(false);
}}
/>
</>
</div>
);
};

View File

@@ -7,8 +7,8 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
import { isDesktop } from '@affine/env/constant';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { headerItemsAtom } from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai';
import { headerItemsAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import {
forwardRef,
@@ -19,11 +19,11 @@ import {
useState,
} from 'react';
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
import { currentModeAtom } from '../../../atoms/mode';
import type { AffineOfficialWorkspace } from '../../../shared';
import DownloadClientTip from './download-tips';
import { EditorOptionMenu } from './header-right-items/editor-option-menu';
import TrashButtonGroup from './header-right-items/trash-button-group';
import * as styles from './styles.css';
import { OSWarningMessage, shouldShowWarning } from './utils';
@@ -38,7 +38,6 @@ export type BaseHeaderProps<
export enum HeaderRightItemName {
EditorOptionMenu = 'editorOptionMenu',
TrashButtonGroup = 'trashButtonGroup',
// some windows only items
WindowsAppControls = 'windowsAppControls',
}
@@ -56,16 +55,12 @@ type HeaderItem = {
};
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.TrashButtonGroup]: {
Component: TrashButtonGroup,
availableWhen: (_, currentPage) => {
return currentPage?.meta.trash === true;
},
},
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic }) => {
return !isPublic;
return (
!isPublic && currentPage?.meta.trash !== true && currentPage !== null
);
},
},
[HeaderRightItemName.WindowsAppControls]: {
@@ -160,10 +155,9 @@ export const Header = forwardRef<
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const [showWarning, setShowWarning] = useState(false);
const [showDownloadTip, setShowDownloadTip] = useState(true);
// const [shouldShowGuideDownloadClientTip] = useAtom(
// guideDownloadClientTipAtom
// );
const [showDownloadTip, setShowDownloadTip] = useAtom(
guideDownloadClientTipAtom
);
useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
@@ -183,7 +177,10 @@ export const Header = forwardRef<
{showDownloadTip ? (
<DownloadClientTip
show={showDownloadTip}
onClose={() => setShowDownloadTip(false)}
onClose={() => {
setShowDownloadTip(false);
localStorage.setItem('affine-is-dt-hide', '1');
}}
/>
) : (
<BrowserWarning

View File

@@ -1,5 +1,9 @@
import { Button } from '@affine/component';
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useSetAtom } from 'jotai';
import type {
FC,
@@ -7,7 +11,7 @@ import type {
PropsWithChildren,
ReactElement,
} from 'react';
import { useRef } from 'react';
import { useCallback, useRef, useState } from 'react';
import { openQuickSearchModalAtom } from '../../../atoms';
import { QuickSearchButton } from '../../pure/quick-search-button';
@@ -27,10 +31,25 @@ export const BlockSuiteEditorHeader: FC<
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
meta => meta.id === currentPage?.id
);
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace);
const [isEditable, setIsEditable] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = useCallback(() => {
if (isEditable) {
setIsEditable(!isEditable);
const value = inputRef.current?.value;
if (value !== pageMeta?.title && currentPage) {
pageTitleMeta.setPageTitle(currentPage?.id, value || '');
}
} else {
setIsEditable(!isEditable);
}
}, [currentPage, isEditable, pageMeta?.title, pageTitleMeta]);
const headerRef = useRef<HTMLDivElement>(null);
assertExists(pageMeta);
const title = pageMeta.title;
const title = pageMeta?.title;
return (
<Header ref={headerRef} {...props}>
{children}
@@ -46,8 +65,38 @@ export const BlockSuiteEditorHeader: FC<
}}
/>
</div>
<div className={styles.title}>{title || 'Untitled'}</div>
<div>
{isEditable ? (
<div>
<input
autoFocus={true}
className={styles.title}
type="text"
data-testid="title-content"
defaultValue={pageMeta?.title}
onBlur={handleClick}
ref={inputRef}
/>
<Button
onClick={handleClick}
data-testid="save-edit-button"
style={{
marginLeft: '12px',
}}
>
Save
</Button>
</div>
) : (
<span
data-testid="title-edit-button"
onClick={handleClick}
style={{ cursor: 'pointer' }}
>
{title || 'Untitled'}
</span>
)}
</div>
<div className={styles.searchArrowWrapper}>
<QuickSearchButton
onClick={() => {

View File

@@ -1,21 +1,19 @@
import './page-detail-editor.css';
import { PageNotFoundError } from '@affine/env/constant';
import type { CallbackMap, LayoutNode } from '@affine/sdk//entry';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import type { CallbackMap } from '@toeverything/plugin-infra/entry';
import {
affinePluginsAtom,
contentLayoutAtom,
editorItemsAtom,
rootStore,
windowItemsAtom,
} from '@toeverything/plugin-infra/manager';
import type { AffinePlugin, LayoutNode } from '@toeverything/plugin-infra/type';
} from '@toeverything/infra/atom';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties, FC, ReactElement } from 'react';
@@ -25,6 +23,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { pageSettingFamily } from '../atoms';
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { TrashButtonGroup } from './blocksuite/workspace-header/header-right-items/trash-button-group';
import * as styles from './page-detail-editor.css';
import { pluginContainer } from './page-detail-editor.css';
@@ -67,62 +66,65 @@ const EditorWrapper = memo(function EditorWrapper({
}, [appSettings.fontStyle]);
return (
<Editor
className={clsx(styles.editor, {
'full-screen': appSettings.fullWidthLayout,
})}
style={
{
'--affine-font-family': value,
} as CSSProperties
}
key={`${workspace.id}-${pageId}`}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
onInit(page, editor);
},
[onInit]
)}
setBlockHub={setBlockHub}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
localStorage.setItem('last_page_id', page.id);
let dispose = () => {};
if (onLoad) {
dispose = onLoad(page, editor);
}
const editorItems = rootStore.get(editorItemsAtom);
let disposes: (() => void)[] = [];
const renderTimeout = setTimeout(() => {
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
const cleanup = editorItem(div, editor);
assertExists(parent);
document.body.appendChild(div);
return () => {
cleanup();
document.body.removeChild(div);
};
<>
<Editor
className={clsx(styles.editor, {
'full-screen': appSettings.fullWidthLayout,
})}
style={
{
'--affine-font-family': value,
} as CSSProperties
}
key={`${workspace.id}-${pageId}`}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
onInit(page, editor);
},
[onInit]
)}
setBlockHub={setBlockHub}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
localStorage.setItem('last_page_id', page.id);
let dispose = () => {};
if (onLoad) {
dispose = onLoad(page, editor);
}
const editorItems = rootStore.get(editorItemsAtom);
let disposes: (() => void)[] = [];
const renderTimeout = setTimeout(() => {
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
const cleanup = editorItem(div, editor);
assertExists(parent);
document.body.appendChild(div);
return () => {
cleanup();
document.body.removeChild(div);
};
});
});
});
return () => {
dispose();
clearTimeout(renderTimeout);
setTimeout(() => {
disposes.forEach(dispose => dispose());
});
};
},
[onLoad]
)}
/>
return () => {
dispose();
clearTimeout(renderTimeout);
setTimeout(() => {
disposes.forEach(dispose => dispose());
});
};
},
[onLoad]
)}
/>
{meta.trash && <TrashButtonGroup />}
</>
);
});
@@ -160,7 +162,6 @@ const PluginContentAdapter = memo<{
type LayoutPanelProps = {
node: LayoutNode;
editorProps: PageDetailEditorProps;
plugins: AffinePlugin<string>[];
};
const LayoutPanel = memo(function LayoutPanel(
@@ -185,21 +186,18 @@ const LayoutPanel = memo(function LayoutPanel(
>
<Panel defaultSize={node.splitPercentage}>
<Suspense>
<LayoutPanel
node={node.first}
editorProps={props.editorProps}
plugins={props.plugins}
/>
<LayoutPanel node={node.first} editorProps={props.editorProps} />
</Suspense>
</Panel>
<PanelResizeHandle />
<Panel defaultSize={100 - node.splitPercentage}>
<Panel
defaultSize={100 - node.splitPercentage}
style={{
overflow: 'scroll',
}}
>
<Suspense>
<LayoutPanel
node={node.second}
editorProps={props.editorProps}
plugins={props.plugins}
/>
<LayoutPanel node={node.second} editorProps={props.editorProps} />
</Suspense>
</Panel>
</PanelGroup>
@@ -215,16 +213,11 @@ export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
}
const layout = useAtomValue(contentLayoutAtom);
const affinePluginsMap = useAtomValue(affinePluginsAtom);
const plugins = useMemo(
() => Object.values(affinePluginsMap),
[affinePluginsMap]
);
return (
<>
<Suspense>
<LayoutPanel node={layout} editorProps={props} plugins={plugins} />
<LayoutPanel node={layout} editorProps={props} />
</Suspense>
</>
);

View File

@@ -14,8 +14,7 @@ export const Footer: FC = () => {
<StyledFooter data-testid="workspace-list-modal-footer">
<StyledSignInButton
data-testid="sign-in-button"
noBorder
bold
type="plain"
icon={
<div className="circle">
<CloudWorkspaceIcon />

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

@@ -50,8 +50,6 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
<ModalCloseButton
top={6}
right={6}
size={[24, 24]}
iconSize={[15, 15]}
onClick={() => {
onClose();
}}

View File

@@ -0,0 +1,59 @@
import { IconButton } from '@affine/component';
import {
EditCollectionModel,
useCollectionManager,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PlusIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { uuidv4 } from '@blocksuite/store';
import { useCallback, useState } from 'react';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
type AddCollectionButtonProps = {
workspace: Workspace;
};
export const AddCollectionButton = ({
workspace,
}: AddCollectionButtonProps) => {
const getPageInfo = useGetPageInfoById(workspace);
const setting = useCollectionManager(workspace.id);
const t = useAFFiNEI18N();
const [show, showUpdateCollection] = useState(false);
const [defaultCollection, setDefaultCollection] = useState<Collection>();
const handleClick = useCallback(() => {
showUpdateCollection(true);
setDefaultCollection({
id: uuidv4(),
name: '',
pinned: true,
filterList: [],
workspaceId: workspace.id,
});
}, [showUpdateCollection, workspace.id]);
return (
<>
<IconButton
data-testid="slider-bar-add-collection-button"
onClick={handleClick}
size="small"
>
<PlusIcon />
</IconButton>
<EditCollectionModel
propertiesMeta={workspace.meta.properties}
getPageInfo={getPageInfo}
onConfirm={setting.saveCollection}
open={show}
onClose={() => showUpdateCollection(false)}
title={t['Save As New Collection']()}
init={defaultCollection}
/>
</>
);
};

View File

@@ -12,6 +12,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteIcon,
FilterIcon,
InformationIcon,
MoreHorizontalIcon,
UnpinIcon,
ViewLayersIcon,
@@ -192,6 +193,7 @@ const CollectionRenderer = ({
/>
<MenuItem
data-testid="collection-item"
data-type="collection-list-item"
ref={setNodeRef}
onCollapsedChange={setCollapsed}
active={isOver}
@@ -251,21 +253,34 @@ export const CollectionsList = ({ workspace }: CollectionsListProps) => {
const metas = useBlockSuitePageMeta(workspace);
const { savedCollections } = useSavedCollections(workspace.id);
const getPageInfo = useGetPageInfoById(workspace);
const pinedCollections = useMemo(
() => savedCollections.filter(v => v.pinned),
[savedCollections]
);
if (pinedCollections.length === 0) {
return (
<MenuItem
data-testid="slider-bar-collection-null-description"
icon={<InformationIcon />}
disabled
>
<span>Create a collection</span>
</MenuItem>
);
}
return (
<div data-testid="collections" className={styles.wrapper}>
{savedCollections
.filter(v => v.pinned)
.map(view => {
return (
<CollectionRenderer
getPageInfo={getPageInfo}
key={view.id}
collection={view}
pages={metas}
workspace={workspace}
/>
);
})}
{pinedCollections.map(view => {
return (
<CollectionRenderer
getPageInfo={getPageInfo}
key={view.id}
collection={view}
pages={metas}
workspace={workspace}
/>
);
})}
</div>
);
};

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

@@ -29,6 +29,7 @@ import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings';
import type { AllWorkspace } from '../../shared';
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
import ImportPage from './import-page';
@@ -190,7 +191,9 @@ export const RootAppSidebar = ({
<SidebarScrollableContainer>
<CategoryDivider label={t['Favorites']()} />
<FavoriteList workspace={blockSuiteWorkspace} />
<CategoryDivider label={t['Collections']()} />
<CategoryDivider label={t['Collections']()}>
<AddCollectionButton workspace={blockSuiteWorkspace} />
</CategoryDivider>
<CollectionsList workspace={blockSuiteWorkspace} />
<CategoryDivider label={t['others']()} />
<RouteMenuLinkItem

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/manager';
} 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/manager';
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';
@@ -10,7 +9,6 @@ import {
ToolContainer,
WorkspaceFallback,
} from '@affine/component/workspace';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootBlockHubAtom,
@@ -28,16 +26,12 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/manager';
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { lazy, Suspense, useCallback, useMemo } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { WorkspaceAdapters } from '../adapters/workspace';
import {
@@ -47,6 +41,7 @@ import {
} from '../atoms';
import { useAppSetting } from '../atoms/settings';
import { AppContainer } from '../components/affine/app-container';
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
import type { IslandItemNames } from '../components/pure/help-island';
import { HelpIsland } from '../components/pure/help-island';
import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections';
@@ -111,20 +106,6 @@ export const CurrentWorkspaceContext = ({
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
const navigate = useNavigate();
// fixme(himself65): this is not a good way to handle this,
// need a better way to check whether this workspace really exist.
useEffect(() => {
const id = setTimeout(() => {
if (!exist) {
navigate('/');
globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
}
}, globalThis.HALTING_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [exist, metadata.length, navigate]);
if (metadata.length === 0) {
return <WorkspaceFallback key="no-workspace" />;
}
@@ -171,32 +152,21 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const { jumpToPage, openPage } = useNavigateHelper();
const { openPage } = useNavigateHelper();
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
useEffect(() => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
setCurrentPageId(currentPageId);
jumpToPage(currentWorkspace.id, page.id);
}
}, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace
);
const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const handleCreatePage = useCallback(() => {
return helper.createPage(nanoid());
}, [helper]);
const id = nanoid();
helper.createPage(id);
const page = currentWorkspace.blockSuiteWorkspace.getPage(id);
assertExists(page);
return page;
}, [currentWorkspace.blockSuiteWorkspace, helper]);
const handleOpenWorkspaceListModal = useCallback(() => {
setOpenWorkspacesModal(true);
}, [setOpenWorkspacesModal]);
@@ -289,7 +259,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
<HelpIsland showList={pageId ? undefined : showList} />
</ToolContainer>
<AffineWatermark />
</MainContainer>
</AppContainer>
<PageListTitleCellDragOverlay />

View File

@@ -1,12 +1,10 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
import { useAtomValue } from 'jotai';
import { lazy, useEffect, useRef } from 'react';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { useWorkspace } from '../hooks/use-workspace';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { rootStore } from '@toeverything/infra/atom';
import { lazy } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
const AllWorkspaceModals = lazy(() =>
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
@@ -14,81 +12,35 @@ const AllWorkspaceModals = lazy(() =>
}))
);
type WorkspaceLoaderProps = {
id: string;
};
const logger = new DebugLogger('index-page');
const WorkspaceLoader = (props: WorkspaceLoaderProps): null => {
useWorkspace(props.id);
export const loader: LoaderFunction = async () => {
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id');
const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
if (target) {
const targetWorkspace = getWorkspace(target.id);
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
({ trash }) => !trash
);
const pageId =
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
nonTrashPages.at(0)?.id;
if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
} else {
logger.debug('Found target workspace. Jump to all page');
return redirect(`/workspace/${targetWorkspace.id}/all`);
}
}
return null;
};
const logger = new DebugLogger('index-page');
export const Component = () => {
const meta = useAtomValue(rootWorkspacesMetadataAtom);
const navigateHelper = useNavigateHelper();
const jumpOnceRef = useRef(false);
useEffect(() => {
if (jumpOnceRef.current) {
return;
}
const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id');
const target =
(lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
if (target) {
const targetWorkspace = getWorkspace(target.id);
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
({ trash }) => !trash
);
const pageId =
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
nonTrashPages.at(0)?.id;
if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
navigateHelper.jumpToPage(
targetWorkspace.id,
pageId,
RouteLogic.REPLACE
);
jumpOnceRef.current = true;
} else {
const clearId = setTimeout(() => {
dispose.dispose();
logger.debug('Found target workspace. Jump to all pages');
navigateHelper.jumpToSubPath(
targetWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
jumpOnceRef.current = true;
}, 1000);
const dispose = targetWorkspace.slots.pageAdded.once(pageId => {
clearTimeout(clearId);
navigateHelper.jumpToPage(
targetWorkspace.id,
pageId,
RouteLogic.REPLACE
);
jumpOnceRef.current = true;
});
return () => {
clearTimeout(clearId);
dispose.dispose();
jumpOnceRef.current = false;
};
}
} else {
console.warn('No workspace found');
}
return;
}, [meta, navigateHelper]);
return (
<>
{meta.map(({ id }) => (
<WorkspaceLoader id={id} key={id} />
))}
<AllWorkspaceModals />
</>
);

View File

@@ -1,15 +1,38 @@
import { useCollectionManager } from '@affine/component/page-list';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { useCallback } from 'react';
import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
import { useAtom } from 'jotai/react';
import { useCallback, useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
const AllPage = () => {
export const loader: LoaderFunction = async args => {
const workspaceId = args.params.workspaceId;
assertExists(workspaceId);
const workspaceAtom = getActiveBlockSuiteWorkspaceAtom(workspaceId);
const workspace = await rootStore.get(workspaceAtom);
const page = workspace.getPage(
`${workspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
workspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
return redirect(`/workspace/${workspace.id}/${page.id}`);
}
return null;
};
export const AllPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
const setting = useCollectionManager(currentWorkspace.id);
const onClickPage = useCallback(
@@ -23,6 +46,18 @@ const AllPage = () => {
},
[currentWorkspace, jumpToPage]
);
useEffect(() => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
setCurrentPageId(currentPageId);
jumpToPage(currentWorkspace.id, page.id);
}
}, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
return (
<>
@@ -42,9 +77,5 @@ const AllPage = () => {
};
export const Component = () => {
return (
<WorkspaceLayout>
<AllPage />
</WorkspaceLayout>
);
return <AllPage />;
};

View File

@@ -7,18 +7,18 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useAtom } from 'jotai/react';
import { type ReactElement, useCallback, useEffect } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import type { LoaderFunction } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
const WorkspaceDetailPageImpl = (): ReactElement => {
const DetailPageImpl = (): ReactElement => {
const { openPage, jumpToSubPath } = useNavigateHelper();
const currentPageId = useAtomValue(currentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
@@ -27,7 +27,7 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const collectionManager = useCollectionManager(currentWorkspace.id);
const onLoad = useCallback(
(page: Page, editor: EditorContainer) => {
(_: Page, editor: EditorContainer) => {
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId);
});
@@ -68,10 +68,10 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
);
};
const WorkspaceDetailPage = (): ReactElement => {
export const DetailPage = (): ReactElement => {
const { workspaceId, pageId } = useParams();
const location = useLocation();
const navigate = useNavigate();
const { jumpTo404 } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const page = currentPageId
@@ -91,7 +91,14 @@ const WorkspaceDetailPage = (): ReactElement => {
const page =
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
navigate('/404');
jumpTo404();
} else {
// fixme: cleanup jumpOnce in the right time
if (page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.setPageMeta(currentPageId, {
jumpOnce: false,
});
}
}
}
}
@@ -99,8 +106,8 @@ const WorkspaceDetailPage = (): ReactElement => {
currentPageId,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
jumpTo404,
location.pathname,
navigate,
pageId,
setCurrentPageId,
workspaceId,
@@ -110,13 +117,17 @@ const WorkspaceDetailPage = (): ReactElement => {
if (!currentPageId || !page) {
return <PageDetailSkeleton key="current-page-is-null" />;
}
return <WorkspaceDetailPageImpl />;
return <DetailPageImpl />;
};
export const loader: LoaderFunction = args => {
if (args.params.pageId) {
localStorage.setItem('last_page_id', args.params.pageId);
rootStore.set(currentPageIdAtom, args.params.pageId);
}
return null;
};
export const Component = () => {
return (
<WorkspaceLayout>
<WorkspaceDetailPage />
</WorkspaceLayout>
);
return <DetailPage />;
};

View File

@@ -0,0 +1,26 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { currentWorkspaceIdAtom, rootStore } from '@toeverything/infra/atom';
import type { ReactElement } from 'react';
import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
export const loader: LoaderFunction = async args => {
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
if (!meta.some(({ id }) => id === args.params.workspaceId)) {
return redirect('/404');
}
if (args.params.workspaceId) {
localStorage.setItem('last_workspace_id', args.params.workspaceId);
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
}
return null;
};
export const Component = (): ReactElement => {
return (
<WorkspaceLayout>
<Outlet />
</WorkspaceLayout>
);
};

View File

@@ -6,9 +6,8 @@ import { getUIAdapter } from '../../adapters/workspace';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
const TrashPage = () => {
export const TrashPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const onClickPage = useCallback(
@@ -44,9 +43,5 @@ const TrashPage = () => {
};
export const Component = () => {
return (
<WorkspaceLayout>
<TrashPage />
</WorkspaceLayout>
);
return <TrashPage />;
};

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/manager';
} from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { FC, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useTransition } from 'react';
@@ -124,7 +124,7 @@ export const AllWorkspaceModals = (): ReactElement => {
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [transitioning, transition] = useTransition();
const [isPending, startTransition] = useTransition();
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
const handleOpenSettingModal = useCallback(
@@ -143,7 +143,7 @@ export const AllWorkspaceModals = (): ReactElement => {
<>
<Suspense>
<WorkspaceListModal
disabled={transitioning}
disabled={isPending}
workspaces={workspaces}
currentWorkspaceId={currentWorkspaceId}
open={
@@ -157,10 +157,10 @@ export const AllWorkspaceModals = (): ReactElement => {
(activeId, overId) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
transition(() => {
startTransition(() => {
setWorkspaces(workspaces =>
arrayMove(workspaces, oldIndex, newIndex)
).catch(console.error);
);
});
},
[setWorkspaces, workspaces]
@@ -195,11 +195,13 @@ export const AllWorkspaceModals = (): ReactElement => {
setOpenCreateWorkspaceModal(false);
}, [setOpenCreateWorkspaceModal])}
onCreate={useCallback(
async id => {
setOpenCreateWorkspaceModal(false);
setOpenWorkspacesModal(false);
setCurrentWorkspaceId(id);
return jumpToSubPath(id, WorkspaceSubPath.ALL);
id => {
startTransition(() => {
setOpenCreateWorkspaceModal(false);
setOpenWorkspacesModal(false);
setCurrentWorkspaceId(id);
jumpToSubPath(id, WorkspaceSubPath.ALL);
});
},
[
jumpToSubPath,

37
apps/core/src/router.ts Normal file
View File

@@ -0,0 +1,37 @@
import { createBrowserRouter } from 'react-router-dom';
export const router = createBrowserRouter(
[
{
path: '/',
lazy: () => import('./pages/index'),
},
{
path: '/workspace/:workspaceId',
lazy: () => import('./pages/workspace/index'),
children: [
{
path: 'all',
lazy: () => import('./pages/workspace/all-page'),
},
{
path: 'trash',
lazy: () => import('./pages/workspace/trash-page'),
},
{
path: ':pageId',
lazy: () => import('./pages/workspace/detail-page'),
},
],
},
{
path: '/404',
lazy: () => import('./pages/404'),
},
],
{
future: {
v7_normalizeFormMethod: true,
},
}
);

View File

@@ -16,7 +16,7 @@
"jsxImportSource": "@emotion/react",
"incremental": true,
"experimentalDecorators": true,
"types": ["webpack-env"]
"types": ["webpack-env", "ses"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"],

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.7.0-canary.51",
"version": "0.8.0-canary.10",
"type": "module",
"private": true,
"scripts": {
@@ -10,12 +10,12 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/blocks": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/editor": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/global": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/lit": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"express": "^4.18.2",
"jotai": "^2.2.2",
"react": "18.3.0-canary-1fdacbefd-20230630",
@@ -24,12 +24,12 @@
"waku": "0.12.1"
},
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react": "^18.2.17",
"@types/react-dom": "^18.2.7",
"@vanilla-extract/css": "^1.12.0",
"@vanilla-extract/vite-plugin": "^3.8.2",
"autoprefixer": "^10.4.14",
"tailwindcss": "^3.3.2",
"tailwindcss": "^3.3.3",
"typescript": "^5.1.6"
}
}

View File

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

@@ -47,6 +47,9 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
test('export then add', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
await page.focus('.affine-default-page-block-title');
await page.fill('.affine-default-page-block-title', 'test1');
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
@@ -108,4 +111,11 @@ test('export then add', async ({ page, appInfo, workspace }) => {
expect(newWorkspace.id).not.toBe(originalId);
// check its name is correct
await expect(page.getByTestId('workspace-name')).toHaveText(newWorkspaceName);
// find button which has the title "test1"
const test1PageButton = await page.waitForSelector(`text="test1"`);
await test1PageButton.click();
const title = page.locator('[data-block-is-title] >> text="test1"');
await expect(title).toBeVisible();
});

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,51 +131,7 @@ module.exports = {
// We need the following line for updater
extraResource: ['./resources/app-update.yml'],
},
makers: [
{
name: '@electron-forge/maker-dmg',
config: {
format: 'ULFO',
icon: icnsPath,
name: 'AFFiNE',
'icon-size': 128,
background: './resources/icons/dmg-background.png',
contents: [
{
x: 176,
y: 192,
type: 'file',
path: path.resolve(
__dirname,
'out',
buildType,
`${productName}-darwin-${arch}`,
`${productName}.app`
),
},
{ x: 432, y: 192, type: 'link', path: '/Applications' },
],
},
},
{
name: '@electron-forge/maker-zip',
config: {
name: 'affine',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['darwin', 'linux', 'win32'],
},
},
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'AFFiNE',
setupIcon: icoPath,
iconUrl: windowsIconUrl,
loadingGif: './resources/icons/affine_installing.gif',
},
},
],
makers,
hooks: {
readPackageJson: async (_, packageJson) => {
// we want different package name for canary build
@@ -112,17 +142,28 @@ module.exports = {
const { rm, cp } = require('node:fs/promises');
const { resolve } = require('node:path');
await rm(
resolve(__dirname, './node_modules/@toeverything/plugin-infra'),
await rm(resolve(__dirname, './node_modules/@toeverything/infra'), {
recursive: true,
force: true,
});
await cp(
resolve(__dirname, '../../packages/infra'),
resolve(__dirname, './node_modules/@toeverything/infra'),
{
recursive: true,
force: true,
}
);
await rm(resolve(__dirname, './node_modules/@affine/sdk'), {
recursive: true,
force: true,
});
await cp(
resolve(__dirname, '../../packages/plugin-infra'),
resolve(__dirname, './node_modules/@toeverything/plugin-infra'),
resolve(__dirname, '../../packages/sdk'),
resolve(__dirname, './node_modules/@affine/sdk'),
{
recursive: true,
force: true,

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.7.0-canary.51",
"version": "0.8.0-canary.10",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -10,6 +10,7 @@
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
"scripts": {
"start": "electron .",
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "NODE_ENV=production zx scripts/build-layers.mjs",
@@ -26,38 +27,39 @@
"@affine-test/kit": "workspace:*",
"@affine/env": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/editor": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/lit": "0.0.0-20230719163314-76d863fc-nightly",
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly",
"@affine/sdk": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"@electron-forge/cli": "^6.2.1",
"@electron-forge/core": "^6.2.1",
"@electron-forge/core-utils": "^6.2.1",
"@electron-forge/maker-deb": "^6.2.1",
"@electron-forge/maker-dmg": "^6.2.1",
"@electron-forge/maker-squirrel": "^6.2.1",
"@electron-forge/maker-zip": "^6.2.1",
"@electron-forge/shared-types": "^6.2.1",
"@electron/remote": "2.0.10",
"@toeverything/infra": "workspace:*",
"@reforged/maker-appimage": "^3.3.1",
"@types/fs-extra": "^11.0.1",
"@types/uuid": "^9.0.2",
"cross-env": "7.0.3",
"electron": "^25.2.0",
"electron": "^25.3.1",
"electron-log": "^5.0.0-beta.24",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
"esbuild": "^0.18.11",
"esbuild": "^0.18.15",
"fs-extra": "^11.1.1",
"jotai": "^2.2.2",
"ts-node": "^10.9.1",
"undici": "^5.22.1",
"uuid": "^9.0.0",
"vitest": "^0.33.0",
"which": "^3.0.1",
"zx": "^7.2.3"
},
"dependencies": {
"@toeverything/plugin-infra": "workspace:*",
"@toeverything/infra": "workspace:*",
"async-call-rpc": "^6.3.1",
"electron-updater": "^6.0.0",
"link-preview-js": "^3.0.4",

View File

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

View File

@@ -15,7 +15,6 @@ if (process.platform === 'win32') {
async function buildLayers() {
const common = config();
await esbuild.build(common.workers);
await esbuild.build({
...common.layers,
define: {

Some files were not shown because too many files have changed in this diff Show More