Compare commits

..

113 Commits

Author SHA1 Message Date
Alex Yang
b509302711 v0.7.0-canary.41 2023-07-12 14:49:08 +08:00
Alex Yang
e51c98c1dd chore: bump version (#3179) 2023-07-12 06:21:11 +00:00
Alex Yang
bbb1387469 feat: display app version in setting panel (#3170) 2023-07-12 02:39:00 +00:00
xiaodong zuo
4f88774999 fix: the image lost after exporting (#3150)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-12 02:21:23 +00:00
Alex Yang
3968deb6d4 feat: add suspense to workspace settings (#3167)
Co-authored-by: Qi <474021214@qq.com>
2023-07-11 15:50:30 +00:00
Alex Yang
37c8465af8 fix: jump to index page after deletion (#3169) 2023-07-11 15:44:00 +00:00
Peng Xiao
d88a21d24a fix: settings style update (#3161) 2023-07-11 12:55:28 +00:00
3720
6ad2d106bc fix: some typo and i18n (#3155) 2023-07-11 11:04:45 +00:00
Alex Yang
8c1fcee135 refactor: remove unused code (#3149) 2023-07-11 08:53:01 +00:00
Peng Xiao
0514da9759 fix: updater not working (#3144) 2023-07-11 07:06:04 +00:00
JimmFly
b2fed03f30 style: modify the style of community item (#3143) 2023-07-11 06:44:06 +00:00
Alex Yang
f5e45573af v0.7.0-canary.40 2023-07-11 12:59:12 +08:00
Alex Yang
ddb2931f38 fix: remove workspace not working (#3140) 2023-07-11 04:37:47 +00:00
Alex Yang
acf17ebace chore: bump version (#3138) 2023-07-11 04:28:01 +00:00
Alex Yang
7af3c05b8b v0.7.0-canary.39 2023-07-10 21:00:06 +08:00
Alex Yang
01de2ae714 revert: restrict node version 2023-07-10 20:51:49 +08:00
Qi
cfa18d1bc3 fix: font style setting only control editor's font (#3117)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-10 11:58:53 +00:00
Alex Yang
127c63601e chore: bump version (#3131) 2023-07-10 11:34:43 +00:00
LongYinan
f079b0b49a fix: add semver into server dependencies 2023-07-10 19:32:39 +08:00
Alex Yang
6caf934d47 refactor: follow correct react rules (#3119) 2023-07-10 10:32:15 +00:00
Qi
2f910fbad0 feat: modify setting modal entry in quick search modal (#3089) 2023-07-10 09:28:14 +00:00
Peng Xiao
dac4e390aa fix: add DB migration to add workspace (#3115) 2023-07-10 08:03:18 +00:00
JimmFly
812e0e9c9a style: change switch tip color (#3123) 2023-07-10 07:00:23 +00:00
Alex Yang
05291a8a36 chore: restrict node version (#3120) 2023-07-10 06:19:59 +00:00
JimmFly
8bcc4d6a57 test: fix incorrect day suffix (#3121) 2023-07-10 05:56:12 +00:00
danielchim
e06d5e1c8d fix: page mode shortcut (#3097) 2023-07-09 18:37:49 +00:00
Alex Yang
1c8895f23f feat: improve error log message (#3112) 2023-07-09 05:54:53 +00:00
Alex Yang
8b5d997322 refactor(hooks): reduce null types (#3111) 2023-07-09 05:01:09 +00:00
Peng Xiao
33644a68b2 fix: disable move db by default (#3105)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-09 03:37:39 +00:00
mon-jai
bc85ad5b65 fix: sidebar noise background on Windows (#3107) 2023-07-08 16:41:07 +00:00
Alex Yang
fe895905bd v0.7.0-canary.38 2023-07-08 15:57:31 +08:00
Alex Yang
3c5ccd7231 fix: init workspace before loaded (#3104) 2023-07-08 07:42:30 +00:00
Alex Yang
da140b0b85 chore: remove unused code (#3102) 2023-07-08 06:49:11 +00:00
Alex Yang
c4d53d59b5 test: fix flaky (#3100) 2023-07-08 06:30:17 +00:00
boomlion8
a48726d088 fix: color of UI in dark mode (#3081)
Co-authored-by: boomlion8 <201116201@manit.ac.in>
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-08 06:00:03 +00:00
Alex Yang
b49306607b feat: improve workspace hook (#3099) 2023-07-08 05:43:39 +00:00
Alex Yang
3d15c60cb1 v0.7.0-canary.37 2023-07-08 02:55:18 +08:00
Alex Yang
283f0cd263 refactor: lazy load workspaces (#3091) 2023-07-07 14:15:27 +00:00
JimmFly
66152401be chore: add new item for share component (#3084) 2023-07-07 13:16:49 +00:00
Qi
b12412a3c1 feat: add font style setting (#3092) 2023-07-07 11:59:38 +00:00
Peng Xiao
ce1e8d868c fix: a possible issue on electron flaky test (#3094) 2023-07-07 11:02:58 +00:00
Alex Yang
3294043180 perf: reduce unused provider connection (#3090) 2023-07-07 08:13:32 +00:00
Alex Yang
152fbaabda ci: fix nx.yml (#3086) 2023-07-07 05:37:40 +00:00
JimmFly
5756bdf8d7 style: adjust settings style (#3083) 2023-07-07 05:36:27 +00:00
Alex Yang
80ee33fd3e chore: bump version (#3078) 2023-07-07 01:55:11 +00:00
Alex Yang
955d80e2c1 test: image preview e2e (#3080)
Co-authored-by: danielchim <kahungchim@gmail.com>
2023-07-06 23:24:03 +00:00
Alex Yang
67fe7f04da build: fix nx inputs (#3079) 2023-07-07 01:15:04 +08:00
Alex Yang
6395521f09 test: upgrade playwright (#3077) 2023-07-06 16:15:18 +00:00
Alex Yang
822078e640 fix: cleanup workspace when switch setting panel (#3072) 2023-07-06 15:27:09 +00:00
Alex Yang
fafd93f7dc refactor: block-hub in tool wrapper (#3073) 2023-07-06 15:18:58 +00:00
Peng Xiao
00ce086e79 fix: workspace storage settings issues (#3055) 2023-07-06 12:48:20 +00:00
Alex Yang
28653d6892 fix(web): setting panel refresh (#3070) 2023-07-06 11:24:26 +00:00
Alex Yang
e30c67482f fix(web): fetch hello-world from local (#3062) 2023-07-06 09:46:17 +00:00
Pratik Kumar
bda28e0404 fix(component): new page button in all page (#3053) 2023-07-06 09:40:37 +00:00
Alex Yang
ce63364299 fix(component): image preview fallback (#3058) 2023-07-06 09:22:23 +00:00
JimmFly
f468dff6aa chore: update communities link and icon (#3052) 2023-07-06 07:24:36 +00:00
Peng Xiao
fab03006e8 fix: menu item click area (#3051) 2023-07-06 06:53:50 +00:00
JimmFly
8a565b8633 fix: date-picker hidden in update collection (#3045) 2023-07-06 06:17:19 +00:00
Alex Yang
e79a6a5d47 v0.7.0-canary.36 2023-07-06 14:07:27 +08:00
Pratik Kumar
95c2e20cb5 fix(component): all page list UI padding (#3046)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-06 05:56:08 +00:00
JimmFly
2e0f410978 chore: temporary fix sync script error (#3044) 2023-07-06 12:30:01 +08:00
Alex Yang
fa1cd87348 chore: bump version (#3041) 2023-07-06 03:49:17 +00:00
Alex Yang
e95d28e136 fix: workspace name should change in the setting panel (#3039) 2023-07-06 02:19:06 +00:00
Qi
87ba71e77e fix: a series of setting issues (#3032) 2023-07-05 14:11:42 +00:00
Peng Xiao
dec0c0d3d1 fix: delete workspace in settings (#3030) 2023-07-05 10:31:11 +00:00
Peng Xiao
776172bc88 fix: updater issues (#3027) 2023-07-05 09:29:11 +00:00
Alex Yang
d582548ed8 v0.7.0-canary.35 2023-07-05 16:02:31 +08:00
Alex Yang
70ac31b907 build: remove legacy cloud config (#3024) 2023-07-05 06:57:56 +00:00
Alex Yang
cff9fd1ead chore: bump version (#3023) 2023-07-05 06:54:09 +00:00
Alex Yang
319febb00d docs: update README.md 2023-07-05 14:31:47 +08:00
3720
72fa2da2d3 fix: tags does not exist (#3020) 2023-07-05 04:06:44 +00:00
Alex Yang
3084c427f1 feat: update server login feature (#3004)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-07-05 03:13:20 +00:00
3720
9cd1f013f8 fix: flaky tests (#3019) 2023-07-05 02:50:43 +00:00
Alex Yang
a3f58d4302 v0.7.0-canary.34 2023-07-05 02:23:53 +08:00
Alex Yang
d4cb89eafc chore: bump version (#3016) 2023-07-04 17:52:40 +00:00
Peng Xiao
33ba034336 fix: sqlite provider import sub doc db file (#2991)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-04 17:47:42 +00:00
Alex Yang
e158c09160 chore: update pre-commit (#3017) 2023-07-04 17:42:14 +00:00
JimmFly
c6ccd6d5de chore: update setting text (#3000) 2023-07-04 17:40:58 +00:00
Alex Yang
ec87864c34 refactor: simplify code (#3015) 2023-07-04 17:34:22 +00:00
Alex Yang
a06ba403d0 ci: check macOS arm64 bundle output (#3012) 2023-07-04 16:59:00 +00:00
Alex Yang
dfbec46ded feat(electron): move preload to infra (#3011) 2023-07-04 16:43:30 +00:00
Alex Yang
24be73ef63 chore: bump nx (#3014) 2023-07-04 16:26:43 +00:00
Alex Yang
3976c37d41 v0.7.0-canary.33 2023-07-04 21:52:04 +08:00
Fangdun Tsai
2bc15665b9 chore(electron): renaming clipboard api (#3005) 2023-07-04 12:51:59 +00:00
Alex Yang
e4539dfeb1 fix: bookmark block output missing (#3010) 2023-07-04 12:48:47 +00:00
Qi
1070e17310 feat: modify setting modal (#3008) 2023-07-04 12:37:46 +00:00
Alex Yang
b4f7eb36ef v0.7.0-canary.32 2023-07-04 16:12:09 +08:00
3720
000f802baa feat: add tags support (#2988)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-04 07:32:11 +00:00
Alex Yang
e871ffcba0 refactor: input component (#2999) 2023-07-04 06:52:46 +00:00
Alex Yang
8d2ffe3936 chore: bump version (#2998) 2023-07-04 06:47:35 +00:00
ShortCipher5
9e253420d2 docs: update README.md (#2997) 2023-07-04 14:13:25 +08:00
Alex Yang
edb7847e95 test: use static server (#2996) 2023-07-04 05:37:06 +00:00
Alex Yang
3d70148e0f chore: add circular check (#2995) 2023-07-04 04:54:08 +00:00
Alex Yang
7f89b197da build: enable next server (#2992) 2023-07-04 01:59:06 +00:00
danielchim
32692bd54a feat: page mode shortcut (#2985) 2023-07-03 16:23:53 +00:00
Alex Yang
7b2acec7c3 v0.7.0-canary.31 2023-07-03 23:14:38 +08:00
Alex Yang
f1adf23631 chore: bump version (#2989) 2023-07-03 14:51:49 +00:00
Alex Yang
a5d2fafad6 refactor: remove legacy cloud (#2987) 2023-07-03 14:29:37 +00:00
xiaodong zuo
3d0a907b49 fix: dark mode export PDF leaves margin and notification (#2978) 2023-07-03 12:11:07 +00:00
LongYinan
bacd00655d ci: reduce yarn cache (#2983) 2023-07-03 11:09:17 +00:00
Peng Xiao
08e003b0f6 fix: potential updater issue (#2973) 2023-07-03 11:04:45 +00:00
Alex Yang
0f1c5163a1 feat: remove old setting page by default (#2980) 2023-07-03 10:59:23 +00:00
JimmFly
18874d0d1e chore: add import to sidebar (#2981) 2023-07-03 10:51:28 +00:00
Peng Xiao
7f0a74c694 fix: some potential tests issue (#2982) 2023-07-03 10:46:47 +00:00
Peng Xiao
901fc87716 fix: potential race condition on app load when migration (#2977)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-03 09:40:02 +00:00
Alex Yang
ee2ab4086f fix(web): hydration issue (#2974) 2023-07-03 09:06:12 +00:00
JimmFly
af94674c18 style: adjust icon button hover color (#2976) 2023-07-03 08:46:16 +00:00
Alex Yang
262289a398 chore: add affine-cloud build config (#2971) 2023-07-03 07:25:14 +00:00
Alex Yang
467eab4ddf build: update build config (#2967) 2023-07-03 06:17:13 +00:00
Alex Yang
63517e4912 chore: update 'lint-staged' rules (#2969) 2023-07-03 06:00:50 +00:00
JimmFly
6f9487deb7 style: adjust copilot chat style (#2915) 2023-07-03 05:57:30 +00:00
JimmFly
8d0edd5255 i18n: update translation resources (#2968) 2023-07-03 05:15:46 +00:00
Qi
bdea153c82 feat: modify preloading data (#2947) 2023-07-03 02:49:55 +00:00
385 changed files with 6446 additions and 11935 deletions

View File

@@ -8,3 +8,4 @@ _next
lib
.eslintrc.js
packages/i18n/src/i18n-generated.ts
e2e-dist-*

View File

@@ -21,6 +21,11 @@ const createPattern = packageName => [
message: 'Do not import package itself',
allowTypeImports: false,
},
{
group: ['@blocksuite/store'],
message: "Import from '@blocksuite/global/utils'",
importNames: ['assertExists', 'assertEquals'],
},
];
const allPackages = [
@@ -134,6 +139,11 @@ const config = {
message: "Don't import from src",
allowTypeImports: false,
},
{
group: ['@blocksuite/store'],
message: "Import from '@blocksuite/global/utils'",
importNames: ['assertExists', 'assertEquals'],
},
],
},
],
@@ -204,6 +214,7 @@ const config = {
'scripts/**/*',
'**/benchmark/**/*',
'**/__debug__/**/*',
'**/e2e/**/*',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 0,

View File

@@ -13,10 +13,18 @@ inputs:
description: 'Run the install step for Playwright.'
required: false
default: 'false'
electron-install:
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
default: 'true'
runs:
using: 'composite'
@@ -29,33 +37,10 @@ runs:
scope: '@toeverything'
cache: 'yarn'
- name: Expose yarn config as "$GITHUB_OUTPUT"
id: yarn-config
- name: Set nmMode
if: ${{ inputs.hard-link-nm == 'true' }}
shell: bash
run: |
echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: Restore yarn cache
uses: actions/cache@v3
id: yarn-download-cache
with:
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
restore-keys: |
yarn-download-cache-
- name: Restore node_modules cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Restore yarn install state
id: yarn-install-state-cache
uses: actions/cache@v3
with:
path: .yarn/ci-cache/
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
run: yarn config set nmMode hardlinks-local
- name: yarn install
if: ${{ inputs.package-install == 'true' }}
@@ -64,9 +49,9 @@ runs:
run: yarn install ${{ inputs.extra-flags }}
env:
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
YARN_ENABLE_GLOBAL_CACHE: 'false'
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
- name: yarn install (try again)
if: ${{ steps.install.outcome == 'failure' }}
@@ -74,9 +59,9 @@ runs:
run: yarn install ${{ inputs.extra-flags }}
env:
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
YARN_ENABLE_GLOBAL_CACHE: 'false'
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
- name: Get installed Playwright version
id: playwright-version
@@ -113,3 +98,30 @@ runs:
shell: bash
if: inputs.playwright-install == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
- name: Get installed Electron version
id: electron-version
if: ${{ inputs.electron-install == 'true' }}
shell: bash
run: |
echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
id: electron-cache
if: ${{ inputs.electron-install == 'true' }}
with:
path: 'node_modules/.cache/electron'
key: '${{ runner.os }}-electron-${{ steps.electron-version.outputs.version }}'
restore-keys: |
${{ runner.os }}-electron-
- name: Install Electron binary
shell: bash
if: inputs.electron-install == 'true'
run: node apps/electron/node_modules/electron/install.js
env:
ELECTRON_OVERRIDE_DIST_PATH: ./node_modules/.cache/electron
- name: Build Infra
shell: bash
run: yarn run build:infra

View File

@@ -43,14 +43,19 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
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 --max-warnings=0 --cache
run: yarn lint:eslint --max-warnings=0
- name: Run Prettier
run: yarn prettier . --ignore-unknown --cache --check
# Set nmMode in `actions/setup-node` will modify the .yarnrc.yml
run: |
git checkout .yarnrc.yml
yarn lint:prettier
- name: Run circular
run: yarn circular
- name: Upload server dist
@@ -69,6 +74,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- run: yarn nx build @affine/docs
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
@@ -82,6 +89,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- run: yarn nx build @affine/storybook
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
@@ -187,6 +196,7 @@ jobs:
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Download storybook artifact
uses: actions/download-artifact@v3
with:
@@ -213,6 +223,7 @@ jobs:
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Download artifact
uses: actions/download-artifact@v3
with:
@@ -256,6 +267,7 @@ jobs:
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Download next static
uses: actions/download-artifact@v3
@@ -327,6 +339,7 @@ jobs:
uses: ./.github/actions/setup-node
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
@@ -339,8 +352,11 @@ jobs:
env:
NATIVE_TEST: 'true'
- name: Build Infra
run: yarn run build:infra
- name: Download static resource artifact
uses: actions/download-artifact@v3
with:
name: next-js-static
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
@@ -348,12 +364,6 @@ jobs:
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Download static resource artifact
uses: actions/download-artifact@v3
with:
name: next-js-static
path: ./apps/electron/resources/web-static
- 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
@@ -366,6 +376,17 @@ jobs:
env:
COVERAGE: true
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
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
- name: Collect code coverage report
if: ${{ matrix.spec.test }}
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
@@ -396,9 +417,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Infra
run: yarn run build:infra
with:
electron-install: false
- name: Unit Test
run: yarn nx test:coverage @affine/monorepo

View File

@@ -123,9 +123,6 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build Infra
run: yarn run build:infra
- name: Build Plugins
run: yarn run build:plugins

View File

@@ -30,17 +30,25 @@ jobs:
name: Nx Cloud - Main Job
uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.13.0
with:
runs-on: macos-latest
main-branch-name: master
number-of-agents: 5
init-commands: |
yarn exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=3
parallel-commands: |
yarn exec nx-cloud record -- yarn exec nx format:check
yarn exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=5
environment-variables: |
BUILD_TYPE=canary
# parallel-commands: |
# yarn exec nx-cloud record -- yarn exec nx format:check
parallel-commands-on-agents: |
yarn exec nx affected --target=build --parallel=5
timeout: 60
agents:
name: Nx Cloud - Agents
uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.13.0
with:
runs-on: macos-latest
number-of-agents: 5
environment-variables: |
BUILD_TYPE=canary
timeout: 60

View File

@@ -123,9 +123,6 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build Infra
run: yarn run build:infra
- name: Build Plugins
run: yarn run build:plugins

21
.github/workflows/workers.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Deploy Cloudflare Worker
on:
push:
branches:
- master
paths:
- packages/workers/**
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
environment: production
steps:
- uses: actions/checkout@v2
- name: Publish
uses: cloudflare/wrangler-action@2.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
workingDirectory: 'packages/workers'

View File

@@ -2,10 +2,22 @@
. "$(dirname -- "$0")/_/husky.sh"
# check lockfile is up to date
yarn install --mode=update-lockfile
yarn install --mode=skip-build --inline-builds --immutable
# build infra code
yarn -T run build:infra
# generate prisma client type
yarn workspace @affine/server prisma generate
# generate i18n
yarn i18n-codegen gen
# lint staged files
yarn exec lint-staged
# type check
yarn typecheck
# circular dependency check
yarn circular

1
.npmrc
View File

@@ -1,2 +1,3 @@
shell-emulator=true
electron_mirror="https://cdn.npmmirror.com/binaries/electron/"
engine-strict=true

View File

@@ -10,3 +10,5 @@ dist
.yarn
tests/affine-legacy/0.7.0-canary.18/static
.github/helm
_next
storybook-static

View File

@@ -2,13 +2,13 @@
<h1 style="border-bottom: none">
<b><a href="https://affine.pro">AFFiNE.PRO</a></b><br />
The Next-Gen Collaborative Knowledge Base
Write, Draw and Plan All at Once
<br>
</h1>
<p>
AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together.<br />
Privacy first, open-source, customizable and ready to use - a free replacement for Notion & Miro. <br />
One hyper-fused platform for wildly creative minds. <br />
A privacy-focussed, local-first, open-source, and ready-to-use alternative for Notion & Miro.
</p>
</div>
@@ -60,7 +60,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<br />
<div align="center">
<em>See docs, canvas and tables are hyper merged with AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
<em>Docs, canvas and tables are hyper-merged with AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
</div>
<br />
@@ -123,6 +123,8 @@ If you have questions, you are welcome to contact us. One of the best places to
## Plugins
> Plugins are a way to extend the functionality of AFFiNE.
>
> (Currently, plugins are under heavy development, and the SDK is not yet available.)
| Name | |
| ------------------------------------------------ | ----------------------------------------- |
@@ -156,7 +158,8 @@ We would like to express our gratitude to all the individuals who have already c
## Self-Host
Get started with Docker and deploy your own feature-rich, restriction-free deployment of AFFiNE - check the [latest packages].
Get started with Docker and deploy your own feature-rich, restriction-free deployment of AFFiNE.
We are working hard to get this updated to the latest version, you can keep an eye on the [latest packages].
## Hiring

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.7.0-canary.30",
"version": "0.7.0-canary.41",
"type": "module",
"private": true,
"scripts": {
@@ -10,17 +10,17 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/blocks": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/editor": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/global": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/lit": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/store": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/block-std": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/global": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
"express": "^4.18.2",
"jotai": "^2.2.1",
"react": "18.3.0-canary-8ec962d82-20230623",
"react-dom": "18.3.0-canary-8ec962d82-20230623",
"react-server-dom-webpack": "18.3.0-canary-8ec962d82-20230623",
"jotai": "^2.2.2",
"react": "18.3.0-canary-1fdacbefd-20230630",
"react-dom": "18.3.0-canary-1fdacbefd-20230630",
"react-server-dom-webpack": "18.3.0-canary-1fdacbefd-20230630",
"waku": "0.12.1"
},
"devDependencies": {

View File

@@ -13,3 +13,5 @@ resources/web-static
!.yarn/sdks
!.yarn/versions
dev.json
zip-out

View File

@@ -149,3 +149,35 @@ test('windows only check', async ({ page }) => {
await expect(windowOnlyUI).not.toBeVisible();
}
});
test('delete workspace', async ({ page }) => {
await page.getByTestId('current-workspace').click();
await page.getByTestId('add-or-new-workspace').click();
await page.getByTestId('new-workspace').click();
await page.getByTestId('create-workspace-default-location-button').click();
await page.getByTestId('create-workspace-input').type('Delete Me');
await page.getByTestId('create-workspace-create-button').click();
await page.getByTestId('create-workspace-continue-button').click();
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('current-workspace-label').click();
expect(await page.getByTestId('workspace-name-input').inputValue()).toBe(
'Delete Me'
);
const contentElement = await page.getByTestId('setting-modal-content');
const boundingBox = await contentElement.boundingBox();
if (!boundingBox) {
throw new Error('boundingBox is null');
}
await page.mouse.move(
boundingBox.x + boundingBox.width / 2,
boundingBox.y + boundingBox.height / 2
);
await page.mouse.wheel(0, 500);
await page.getByTestId('delete-workspace-button').click();
await page.getByTestId('delete-workspace-input').type('Delete Me');
await page.getByTestId('delete-workspace-confirm-button').click();
await page.waitForTimeout(1000);
expect(await page.getByTestId('workspace-name').textContent()).toBe(
'Demo Workspace'
);
});

View File

@@ -16,6 +16,8 @@ function generateUUID() {
return crypto.randomUUID();
}
type RoutePath = 'setting';
export const test = base.extend<{
page: Page;
electronApp: ElectronApplication;
@@ -28,6 +30,9 @@ export const test = base.extend<{
// get current workspace
current: () => Promise<any>; // todo: type
};
router: {
goto: (path: RoutePath) => Promise<void>;
};
}>({
page: async ({ electronApp }, use) => {
const page = await electronApp.firstWindow();
@@ -41,10 +46,6 @@ export const test = base.extend<{
});
});
}
const logFilePath = await page.evaluate(async () => {
// @ts-expect-error
return window.apis?.debug.logFilePath();
});
// wat for blocksuite to be loaded
await page.waitForSelector('v-line');
if (enableCoverage) {
@@ -71,10 +72,6 @@ export const test = base.extend<{
);
}
await page.close();
if (logFilePath) {
const logs = await fs.readFile(logFilePath, 'utf-8');
console.log(logs);
}
},
electronApp: async ({}, use) => {
// a random id to avoid conflicts between tests

View File

@@ -5,7 +5,7 @@ import fs from 'fs-extra';
import { test } from './fixture';
test.skip('check workspace has a DB file', async ({ appInfo, workspace }) => {
test('check workspace has a DB file', async ({ appInfo, workspace }) => {
const w = await workspace.current();
const dbPath = path.join(
appInfo.sessionData,
@@ -19,9 +19,11 @@ test.skip('check workspace has a DB file', async ({ appInfo, workspace }) => {
test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
// goto settings
await settingButton.click();
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
// goto workspace setting
await page.getByTestId('workspace-list-item').click();
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp-dir');
@@ -42,21 +44,26 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
expect(files.some(f => f.endsWith('.affine'))).toBe(true);
});
test.skip('export then add', async ({ page, appInfo, workspace }) => {
test('export then add', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
// goto settings
await settingButton.click();
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
const originalId = w.id;
const newWorkspaceName = 'new-test-name';
// goto workspace setting
await page.getByTestId('workspace-list-item').click();
await page.waitForTimeout(500);
// change workspace name
await page.getByTestId('workspace-name-input').fill(newWorkspaceName);
await page.getByTestId('save-workspace-name').click();
await page.waitForSelector('text="Update workspace name success"');
await page.click('[data-tab-key="export"]');
await page.waitForTimeout(500);
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp.db');
@@ -73,10 +80,11 @@ test.skip('export then add', async ({ page, appInfo, workspace }) => {
expect(await fs.exists(tmpPath)).toBe(true);
await page.getByTestId('modal-close-button').click();
// add workspace
// we are reusing the same db file so that we don't need to maintain one
// in the codebase
await page.getByTestId('current-workspace').click();
await page.getByTestId('add-or-new-workspace').click();

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.7.0-canary.30",
"version": "0.7.0-canary.41",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -12,11 +12,11 @@
"scripts": {
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "zx scripts/build-layers.mjs",
"build": "NODE_ENV=production zx scripts/build-layers.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"test": "DEBUG=pw:browser playwright test"
"test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts"
},
"config": {
"forge": "./forge.config.js"
@@ -24,11 +24,12 @@
"main": "./dist/main.js",
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/env": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/editor": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/lit": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/store": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
"@electron-forge/cli": "^6.2.1",
"@electron-forge/core": "^6.2.1",
"@electron-forge/core-utils": "^6.2.1",
@@ -48,8 +49,7 @@
"electron-window-state": "^5.0.3",
"esbuild": "^0.18.11",
"fs-extra": "^11.1.1",
"jotai": "^2.2.1",
"playwright": "=1.33.0",
"jotai": "^2.2.2",
"ts-node": "^10.9.1",
"undici": "^5.22.1",
"uuid": "^9.0.0",
@@ -59,7 +59,7 @@
"dependencies": {
"@toeverything/plugin-infra": "workspace:*",
"async-call-rpc": "^6.3.1",
"electron-updater": "^5.3.0",
"electron-updater": "^6.0.0",
"link-preview-js": "^3.0.4",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
@@ -81,7 +81,6 @@
"hoistingLimits": "workspaces"
},
"peerDependencies": {
"playwright": "*",
"ts-node": "*"
}
}

View File

@@ -11,7 +11,7 @@ import type { PlaywrightTestConfig } from '@playwright/test';
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
testDir: './e2e',
testIgnore: '**/lib/**',
fullyParallel: true,
timeout: process.env.CI ? 50_000 : 30_000,

View File

@@ -38,7 +38,12 @@ export const config = () => {
bundle: true,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron', 'electron-updater', '@toeverything/plugin-infra'],
external: [
'electron',
'electron-updater',
'@toeverything/plugin-infra',
'yjs',
],
define: define,
format: 'cjs',
loader: {

View File

@@ -0,0 +1,42 @@
import { fileURLToPath } from 'node:url';
import { readdir } from 'node:fs/promises';
const outputRoot = fileURLToPath(
new URL(
'../zip-out/AFFiNE-canary.app/Contents/Resources/app',
import.meta.url
)
);
const outputList = [
[
'dist',
[
'main.js',
'helper.js',
'preload.js',
'affine.darwin-arm64.node',
'plugins',
'workers',
],
],
['dist/plugins', ['bookmark-block']],
['dist/plugins/bookmark-block', ['index.mjs']],
['dist/workers', ['plugin.worker.js']],
[
'node_modules/@toeverything/plugin-infra/dist',
['manager.js', 'manager.cjs'],
],
] as [entry: string, expected: string[]][];
await Promise.all(
outputList.map(async ([entry, output]) => {
const files = await readdir(`${outputRoot}/${entry}`);
output.forEach(file => {
if (!files.includes(file)) {
throw new Error(`File ${entry}/${file} not found`);
}
});
})
);
console.log('Output check passed');

View File

@@ -0,0 +1,16 @@
#!/bin/bash
# Set the directory
dir="./out/canary/make/zip/darwin/arm64"
# Get the first file
file=$(ls -1 $dir | head -n 1)
# Check if file exists and is a zip file
if [ -f "$dir/$file" ] && [ ${file: -4} == ".zip" ]
then
# Unzip the file
unzip "$dir/$file" -d "zip-out"
else
echo "No zip file found"
fi

View File

@@ -1,10 +1,11 @@
import path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { removeWithRetry } from '../../../../tests/utils';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
@@ -44,11 +45,7 @@ beforeEach(() => {
afterEach(async () => {
existProcess();
// wait for the db to be closed on Windows
if (process.platform === 'win32') {
await setTimeout(200);
}
await fs.remove(tmpDir);
await removeWithRetry(tmpDir);
vi.useRealTimers();
});

View File

@@ -0,0 +1,69 @@
import path from 'node:path';
import { SqliteConnection } from '@affine/native';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as Y from 'yjs';
import { removeWithRetry } from '../../../../tests/utils';
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration';
const tmpDir = path.join(__dirname, 'tmp');
const testDBFilePath = path.resolve(__dirname, 'old-db.affine');
const appDataPath = path.join(tmpDir, 'app-data');
vi.mock('../../main-rpc', () => ({
mainRPC: {
getPath: async () => appDataPath,
},
}));
afterEach(async () => {
await removeWithRetry(tmpDir);
});
describe('migrateToSubdocAndReplaceDatabase', () => {
it('should migrate and replace the database', async () => {
const copiedDbFilePath = await copyToTemp(testDBFilePath);
await migrateToSubdocAndReplaceDatabase(copiedDbFilePath);
const db = new SqliteConnection(copiedDbFilePath);
await db.connect();
// check if db has two rows, one for root doc and one for subdoc
const rows = await db.getAllUpdates();
expect(rows.length).toBe(2);
const rootUpdate = rows.find(row => row.docId === undefined)!.data;
const subdocUpdate = rows.find(row => row.docId !== undefined)!.data;
expect(rootUpdate).toBeDefined();
expect(subdocUpdate).toBeDefined();
// apply updates
const rootDoc = new Y.Doc();
Y.applyUpdate(rootDoc, rootUpdate);
// check if root doc has one subdoc
expect(rootDoc.subdocs.size).toBe(1);
// populates subdoc
Y.applyUpdate(rootDoc.subdocs.values().next().value, subdocUpdate);
// check if root doc's meta is correct
const meta = rootDoc.getMap('meta').toJSON();
expect(meta.workspaceVersion).toBe(1);
expect(meta.name).toBe('hiw');
expect(meta.pages.length).toBe(1);
const pageMeta = meta.pages[0];
expect(pageMeta.title).toBe('Welcome to AFFiNEd');
// get the subdoc through id
const subDoc = rootDoc
.getMap('spaces')
.get(`space:${pageMeta.id}`) as Y.Doc;
expect(subDoc).toEqual(rootDoc.subdocs.values().next().value);
await db.close();
});
});

Binary file not shown.

View File

@@ -5,6 +5,7 @@ import { v4 } from 'uuid';
import { afterEach, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
import { removeWithRetry } from '../../../../tests/utils';
import { dbSubjects } from '../subjects';
const tmpDir = path.join(__dirname, 'tmp');
@@ -17,7 +18,7 @@ vi.doMock('../../main-rpc', () => ({
}));
afterEach(async () => {
await fs.remove(tmpDir);
await removeWithRetry(tmpDir);
});
let testYDoc: Y.Doc;

View File

@@ -119,6 +119,8 @@ export abstract class BaseSQLiteAdapter {
`[SQLiteAdapter][${this.role}] addUpdateToSQLite`,
'length:',
updates.length,
'docids',
updates.map(u => u.docId),
performance.now() - start,
'ms'
);

View File

@@ -0,0 +1,55 @@
import { resolve } from 'node:path';
import { migrateToSubdoc } from '@affine/env/blocksuite';
import { SqliteConnection } from '@affine/native';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import * as Y from 'yjs';
import { mainRPC } from '../main-rpc';
export const migrateToSubdocAndReplaceDatabase = async (path: string) => {
const db = new SqliteConnection(path);
await db.connect();
const rows = await db.getAllUpdates();
const originalDoc = new Y.Doc();
// 1. apply all updates to the root doc
rows.forEach(row => {
Y.applyUpdate(originalDoc, row.data);
});
// 2. migrate using migrateToSubdoc
const migratedDoc = migrateToSubdoc(originalDoc);
// 3. replace db rows with the migrated doc
await replaceRows(db, migratedDoc, true);
// 4. close db
await db.close();
};
export const copyToTemp = async (path: string) => {
const tmpDirPath = resolve(await mainRPC.getPath('sessionData'), 'tmp');
const tmpFilePath = resolve(tmpDirPath, nanoid());
await fs.ensureDir(tmpDirPath);
await fs.copyFile(path, tmpFilePath);
return tmpFilePath;
};
async function replaceRows(
db: SqliteConnection,
doc: Y.Doc,
isRoot: boolean
): Promise<void> {
const migratedUpdates = Y.encodeStateAsUpdate(doc);
const docId = isRoot ? undefined : doc.guid;
const rows = [{ data: migratedUpdates, docId: docId }];
await db.replaceUpdates(docId, rows);
await Promise.all(
[...doc.subdocs].map(async subdoc => {
await replaceRows(db, subdoc, false);
})
);
}

View File

@@ -115,19 +115,43 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
}
setupListener(docId?: string) {
logger.debug(
'SecondaryWorkspaceSQLiteDB:setupListener',
this.workspaceId,
docId
);
const doc = this.getDoc(docId);
if (!doc) {
const upstreamDoc = this.upstream.getDoc(docId);
if (!doc || !upstreamDoc) {
logger.warn(
'[SecondaryWorkspaceSQLiteDB] setupListener: doc not found',
docId
);
return;
}
const onUpstreamUpdate = (update: Uint8Array, origin: YOrigin) => {
if (origin === 'renderer') {
logger.debug(
'SecondaryWorkspaceSQLiteDB:onUpstreamUpdate',
origin,
this.workspaceId,
docId,
update.length
);
if (origin === 'renderer' || origin === 'self') {
// update to upstream yDoc should be replicated to self yDoc
this.applyUpdate(update, 'upstream', docId);
}
};
const onSelfUpdate = async (update: Uint8Array, origin: YOrigin) => {
logger.debug(
'SecondaryWorkspaceSQLiteDB:onSelfUpdate',
origin,
this.workspaceId,
docId,
update.length
);
// for self update from upstream, we need to push it to external DB
if (origin === 'upstream') {
await this.addUpdateToUpdateQueue({
@@ -147,15 +171,19 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
});
};
doc.subdocs.forEach(subdoc => {
this.setupListener(subdoc.guid);
});
// listen to upstream update
this.upstream.yDoc.on('update', onUpstreamUpdate);
this.yDoc.on('update', onSelfUpdate);
this.yDoc.on('subdocs', onSubdocs);
doc.on('update', onSelfUpdate);
doc.on('subdocs', onSubdocs);
this.unsubscribers.add(() => {
this.upstream.yDoc.off('update', onUpstreamUpdate);
this.yDoc.off('update', onSelfUpdate);
this.yDoc.off('subdocs', onSubdocs);
doc.off('update', onSelfUpdate);
doc.off('subdocs', onSubdocs);
});
}
@@ -188,7 +216,10 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
if (doc) {
Y.applyUpdate(this.yDoc, data, origin);
} else {
logger.warn('applyUpdate: doc not found', docId);
logger.warn(
'[SecondaryWorkspaceSQLiteDB] applyUpdate: doc not found',
docId
);
}
};

View File

@@ -18,7 +18,10 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
update$ = new Subject<void>();
constructor(public override path: string, public workspaceId: string) {
constructor(
public override path: string,
public workspaceId: string
) {
super(path);
}
@@ -49,9 +52,21 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
};
setupListener(docId?: string) {
logger.debug(
'WorkspaceSQLiteDB:setupListener',
this.workspaceId,
docId,
this.getWorkspaceName()
);
const doc = this.getDoc(docId);
if (doc) {
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
logger.debug(
'WorkspaceSQLiteDB:onUpdate',
this.workspaceId,
docId,
update.length
);
const insertRows = [{ data: update, docId }];
if (origin === 'renderer') {
await this.addUpdateToSQLite(insertRows);
@@ -65,7 +80,11 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
logger.debug('external update', this.workspaceId);
}
};
doc.subdocs.forEach(subdoc => {
this.setupListener(subdoc.guid);
});
const onSubdocs = ({ added }: { added: Set<Y.Doc> }) => {
logger.info('onSubdocs', this.workspaceId, docId, added);
added.forEach(subdoc => {
this.setupListener(subdoc.guid);
});
@@ -132,7 +151,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
if (doc) {
Y.applyUpdate(doc, data, origin);
} else {
logger.warn('applyUpdate: doc not found', docId);
logger.warn('[WorkspaceSQLiteDB] applyUpdate: doc not found', docId);
}
};

View File

@@ -1,9 +1,11 @@
import path from 'node:path';
import { ValidationResult } from '@affine/native';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import { ensureSQLiteDB } from '../db/ensure-db';
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../db/migration';
import type { WorkspaceSQLiteDB } from '../db/workspace-db-adapter';
import { logger } from '../logger';
import { mainRPC } from '../main-rpc';
@@ -55,6 +57,7 @@ const ErrorMessages = [
'DB_FILE_ALREADY_LOADED',
'DB_FILE_PATH_INVALID',
'DB_FILE_INVALID',
'DB_FILE_MIGRATION_FAILED',
'FILE_ALREADY_EXISTS',
'UNKNOWN_ERROR',
] as const;
@@ -191,27 +194,42 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
],
message: 'Load Workspace from a AFFiNE file',
}));
const filePath = ret.filePaths?.[0];
if (ret.canceled || !filePath) {
let originalPath = ret.filePaths?.[0];
if (ret.canceled || !originalPath) {
logger.info('loadDBFile canceled');
return { canceled: true };
}
// the imported file should not be in app data dir
if (filePath.startsWith(await getWorkspacesBasePath())) {
if (originalPath.startsWith(await getWorkspacesBasePath())) {
logger.warn('loadDBFile: db file in app data dir');
return { error: 'DB_FILE_PATH_INVALID' };
}
if (await dbFileAlreadyLoaded(filePath)) {
if (await dbFileAlreadyLoaded(originalPath)) {
logger.warn('loadDBFile: db file already loaded');
return { error: 'DB_FILE_ALREADY_LOADED' };
}
const { SqliteConnection } = await import('@affine/native');
if (!(await SqliteConnection.validate(filePath))) {
// TODO: report invalid db file error?
const validationResult = await SqliteConnection.validate(originalPath);
if (validationResult === ValidationResult.MissingDocIdColumn) {
try {
const tmpDBPath = await copyToTemp(originalPath);
await migrateToSubdocAndReplaceDatabase(tmpDBPath);
originalPath = tmpDBPath;
} catch (error) {
logger.warn(`loadDBFile, migration failed: ${originalPath}`, error);
return { error: 'DB_FILE_MIGRATION_FAILED' };
}
}
if (
validationResult !== ValidationResult.MissingDocIdColumn &&
validationResult !== ValidationResult.Valid
) {
return { error: 'DB_FILE_INVALID' }; // invalid db file
}
@@ -220,14 +238,12 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
const internalFilePath = await getWorkspaceDBPath(workspaceId);
await fs.ensureDir(await getWorkspacesBasePath());
await fs.copy(filePath, internalFilePath);
logger.info(`loadDBFile, copy: ${filePath} -> ${internalFilePath}`);
await fs.copy(originalPath, internalFilePath);
logger.info(`loadDBFile, copy: ${originalPath} -> ${internalFilePath}`);
await storeWorkspaceMeta(workspaceId, {
id: workspaceId,
mainDBPath: internalFilePath,
secondaryDBPath: filePath,
});
return { workspaceId };

View File

@@ -1,3 +1,4 @@
import type { RendererToHelper } from '@toeverything/infra/preload/electron';
import { AsyncCall } from 'async-call-rpc';
import { events, handlers } from './exposed';
@@ -30,7 +31,7 @@ function setupRendererConnection(rendererPort: Electron.MessagePortMain) {
});
}
);
const rpc = AsyncCall<PeersAPIs.RendererToHelper>(
const rpc = AsyncCall<RendererToHelper>(
Object.fromEntries(flattenedHandlers),
{
channel: {

View File

@@ -1,12 +1,16 @@
import type {
HelperToMain,
MainToHelper,
} from '@toeverything/infra/preload/electron';
import { AsyncCall } from 'async-call-rpc';
import { getExposedMeta } from './exposed';
const helperToMainServer: PeersAPIs.HelperToMain = {
const helperToMainServer: HelperToMain = {
getMeta: () => getExposedMeta(),
};
export const mainRPC = AsyncCall<PeersAPIs.MainToHelper>(helperToMainServer, {
export const mainRPC = AsyncCall<MainToHelper>(helperToMainServer, {
strict: {
unknownMessage: false,
},

View File

@@ -4,6 +4,8 @@ import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { removeWithRetry } from '../../../../tests/utils';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
@@ -20,7 +22,7 @@ vi.doMock('../../main-rpc', () => ({
}));
afterEach(async () => {
await fs.remove(tmpDir);
await removeWithRetry(tmpDir);
});
describe('list workspaces', () => {

View File

@@ -1,10 +1,10 @@
import assert from 'node:assert';
import path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { removeWithRetry } from '../../../tests/utils';
import type { MainIPCHandlerMap } from '../exposed';
const registeredHandlers = new Map<
@@ -21,7 +21,7 @@ type WithoutFirstParameter<T> = T extends (_: any, ...args: infer P) => infer R
// however this is too hard to be typed correctly
async function dispatch<
T extends keyof MainIPCHandlerMap,
F extends keyof MainIPCHandlerMap[T]
F extends keyof MainIPCHandlerMap[T],
>(
namespace: T,
functionName: F,
@@ -121,11 +121,7 @@ beforeEach(async () => {
afterEach(async () => {
// reset registered handlers
registeredHandlers.get('before-quit')?.forEach(fn => fn());
// wait for the db to be closed on Windows
if (process.platform === 'win32') {
await setTimeout(200);
}
await fs.remove(SESSION_DATA_PATH);
await removeWithRetry(SESSION_DATA_PATH);
});
describe('UI handlers', () => {

View File

@@ -1,7 +1,7 @@
import { app, Menu } from 'electron';
import { revealLogFile } from '../logger';
import { checkForUpdatesAndNotify } from '../updater';
import { checkForUpdates } from '../updater';
import { isMacOS } from '../utils';
import { applicationMenuSubjects } from './subject';
@@ -125,7 +125,7 @@ export function createApplicationMenu() {
{
label: 'Check for Updates',
click: async () => {
await checkForUpdatesAndNotify(true);
await checkForUpdates(true);
},
},
],

View File

@@ -1,9 +1,9 @@
import { clipboard, nativeImage } from 'electron';
import { clipboard, type IpcMainInvokeEvent, nativeImage } from 'electron';
import type { NamespaceHandlers } from '../type';
export const clipboardHandlers = {
copyAsPng: async (_, dataURL: string) => {
copyAsImageFromString: async (_: IpcMainInvokeEvent, dataURL: string) => {
clipboard.writeImage(nativeImage.createFromDataURL(dataURL));
},
} satisfies NamespaceHandlers;

View File

@@ -39,6 +39,13 @@ export async function savePDFFileAs(
await BrowserWindow.getFocusedWindow()
?.webContents.printToPDF({
margins: {
marginType: 'custom',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
pageSize: 'A4',
printBackground: true,
landscape: false,

View File

@@ -1,5 +1,9 @@
import path from 'node:path';
import type {
HelperToMain,
MainToHelper,
} from '@toeverything/infra/preload/electron';
import { type _AsyncVersionOf, AsyncCall } from 'async-call-rpc';
import {
app,
@@ -36,7 +40,7 @@ class HelperProcessManager {
#process: UtilityProcess;
// a rpc server for the main process -> helper process
rpc?: _AsyncVersionOf<PeersAPIs.HelperToMain>;
rpc?: _AsyncVersionOf<HelperToMain>;
static instance = new HelperProcessManager();
@@ -86,13 +90,13 @@ class HelperProcessManager {
]);
const appMethods = pickAndBind(app, ['getPath']);
const mainToHelperServer: PeersAPIs.MainToHelper = {
const mainToHelperServer: MainToHelper = {
...dialogMethods,
...shellMethods,
...appMethods,
};
this.rpc = AsyncCall<PeersAPIs.HelperToMain>(mainToHelperServer, {
this.rpc = AsyncCall<HelperToMain>(mainToHelperServer, {
strict: {
// the channel is shared for other purposes as well so that we do not want to
// restrict to only JSONRPC messages

View File

@@ -1,5 +1,5 @@
import { app } from 'electron';
import type { AppUpdater } from 'electron-updater';
import { autoUpdater } from 'electron-updater';
import { z } from 'zod';
import { logger } from '../logger';
@@ -20,56 +20,55 @@ export const buildType = ReleaseTypeSchema.parse(envBuildType);
const mode = process.env.NODE_ENV;
const isDev = mode === 'development';
let _autoUpdater: AppUpdater | null = null;
export const quitAndInstall = async () => {
_autoUpdater?.quitAndInstall();
autoUpdater.quitAndInstall();
};
let lastCheckTime = 0;
export const checkForUpdatesAndNotify = async (force = true) => {
if (!_autoUpdater) {
return void 0;
}
export const checkForUpdates = async (force = true) => {
// check every 30 minutes (1800 seconds) at most
if (force || lastCheckTime + 1000 * 1800 < Date.now()) {
lastCheckTime = Date.now();
return await _autoUpdater.checkForUpdatesAndNotify();
return await autoUpdater.checkForUpdates();
}
return void 0;
};
export const registerUpdater = async () => {
// so we wrap it in a function
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { autoUpdater } = require('electron-updater');
_autoUpdater = autoUpdater;
// skip auto update in dev mode
if (!_autoUpdater || isDev) {
if (isDev) {
return;
}
// TODO: support auto update on windows and linux
const allowAutoUpdate = isMacOS();
_autoUpdater.autoDownload = false;
_autoUpdater.allowPrerelease = buildType !== 'stable';
_autoUpdater.autoInstallOnAppQuit = false;
_autoUpdater.autoRunAppAfterInstall = true;
_autoUpdater.setFeedURL({
autoUpdater.logger = logger;
autoUpdater.autoDownload = false;
autoUpdater.allowPrerelease = buildType !== 'stable';
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.autoRunAppAfterInstall = true;
const feedUrl: Parameters<typeof autoUpdater.setFeedURL>[0] = {
channel: buildType,
provider: 'github',
repo: buildType !== 'internal' ? 'AFFiNE' : 'AFFiNE-Releases',
owner: 'toeverything',
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
});
};
logger.debug('auto-updater feed config', feedUrl);
autoUpdater.setFeedURL(feedUrl);
// register events for checkForUpdatesAndNotify
_autoUpdater.on('update-available', info => {
autoUpdater.on('checking-for-update', () => {
logger.info('Checking for update');
});
autoUpdater.on('update-available', info => {
logger.info('Update available', info);
if (allowAutoUpdate) {
_autoUpdater?.downloadUpdate().catch(e => {
autoUpdater?.downloadUpdate().catch(e => {
logger.error('Failed to download update', e);
});
logger.info('Update available, downloading...', info);
@@ -79,11 +78,14 @@ export const registerUpdater = async () => {
allowAutoUpdate,
});
});
_autoUpdater.on('download-progress', e => {
autoUpdater.on('update-not-available', info => {
logger.info('Update not available', info);
});
autoUpdater.on('download-progress', e => {
logger.info(`Download progress: ${e.percent}`);
updaterSubjects.downloadProgress.next(e.percent);
});
_autoUpdater.on('update-downloaded', e => {
autoUpdater.on('update-downloaded', e => {
updaterSubjects.updateReady.next({
version: e.version,
allowAutoUpdate,
@@ -92,12 +94,12 @@ export const registerUpdater = async () => {
// updaterSubjects.clientDownloadProgress.next(100);
logger.info('Update downloaded, ready to install');
});
_autoUpdater.on('error', e => {
autoUpdater.on('error', e => {
logger.error('Error while updating client', e);
});
_autoUpdater.forceDevUpdateConfig = isDev;
autoUpdater.forceDevUpdateConfig = isDev;
app.on('activate', async () => {
await checkForUpdatesAndNotify(false);
await checkForUpdates(false);
});
};

View File

@@ -1,7 +1,7 @@
import { app } from 'electron';
import type { NamespaceHandlers } from '../type';
import { checkForUpdatesAndNotify, quitAndInstall } from './electron-updater';
import { checkForUpdates, quitAndInstall } from './electron-updater';
export const updaterHandlers = {
currentVersion: async () => {
@@ -11,7 +11,14 @@ export const updaterHandlers = {
return quitAndInstall();
},
checkForUpdatesAndNotify: async () => {
return checkForUpdatesAndNotify(true);
const res = await checkForUpdates(true);
if (res) {
const { updateInfo } = res;
return {
updateInfo,
};
}
return null;
},
} satisfies NamespaceHandlers;

View File

@@ -1,8 +1,10 @@
import { contextBridge, ipcRenderer } from 'electron';
(async () => {
const { appInfo, getAffineAPIs } = await import('./affine-apis');
const { apis, events } = getAffineAPIs();
const { appInfo, getElectronAPIs } = await import(
'@toeverything/infra/preload/electron'
);
const { apis, events } = getElectronAPIs();
contextBridge.exposeInMainWorld('appInfo', appInfo);
contextBridge.exposeInMainWorld('apis', apis);

View File

@@ -1,35 +0,0 @@
declare namespace PeersAPIs {
import type { app, dialog, shell } from 'electron';
interface ExposedMeta {
handlers: [string, string[]][];
events: [string, string[]][];
}
// render <-> helper
interface RendererToHelper {
postEvent: (channel: string, ...args: any[]) => void;
}
interface HelperToRenderer {
[key: string]: (...args: any[]) => Promise<any>;
}
// helper <-> main
interface HelperToMain {
getMeta: () => ExposedMeta;
}
type MainToHelper = Pick<
typeof dialog & typeof shell & typeof app,
| 'showOpenDialog'
| 'showSaveDialog'
| 'openExternal'
| 'showItemInFolder'
| 'getPath'
>;
// render <-> main
// these are handled via IPC
// TODO: fix type
}

View File

@@ -0,0 +1,26 @@
import { setTimeout } from 'node:timers/promises';
import fs from 'fs-extra';
export async function removeWithRetry(
filePath: string,
maxRetries = 5,
delay = 500
) {
for (let i = 0; i < maxRetries; i++) {
try {
await fs.remove(filePath);
console.log(`File ${filePath} successfully deleted.`);
return true;
} catch (err: any) {
if (err.code === 'EBUSY' || err.code === 'EPERM') {
console.log(`File ${filePath} is busy or locked, retrying...`);
await setTimeout(delay);
} else {
console.error(`Failed to delete file ${filePath}:`, err);
}
}
}
// Add a return statement here to ensure that a value is always returned
return false;
}

View File

@@ -14,7 +14,7 @@
"noImplicitOverride": true
},
"include": ["./src"],
"exclude": ["node_modules", "out", "dist"],
"exclude": ["node_modules", "out", "dist", "**/__tests__/**/*"],
"references": [
{
"path": "../../packages/plugin-infra"
@@ -25,13 +25,16 @@
{
"path": "../../packages/infra"
},
{
"path": "../../packages/env"
},
// Tests
{
"path": "./tsconfig.node.json"
},
{
"path": "./tests/tsconfig.json"
"path": "./e2e/tsconfig.json"
},
{ "path": "../../tests/kit" }
],

View File

@@ -7,7 +7,8 @@
"resolveJsonModule": true,
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"noEmit": false
"noEmit": false,
"outDir": "./lib/scripts"
},
"include": ["./scripts", "esbuild.main.config.ts", "esbuild.plugin.config.ts"]
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true
},
"include": ["**/__tests__/**/*", "./tests"],
"references": [
{
"path": "./tsconfig.json"
}
]
}

View File

@@ -1,2 +1,2 @@
SECRET_KEY="secret"
DATABASE_URL="postgresql://affine@localhost:5432/affine"
NEXTAUTH_URL="http://localhost:8080"

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.7.0-canary.30",
"version": "0.7.0-canary.41",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -27,6 +27,7 @@
"@node-rs/crc32": "^1.7.0",
"@node-rs/jsonwebtoken": "^0.2.0",
"@prisma/client": "^4.16.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"graphql": "^16.7.1",
@@ -34,18 +35,22 @@
"graphql-upload": "^16.0.2",
"lodash-es": "^4.17.21",
"next-auth": "^4.22.1",
"nodemailer": "^6.9.3",
"parse-duration": "^1.1.0",
"prisma": "^4.16.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"semver": "^7.5.4"
},
"devDependencies": {
"@affine/storage": "workspace:*",
"@napi-rs/image": "^1.6.1",
"@nestjs/testing": "^10.0.4",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.19",
"@types/nodemailer": "^6.4.8",
"@types/supertest": "^2.0.12",
"c8": "^8.0.0",
"nodemon": "^2.0.22",

View File

@@ -233,5 +233,11 @@ export interface AFFiNEConfig {
}
>
>;
email: {
server: string;
port: number;
sender: string;
password: string;
};
};
}

View File

@@ -65,6 +65,10 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
OAUTH_EMAIL_SENDER: 'auth.email.sender',
OAUTH_EMAIL_SERVER: 'auth.email.server',
OAUTH_EMAIL_PORT: 'auth.email.port',
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
} satisfies AFFiNEConfig['ENV_MAP'],
env: process.env.NODE_ENV ?? 'development',
get prod() {
@@ -102,7 +106,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
},
introspection: true,
playground: true,
debug: true,
},
auth: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -114,8 +117,16 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
publicKey: jwtKeyPair.publicKey,
enableSignup: true,
enableOauth: false,
nextAuthSecret: '',
get nextAuthSecret() {
return this.privateKey;
},
oauthProviders: {},
email: {
server: 'smtp.gmail.com',
port: 465,
sender: '',
password: '',
},
},
objectStorage: {
r2: {
@@ -129,7 +140,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
path: join(homedir(), '.affine-storage'),
},
},
} as const;
} satisfies AFFiNEConfig;
applyEnvToConfig(defaultConfig);

View File

@@ -1,6 +1,7 @@
/// <reference types="./global.d.ts" />
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import cookieParser from 'cookie-parser';
import { static as staticMiddleware } from 'express';
// @ts-expect-error graphql-upload is not typed
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
@@ -27,6 +28,8 @@ app.use(
})
);
app.use(cookieParser());
const config = app.get(Config);
const host = config.host ?? 'localhost';

View File

@@ -41,7 +41,10 @@ export const CurrentUser = createParamDecorator(
@Injectable()
class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private prisma: PrismaService) {}
constructor(
private auth: AuthService,
private prisma: PrismaService
) {}
async canActivate(context: ExecutionContext) {
const { req } = getRequestResponseFromContext(context);

View File

@@ -2,11 +2,10 @@ import { randomUUID } from 'node:crypto';
import { PrismaAdapter } from '@auth/prisma-adapter';
import {
All,
BadRequestException,
Controller,
Get,
Next,
Post,
Query,
Req,
Res,
@@ -15,6 +14,7 @@ import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
import type { NextFunction, Request, Response } from 'express';
import type { AuthAction, AuthOptions } from 'next-auth';
import { AuthHandler } from 'next-auth/core';
import Email from 'next-auth/providers/email';
import Github from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
@@ -28,16 +28,44 @@ const BASE_URL = '/api/auth/';
export class NextAuthController {
private readonly nextAuthOptions: AuthOptions;
constructor(readonly config: Config, readonly prisma: PrismaService) {
constructor(
readonly config: Config,
readonly prisma: PrismaService
) {
const prismaAdapter = PrismaAdapter(prisma);
// createUser exists in the adapter
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const createUser = prismaAdapter.createUser!.bind(prismaAdapter);
prismaAdapter.createUser = async data => {
if (data.email && !data.name) {
data.name = data.email.split('@')[0];
}
return createUser(data);
};
this.nextAuthOptions = {
providers: [],
providers: [
// @ts-expect-error esm interop issue
Email.default({
server: {
host: config.auth.email.server,
port: config.auth.email.port,
auth: {
user: config.auth.email.sender,
pass: config.auth.email.password,
},
},
from: `AFFiNE <no-reply@toeverything.info>`,
}),
],
// @ts-expect-error Third part library type mismatch
adapter: PrismaAdapter(prisma),
adapter: prismaAdapter,
debug: !config.prod,
};
if (config.auth.oauthProviders.github) {
this.nextAuthOptions.providers.push(
Github({
// @ts-expect-error esm interop issue
Github.default({
clientId: config.auth.oauthProviders.github.clientId,
clientSecret: config.auth.oauthProviders.github.clientSecret,
})
@@ -46,12 +74,14 @@ export class NextAuthController {
if (config.auth.oauthProviders.google) {
this.nextAuthOptions.providers.push(
Google({
// @ts-expect-error esm interop issue
Google.default({
clientId: config.auth.oauthProviders.google.clientId,
clientSecret: config.auth.oauthProviders.google.clientSecret,
})
);
}
this.nextAuthOptions.jwt = {
encode: async ({ token, maxAge }) => {
if (!token?.email) {
@@ -108,8 +138,7 @@ export class NextAuthController {
this.nextAuthOptions.secret ??= config.auth.nextAuthSecret;
}
@Get()
@Post()
@All('*')
async auth(
@Req() req: Request,
@Res() res: Response,

View File

@@ -19,7 +19,10 @@ export const getUtcTimestamp = () => Math.floor(new Date().getTime() / 1000);
@Injectable()
export class AuthService {
constructor(private config: Config, private prisma: PrismaService) {}
constructor(
private config: Config,
private prisma: PrismaService
) {}
sign(user: UserClaim) {
const now = getUtcTimestamp();

View File

@@ -101,7 +101,6 @@ type Query {
type Mutation {
register(name: String!, email: String!, password: String!): UserType!
signIn(email: String!, password: String!): UserType!
signUp(email: String!, password: String!, name: String!): UserType!
"""
Create a new workspace

View File

@@ -28,7 +28,7 @@ export type LeafPaths<
T,
Path extends string = '',
MaxDepth extends string = '...',
Depth extends string = ''
Depth extends string = '',
> = Depth extends MaxDepth
? never
: T extends Record<string | number, any>

View File

@@ -30,15 +30,15 @@
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/blocks": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/editor": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/global": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/block-std": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/global": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/icons": "^2.1.24",
"@blocksuite/lit": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/store": "0.0.0-20230702013906-50f93499-nightly",
"react": "18.3.0-canary-8ec962d82-20230623",
"react-dom": "18.3.0-canary-8ec962d82-20230623"
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
"react": "18.3.0-canary-1fdacbefd-20230630",
"react-dom": "18.3.0-canary-1fdacbefd-20230630"
},
"peerDependencies": {
"@blocksuite/blocks": "*",
@@ -48,5 +48,5 @@
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.7.0-canary.30"
"version": "0.7.0-canary.41"
}

View File

@@ -1,24 +0,0 @@
import { AffineLoading } from '@affine/component/affine-loading';
import type { StoryFn } from '@storybook/react';
export default {
title: 'AFFiNE/Loading',
component: AffineLoading,
};
export const Default: StoryFn = ({ width, loop, autoplay, autoReverse }) => (
<div
style={{
width: width,
height: width,
}}
>
<AffineLoading loop={loop} autoplay={autoplay} autoReverse={autoReverse} />
</div>
);
Default.args = {
width: 100,
loop: true,
autoplay: true,
autoReverse: true,
};

View File

@@ -1,6 +1,8 @@
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
import { BlockHubWrapper } from '@affine/component/block-hub';
import type { EditorProps } from '@affine/component/block-suite-editor';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
@@ -54,13 +56,13 @@ const Template: StoryFn<EditorProps> = (props: Partial<EditorProps>) => {
}}
>
<BlockSuiteEditor onInit={initPage} page={page} mode="page" {...props} />
<div
<BlockHubWrapper
style={{
position: 'absolute',
right: 12,
bottom: 12,
}}
id="toolWrapper"
blockHubAtom={rootBlockHubAtom}
/>
</div>
);

View File

@@ -2,27 +2,27 @@ import { toast } from '@affine/component';
import { BlockCard } from '@affine/component/card/block-card';
import { WorkspaceCard } from '@affine/component/card/workspace-card';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { Workspace } from '@blocksuite/store';
export default {
title: 'AFFiNE/Card',
component: WorkspaceCard,
};
const blockSuiteWorkspace = new Workspace({
id: 'blocksuite-local',
});
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
'blocksuite-local',
WorkspaceFlavour.LOCAL
);
blockSuiteWorkspace.meta.setName('Hello World');
export const AffineWorkspaceCard = () => {
return (
<WorkspaceCard
workspace={{
meta={{
id: 'blocksuite-local',
flavour: WorkspaceFlavour.LOCAL,
id: 'local',
blockSuiteWorkspace,
}}
onClick={() => {}}
onSettingClick={() => {}}

View File

@@ -1,35 +0,0 @@
import { Button } from '@affine/component';
import type { ContactModalProps } from '@affine/component/contact-modal';
import { ContactModal } from '@affine/component/contact-modal';
import type { StoryFn } from '@storybook/react';
import { useState } from 'react';
export default {
title: 'AFFiNE/ContactModal',
component: ContactModal,
};
export const Basic: StoryFn<ContactModalProps> = args => {
const [open, setOpen] = useState(false);
return (
<>
<Button
onClick={() => {
setOpen(true);
}}
>
Open
</Button>
<ContactModal
{...args}
open={open}
onClose={() => {
setOpen(false);
}}
/>
</>
);
};
Basic.args = {
logoSrc: '/imgs/affine-text-logo.png',
};

View File

@@ -1,7 +1,9 @@
import { BlockHubWrapper } from '@affine/component/block-hub';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import { ImagePreviewModal } from '@affine/component/image-preview-modal';
import { initEmptyPage } from '@affine/env/blocksuite';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { Meta } from '@storybook/react';
@@ -54,13 +56,13 @@ export const Default = () => {
>
<BlockSuiteEditor mode="page" page={page} onInit={initEmptyPage} />
</div>
<div
<BlockHubWrapper
style={{
position: 'absolute',
right: 12,
bottom: 12,
}}
id="toolWrapper"
blockHubAtom={rootBlockHubAtom}
/>
</>
);

View File

@@ -1,6 +1,5 @@
import { Empty } from '@affine/component';
import { toast } from '@affine/component';
import { AffineLoading } from '@affine/component/affine-loading';
import type { OperationCellProps } from '@affine/component/page-list';
import { PageListTrashView } from '@affine/component/page-list';
import { PageList } from '@affine/component/page-list';
@@ -13,7 +12,7 @@ import { userEvent } from '@storybook/testing-library';
export default {
title: 'AFFiNE/PageList',
component: AffineLoading,
component: PageList,
};
export const AffineOperationCell: StoryFn<OperationCellProps> = ({
@@ -71,6 +70,7 @@ AffineAllPageList.args = {
icon: <PageIcon />,
isPublicPage: true,
title: 'Today Page',
tags: [],
preview: 'this is page preview',
createDate: new Date(),
updatedDate: new Date(),
@@ -87,6 +87,7 @@ AffineAllPageList.args = {
isPublicPage: true,
title:
'1 Example Public Page with long title that will be truncated because it is too too long',
tags: [],
preview:
'this is page preview and it is very long and will be truncated because it is too long and it is very long and will be truncated because it is too long',
createDate: new Date('2021-01-01'),
@@ -103,6 +104,7 @@ AffineAllPageList.args = {
isPublicPage: false,
icon: <PageIcon />,
title: '2 Favorited Page 2021',
tags: [],
createDate: new Date('2021-01-02'),
updatedDate: new Date('2021-01-01'),
bookmarkPage: () => toast('Bookmark page'),
@@ -117,6 +119,7 @@ AffineAllPageList.args = {
isPublicPage: false,
icon: <PageIcon />,
title: 'page created in 2023-04-01',
tags: [],
createDate: new Date('2023-04-01'),
updatedDate: new Date('2023-04-01'),
bookmarkPage: () => toast('Bookmark page'),

View File

@@ -5,14 +5,10 @@ import {
} from '@affine/component/share-menu';
import { ShareMenu } from '@affine/component/share-menu';
import type {
AffineLegacyCloudWorkspace,
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
PermissionType,
WorkspaceType,
} from '@affine/env/workspace/legacy-cloud';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { Page } from '@blocksuite/store';
import { expect } from '@storybook/jest';
@@ -59,13 +55,10 @@ const localWorkspace: LocalWorkspace = {
blockSuiteWorkspace,
};
const affineWorkspace: AffineLegacyCloudWorkspace = {
const affineWorkspace: AffineCloudWorkspace = {
id: 'test-workspace',
flavour: WorkspaceFlavour.AFFINE,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
blockSuiteWorkspace,
public: false,
type: WorkspaceType.Normal,
permission: PermissionType.Owner,
};
async function unimplemented() {

View File

@@ -1,6 +1,5 @@
import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Workspace } from '@blocksuite/store';
import type { Meta, StoryFn } from '@storybook/react';
@@ -25,16 +24,7 @@ const basicBlockSuiteWorkspace = new Workspace({
basicBlockSuiteWorkspace.meta.setName('Hello World');
export const Basic: StoryFn<WorkspaceAvatarProps> = props => {
return (
<WorkspaceAvatar
{...props}
workspace={{
flavour: WorkspaceFlavour.LOCAL,
id: 'local',
blockSuiteWorkspace: basicBlockSuiteWorkspace,
}}
/>
);
return <WorkspaceAvatar {...props} workspace={basicBlockSuiteWorkspace} />;
};
Basic.args = {
@@ -60,16 +50,7 @@ fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
});
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
return (
<WorkspaceAvatar
{...props}
workspace={{
flavour: WorkspaceFlavour.LOCAL,
id: 'local',
blockSuiteWorkspace: avatarBlockSuiteWorkspace,
}}
/>
);
return <WorkspaceAvatar {...props} workspace={avatarBlockSuiteWorkspace} />;
};
BlobExample.args = {

View File

@@ -13,6 +13,9 @@ import { blockSuiteFeatureFlags, buildFlags } from './preset.config.mjs';
import { getCommitHash, getGitVersion } from './scripts/git-info.mjs';
const require = createRequire(import.meta.url);
const packageJson = require('./package.json');
const appVersion = packageJson.version;
const editorVersion = packageJson.dependencies['@blocksuite/editor'];
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin');
const withVanillaExtract = createVanillaExtractPlugin();
@@ -45,17 +48,12 @@ if (process.env.COVERAGE === 'true') {
}
const profileTarget = {
ac: '100.85.73.88:12001',
dev: '100.84.105.99:11001',
test: '100.84.105.99:11001',
stage: '',
prod: 'https://app.affine.pro',
local: '127.0.0.1:3000',
};
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
output: process.env.NODE_ENV === 'development' ? 'standalone' : 'export',
typescript: {
// We use `yarn typecheck` on top level to check types
ignoreBuildErrors: true,
@@ -107,11 +105,11 @@ const nextConfig = {
publicRuntimeConfig: {
PROJECT_NAME: process.env.npm_package_name ?? 'AFFiNE',
BUILD_DATE: new Date().toISOString(),
appVersion,
editorVersion,
gitVersion: getGitVersion(),
hash: getCommitHash(),
serverAPI:
profileTarget[process.env.API_SERVER_PROFILE || 'dev'] ??
profileTarget.dev,
serverAPI: profileTarget.local,
editorFlags: blockSuiteFeatureFlags,
...buildFlags,
},
@@ -157,6 +155,15 @@ const nextConfig = {
],
};
if (process.env.NODE_ENV === 'development') {
nextConfig.rewrites = async () => [
{
source: '/api/auth/:path*/:path2*',
destination: 'http://localhost:3010/api/auth/:path*/:path2*',
},
];
}
const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/';
const withDebugLocal = debugLocal(
{

View File

@@ -1,11 +1,12 @@
{
"name": "@affine/web",
"private": true,
"version": "0.7.0-canary.30",
"version": "0.7.0-canary.41",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "ts-node-esm server.mts"
"build": "next build && next export",
"start": "NODE_ENV=development next start",
"static-server": "ts-node-esm server.mts"
},
"dependencies": {
"@affine-test/fixtures": "workspace:*",
@@ -18,13 +19,13 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/blocks": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/editor": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/global": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/block-std": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/global": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/icons": "^2.1.24",
"@blocksuite/lit": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/store": "0.0.0-20230702013906-50f93499-nightly",
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
@@ -41,13 +42,13 @@
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"graphql": "^16.7.1",
"jotai": "^2.2.1",
"jotai": "^2.2.2",
"jotai-devtools": "^0.6.0",
"lit": "^2.7.5",
"lottie-web": "^5.12.2",
"next-themes": "^0.2.1",
"react": "18.3.0-canary-8ec962d82-20230623",
"react-dom": "18.3.0-canary-8ec962d82-20230623",
"react": "18.3.0-canary-1fdacbefd-20230630",
"react-dom": "18.3.0-canary-1fdacbefd-20230630",
"react-is": "^18.2.0",
"react-resizable-panels": "^0.0.53",
"rxjs": "^7.8.1",

View File

@@ -19,35 +19,37 @@ export const blockSuiteFeatureFlags = {
*/
const buildPreset = {
stable: {
enableAllPageSaving: false,
enablePlugin: false,
enableTestProperties: false,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
enableLegacyCloud: false,
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
enablePreloading: true,
enableNewSettingModal: false,
enableNewSettingModal: true,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: false,
enableSQLiteProvider: true,
enableMoveDatabase: false,
enableNotificationCenter: false,
enableCloud: false,
},
beta: {},
internal: {},
// canary will be aggressive and enable all features
canary: {
enableAllPageSaving: true,
enablePlugin: true,
enableTestProperties: true,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
enableLegacyCloud: false,
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
enablePreloading: true,
enableNewSettingModal: true,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: false,
enableSQLiteProvider: true,
enableMoveDatabase: false,
enableNotificationCenter: true,
enableCloud: false,
},
};
@@ -67,15 +69,9 @@ const environmentPreset = {
enablePlugin: process.env.ENABLE_PLUGIN
? process.env.ENABLE_PLUGIN === 'true'
: currentBuildPreset.enablePlugin,
enableAllPageSaving: process.env.ENABLE_ALL_PAGE_SAVING
? process.env.ENABLE_ALL_PAGE_FILTER === 'true'
: currentBuildPreset.enableAllPageSaving,
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
? process.env.ENABLE_TEST_PROPERTIES === 'true'
: currentBuildPreset.enableTestProperties,
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: currentBuildPreset.enableLegacyCloud,
enableBroadcastChannelProvider: process.env.ENABLE_BC_PROVIDER
? process.env.ENABLE_BC_PROVIDER !== 'false'
: currentBuildPreset.enableBroadcastChannelProvider,
@@ -95,6 +91,12 @@ const environmentPreset = {
enableNotificationCenter: process.env.ENABLE_NOTIFICATION_CENTER
? process.env.ENABLE_NOTIFICATION_CENTER === 'true'
: currentBuildPreset.enableNotificationCenter,
enableCloud: process.env.ENABLE_CLOUD
? process.env.ENABLE_CLOUD === 'true'
: currentBuildPreset.enableCloud,
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
? process.env.ENABLE_MOVE_DATABASE === 'true'
: currentBuildPreset.enableMoveDatabase,
};
/**

View File

@@ -8,6 +8,16 @@
"build": {
"executor": "nx:run-script",
"dependsOn": ["^build"],
"inputs": [
"{projectRoot}/**/*",
"{workspaceRoot}/packages/component/src/**/*",
"{workspaceRoot}/packages/debug/src/**/*",
"{workspaceRoot}/packages/debug/graphql/**/*",
"{workspaceRoot}/packages/hooks/src/**/*",
"{workspaceRoot}/packages/jotai/src/**/*",
"{workspaceRoot}/packages/templates/src/**/*",
"{workspaceRoot}/packages/workspace/src/**/*"
],
"options": {
"script": "build"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

View File

@@ -1,92 +0,0 @@
import { Unreachable } from '@affine/env/constant';
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { affineApis } from '@affine/workspace/affine/shared';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { assertExists } from '@blocksuite/store';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { workspacesAtom } from '../../atoms';
type Query = (typeof QueryKey)[keyof typeof QueryKey];
export const fetcher = async (
query:
| Query
| [Query, string, boolean]
| [Query, string]
| [Query, string, string]
) => {
if (Array.isArray(query)) {
if (query[0] === QueryKey.downloadWorkspace) {
if (typeof query[2] !== 'boolean') {
throw new Unreachable();
}
return affineApis.downloadWorkspace(query[1], query[2]);
} else if (query[0] === QueryKey.getMembers) {
return affineApis.getWorkspaceMembers({
id: query[1],
});
} else if (query[0] === QueryKey.getUserByEmail) {
if (typeof query[2] !== 'string') {
throw new Unreachable();
}
return affineApis.getUserByEmail({
workspace_id: query[1],
email: query[2],
});
} else if (query[0] === QueryKey.getImage) {
const workspaceId = query[1];
const key = query[2];
if (typeof key !== 'string') {
throw new TypeError('key must be a string');
}
const workspaces = await rootStore.get(workspacesAtom);
const workspace = workspaces.find(({ id }) => id === workspaceId);
assertExists(workspace);
const storage = await workspace.blockSuiteWorkspace.blobs;
if (!storage) {
return null;
}
return storage.get(key);
} else if (query[0] === QueryKey.acceptInvite) {
const invitingCode = query[1];
if (typeof invitingCode !== 'string') {
throw new TypeError('invitingCode must be a string');
}
return affineApis.acceptInviting({
invitingCode,
});
}
} else {
if (query === QueryKey.getWorkspaces) {
return affineApis.getWorkspaces().then(workspaces => {
return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
}
);
const remWorkspace: AffineLegacyCloudWorkspace = {
...workspace,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
};
return remWorkspace;
});
});
}
return (affineApis as any)[query]();
}
};
export const QueryKey = {
acceptInvite: 'acceptInvite',
getImage: 'getImage',
getWorkspaces: 'getWorkspaces',
downloadWorkspace: 'downloadWorkspace',
getMembers: 'getMembers',
getUserByEmail: 'getUserByEmail',
} as const;

View File

@@ -1,380 +0,0 @@
/**
* This file has deprecated because we do not maintain legacy affine cloud,
* please use new affine cloud instead.
*/
import { initEmptyPage } from '@affine/env/blocksuite';
import { AFFINE_STORAGE_KEY, PageNotFoundError } from '@affine/env/constant';
import type {
AffineDownloadProvider,
AffineLegacyCloudWorkspace,
LocalIndexedDBDownloadProvider,
} from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
getLoginStorage,
isExpired,
parseIdToken,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { affineApis, affineAuth } from '@affine/workspace/affine/shared';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { createAffineDownloadProvider } from '@affine/workspace/providers';
import {
cleanupWorkspace,
createEmptyBlockSuiteWorkspace,
} from '@affine/workspace/utils';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { createJSONStorage } from 'jotai/utils';
import type { PropsWithChildren, ReactElement } from 'react';
import { Suspense, useEffect } from 'react';
import { mutate } from 'swr';
import { z } from 'zod';
import { PageLoading } from '../../components/pure/loading';
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
import { BlockSuiteWorkspace } from '../../shared';
import { toast } from '../../utils';
import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
const schema = z.object({
id: z.string(),
type: z.number(),
public: z.boolean(),
permission: z.number(),
});
const getPersistenceAllWorkspace = () => {
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
const allWorkspaces: AffineLegacyCloudWorkspace[] = [];
if (
Array.isArray(items) &&
items.every(item => schema.safeParse(item).success)
) {
allWorkspaces.push(
...items.map((item: z.infer<typeof schema>) => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
item.id,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
}
);
const affineWorkspace: AffineLegacyCloudWorkspace = {
...item,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
};
return affineWorkspace;
})
);
}
return allWorkspaces;
};
function AuthContext({ children }: PropsWithChildren): ReactElement {
const login = useAffineRefreshAuthToken();
useEffect(() => {
if (!login) {
console.warn('No login, redirecting to local workspace page...');
}
}, [login]);
if (!login) {
return <PageLoading />;
}
return <>{children}</>;
}
export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
releaseType: ReleaseType.STABLE,
flavour: WorkspaceFlavour.AFFINE,
loadPriority: LoadPriority.HIGH,
Events: {
'workspace:access': async () => {
if (!runtimeConfig.enableLegacyCloud) {
console.warn('Legacy cloud is disabled');
return;
}
const response = await affineAuth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
const user = parseIdToken(response.token);
rootStore.set(currentAffineUserAtom, user);
} else {
toast('Login failed');
}
},
'workspace:revoke': async () => {
if (!runtimeConfig.enableLegacyCloud) {
console.warn('Legacy cloud is disabled');
return;
}
await rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
)
);
storage.removeItem(AFFINE_STORAGE_KEY);
clearLoginStorage();
rootStore.set(currentAffineUserAtom, null);
},
},
CRUD: {
create: async blockSuiteWorkspace => {
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
blockSuiteWorkspace.doc
);
const { id } = await affineApis.createWorkspace(binary);
// fixme: syncing images
const newWorkspaceId = id;
await new Promise(resolve => setTimeout(resolve, 1000));
const blobManager = blockSuiteWorkspace.blobs;
for (const id of await blobManager.list()) {
const blob = await blobManager.get(id);
if (blob) {
await affineApis.uploadBlob(
newWorkspaceId,
await blob.arrayBuffer(),
blob.type
);
}
}
{
const bs = createEmptyBlockSuiteWorkspace(id, WorkspaceFlavour.AFFINE, {
workspaceApis: affineApis,
});
// fixme:
// force to download workspace binary
// to make sure the workspace is synced
const provider = createAffineDownloadProvider(bs.id, bs.doc, {
awareness: bs.awarenessStore.awareness,
}) as AffineDownloadProvider;
const indexedDBProvider = createIndexedDBDownloadProvider(
bs.id,
bs.doc,
{
awareness: bs.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
indexedDBProvider.sync();
await indexedDBProvider.whenReady;
provider.disconnect();
}
await mutate(matcher => matcher === QueryKey.getWorkspaces);
// refresh the local storage
await AffineAdapter.CRUD.list();
return id;
},
delete: async workspace => {
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
if (
Array.isArray(items) &&
items.every(item => schema.safeParse(item).success)
) {
storage.setItem(
AFFINE_STORAGE_KEY,
items.filter(item => item.id !== workspace.id)
);
}
await affineApis.deleteWorkspace({
id: workspace.id,
});
await mutate(matcher => matcher === QueryKey.getWorkspaces);
},
get: async workspaceId => {
// fixme(himself65): rewrite the auth logic
try {
const loginStorage = getLoginStorage();
if (
loginStorage == null ||
isExpired(parseIdToken(loginStorage.token))
) {
rootStore.set(currentAffineUserAtom, null);
storage.removeItem(AFFINE_STORAGE_KEY);
cleanupWorkspace(WorkspaceFlavour.AFFINE);
return null;
}
const workspaces: AffineLegacyCloudWorkspace[] =
await AffineAdapter.CRUD.list();
return (
workspaces.find(workspace => workspace.id === workspaceId) ?? null
);
} catch (e) {
const workspaces = getPersistenceAllWorkspace();
return (
workspaces.find(workspace => workspace.id === workspaceId) ?? null
);
}
},
list: async () => {
const allWorkspaces = getPersistenceAllWorkspace();
const loginStorage = getLoginStorage();
// fixme(himself65): rewrite the auth logic
try {
if (
loginStorage == null ||
isExpired(parseIdToken(loginStorage.token))
) {
rootStore.set(currentAffineUserAtom, null);
storage.removeItem(AFFINE_STORAGE_KEY);
return [];
}
} catch (e) {
storage.removeItem(AFFINE_STORAGE_KEY);
return [];
}
try {
const workspaces = await affineApis.getWorkspaces().then(workspaces => {
return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
}
);
const dump = workspaces.map(workspace => {
return {
id: workspace.id,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
} satisfies z.infer<typeof schema>;
});
const old = storage.getItem(AFFINE_STORAGE_KEY, []);
if (
Array.isArray(old) &&
old.every(item => schema.safeParse(item).success)
) {
const data = [...dump];
old.forEach((item: z.infer<typeof schema>) => {
const has = dump.find(dump => dump.id === item.id);
if (!has) {
data.push(item);
}
});
storage.setItem(AFFINE_STORAGE_KEY, [...data]);
}
const affineWorkspace: AffineLegacyCloudWorkspace = {
...workspace,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
};
return affineWorkspace;
});
});
workspaces.forEach(workspace => {
const idx = allWorkspaces.findIndex(({ id }) => id === workspace.id);
if (idx !== -1) {
allWorkspaces.splice(idx, 1, workspace);
} else {
allWorkspaces.push(workspace);
}
});
// only save data when login in
const dump = allWorkspaces.map(workspace => {
return {
id: workspace.id,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
} satisfies z.infer<typeof schema>;
});
storage.setItem(AFFINE_STORAGE_KEY, [...dump]);
} catch (e) {
console.error('fetch affine workspaces failed', e);
}
return [...allWorkspaces];
},
},
UI: {
Provider: ({ children }) => {
return (
<Suspense fallback={<PageLoading />}>
<AuthContext>{children}</AuthContext>
</Suspense>
);
},
Header: WorkspaceHeader,
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
}
return (
<>
<PageDetailEditor
pageId={currentPageId}
workspace={currentWorkspace}
onInit={initEmptyPage}
onLoad={onLoadEditor}
/>
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
collection={collection}
listType="all"
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
},
SettingsDetail: ({
currentWorkspace,
onChangeTab,
currentTab,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<WorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onChangeTab={onChangeTab}
currentTab={currentTab}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
NewSettingsDetail: ({
currentWorkspace,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
},
};

View File

@@ -19,13 +19,13 @@ import {
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { nanoid } from '@blocksuite/store';
import { useStaticBlockSuiteWorkspace } from '@toeverything/hooks/use-block-suite-workspace';
import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
const logger = new DebugLogger('use-create-first-workspace');
@@ -76,13 +76,11 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
Provider: ({ children }) => {
return <>{children}</>;
},
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
const page = workspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
throw new PageNotFoundError(workspace, currentPageId);
}
return (
<>
@@ -90,7 +88,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
pageId={currentPageId}
onInit={initEmptyPage}
onLoad={onLoadEditor}
workspace={currentWorkspace}
workspace={workspace}
/>
</>
);
@@ -105,32 +103,15 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
/>
);
},
SettingsDetail: ({
currentWorkspace,
onChangeTab,
currentTab,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<WorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onChangeTab={onChangeTab}
currentTab={currentTab}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
NewSettingsDetail: ({
currentWorkspace,
currentWorkspaceId,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
workspace={currentWorkspace}
workspaceId={currentWorkspaceId}
onTransferWorkspace={onTransformWorkspace}
/>
);

View File

@@ -1,12 +1,5 @@
import { lazy } from 'react';
// export { WorkspaceSettingDetail as NewWorkspaceSettingDetail } from '../components/affine/new-workspace-setting-detail';
export const WorkspaceSettingDetail = lazy(() =>
import('../components/affine/workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({
default: WorkspaceSettingDetail,
})
)
);
export const NewWorkspaceSettingDetail = lazy(() =>
import('../components/affine/new-workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({
@@ -14,6 +7,7 @@ export const NewWorkspaceSettingDetail = lazy(() =>
})
)
);
export const BlockSuitePageList = lazy(() =>
import('../components/blocksuite/block-suite-page-list').then(
({ BlockSuitePageList }) => ({
@@ -21,6 +15,7 @@ export const BlockSuitePageList = lazy(() =>
})
)
);
export const PageDetailEditor = lazy(() =>
import('../components/page-detail-editor').then(({ PageDetailEditor }) => ({
default: PageDetailEditor,

View File

@@ -10,7 +10,6 @@ import {
WorkspaceFlavour,
} from '@affine/env/workspace';
import { AffineAdapter } from './affine';
import { LocalAdapter } from './local';
const unimplemented = () => {
@@ -22,7 +21,6 @@ const bypassList = async () => {
};
export const WorkspaceAdapters = {
[WorkspaceFlavour.AFFINE]: AffineAdapter,
[WorkspaceFlavour.LOCAL]: LocalAdapter,
[WorkspaceFlavour.AFFINE_CLOUD]: {
releaseType: ReleaseType.UNRELEASED,
@@ -42,7 +40,6 @@ export const WorkspaceAdapters = {
Header: unimplemented,
PageDetail: unimplemented,
PageList: unimplemented,
SettingsDetail: unimplemented,
NewSettingsDetail: unimplemented,
},
},
@@ -64,7 +61,6 @@ export const WorkspaceAdapters = {
Header: unimplemented,
PageDetail: unimplemented,
PageList: unimplemented,
SettingsDetail: unimplemented,
NewSettingsDetail: unimplemented,
},
},

View File

@@ -3,34 +3,14 @@
*/
import 'fake-indexeddb/auto';
import { initEmptyPage } from '@affine/env/blocksuite';
import type {
LocalIndexedDBBackgroundProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
import {
_cleanupBlockSuiteWorkspaceCache,
createEmptyBlockSuiteWorkspace,
} from '@affine/workspace/utils';
import type { ParagraphBlockModel } from '@blocksuite/blocks/models';
import type { Page } from '@blocksuite/store';
import { createStore } from 'jotai';
import { describe, expect, test } from 'vitest';
import { WorkspaceAdapters } from '../../adapters/workspace';
import {
pageSettingFamily,
pageSettingsAtom,
recentPageSettingsAtom,
} from '../index';
import { rootCurrentWorkspaceAtom } from '../root';
describe('page mode atom', () => {
test('basic', () => {
@@ -63,66 +43,3 @@ describe('page mode atom', () => {
]);
});
});
describe('currentWorkspace atom', () => {
test('should be defined', async () => {
const store = createStore();
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
let id: string;
{
const workspace = createEmptyBlockSuiteWorkspace(
'test',
WorkspaceFlavour.LOCAL
);
const page = workspace.createPage({ id: 'page0' });
await initEmptyPage(page);
const frameId = page.getBlockByFlavour('affine:note').at(0)?.id as string;
id = page.addBlock(
'affine:paragraph',
{
text: new page.Text('test 1'),
},
frameId
);
const provider = createIndexedDBBackgroundProvider(
workspace.id,
workspace.doc,
{
awareness: workspace.awarenessStore.awareness,
}
) as LocalIndexedDBBackgroundProvider;
provider.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
provider.disconnect();
const workspaceId = await WorkspaceAdapters[
WorkspaceFlavour.LOCAL
].CRUD.create(workspace);
await store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.SubDoc,
},
]);
_cleanupBlockSuiteWorkspaceCache();
}
store.set(
rootCurrentWorkspaceIdAtom,
(await store.get(rootWorkspacesMetadataAtom))[0].id
);
const workspace = await store.get(rootCurrentWorkspaceAtom);
expect(workspace).toBeDefined();
const page = workspace.blockSuiteWorkspace.getPage('page0') as Page;
await page.waitForLoaded();
expect(page).not.toBeNull();
const paragraphBlock = page.getBlockById(id) as ParagraphBlockModel;
expect(paragraphBlock).not.toBeNull();
expect(paragraphBlock.text.toString()).toBe('test 1');
});
});

View File

@@ -2,18 +2,26 @@ import { atom } from 'jotai';
import { atomFamily, atomWithStorage } from 'jotai/utils';
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
import type { SettingProps } from '../components/affine/setting-modal';
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false);
export const openSettingModalAtom = atom(false);
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
open: boolean;
};
export const openSettingModalAtom = atom<SettingAtom>({
activeTab: 'appearance',
workspaceId: null,
open: false,
});
export const openDisableCloudAlertModalAtom = atom(false);
export { workspacesAtom } from './root';
type PageMode = 'page' | 'edgeless';
type PageLocalSetting = {
mode: PageMode;
@@ -47,9 +55,16 @@ export const recentPageSettingsAtom = atom<PartialPageLocalSettingWithPageId[]>(
}
);
const defaultPageSetting = {
mode: 'page',
} satisfies PageLocalSetting;
export const pageSettingFamily = atomFamily((pageId: string) =>
atom(
get => get(pageSettingsBaseAtom)[pageId],
get =>
get(pageSettingsBaseAtom)[pageId] ?? {
...defaultPageSetting,
},
(
get,
set,
@@ -61,11 +76,15 @@ export const pageSettingFamily = atomFamily((pageId: string) =>
// pick 3 recent page ids
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
});
const prevSetting = {
...defaultPageSetting,
...get(pageSettingsBaseAtom)[pageId],
};
set(pageSettingsBaseAtom, settings => ({
...settings,
[pageId]: {
...settings[pageId],
...(typeof patch === 'function' ? patch(settings[pageId]) : patch),
...prevSetting,
...(typeof patch === 'function' ? patch(prevSetting) : patch),
},
}));
}

View File

@@ -1,68 +0,0 @@
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
import type { AffinePublicWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { affineApis } from '@affine/workspace/affine/shared';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { atom } from 'jotai';
import { BlockSuiteWorkspace } from '../../shared';
function createPublicWorkspace(
workspaceId: string,
binary: ArrayBuffer,
singlePage = false
): AffinePublicWorkspace {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
}
);
BlockSuiteWorkspace.Y.applyUpdate(
blockSuiteWorkspace.doc,
new Uint8Array(binary)
);
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
blockSuiteWorkspace.awarenessStore.setFlag(
key as keyof BlockSuiteFeatureFlags,
value
);
});
// force disable some features
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false);
return {
flavour: WorkspaceFlavour.PUBLIC,
id: workspaceId,
blockSuiteWorkspace,
};
}
export const publicWorkspaceIdAtom = atom<string | null>(null);
export const publicWorkspacePageIdAtom = atom<string | null>(null);
export const publicPageBlockSuiteAtom = atom<Promise<AffinePublicWorkspace>>(
async get => {
const workspaceId = get(publicWorkspaceIdAtom);
const pageId = get(publicWorkspacePageIdAtom);
if (!workspaceId || !pageId) {
throw new Error('No workspace id or page id');
}
const binary = await affineApis.downloadPublicWorkspacePage(
workspaceId,
pageId
);
return createPublicWorkspace(workspaceId, binary, true);
}
);
export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
async get => {
const workspaceId = get(publicWorkspaceIdAtom);
if (!workspaceId) {
throw new Error('No workspace id');
}
const binary = await affineApis.downloadWorkspace(workspaceId, true);
return createPublicWorkspace(workspaceId, binary, false);
}
);

View File

@@ -1,166 +0,0 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import type {
WorkspaceAdapter,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { ActiveDocProvider } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
/**
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
async (get, { signal }) => {
const { WorkspaceAdapters } = await import('../adapters/workspace');
const flavours: string[] = Object.values(WorkspaceAdapters).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom))
.filter(
workspace => flavours.includes(workspace.flavour)
// TODO: remove this when we remove the legacy cloud
)
.filter(workspace =>
!runtimeConfig.enableLegacyCloud
? workspace.flavour !== WorkspaceFlavour.AFFINE
: true
);
if (jotaiWorkspaces.some(meta => !('version' in meta))) {
// wait until all workspaces have migrated to v2
await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject);
setTimeout(resolve, 1000);
}).catch(() => {
// do nothing
});
}
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const adapter = WorkspaceAdapters[
workspace.flavour
] as WorkspaceAdapter<WorkspaceFlavour>;
assertExists(adapter);
const { CRUD } = adapter;
return CRUD.get(workspace.id).then(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
return workspace;
});
})
).then(workspaces =>
workspaces.filter(
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] =>
workspace !== null
)
);
const workspaceProviders = workspaces.map(workspace =>
workspace.blockSuiteWorkspace.providers.filter(
(provider): provider is ActiveDocProvider =>
'active' in provider && provider.active
)
);
const promises: Promise<void>[] = [];
for (const providers of workspaceProviders) {
for (const provider of providers) {
provider.sync();
promises.push(provider.whenReady);
}
}
// we will wait for all the necessary providers to be ready
await Promise.all(promises);
logger.info('workspaces', workspaces);
return workspaces;
}
);
/**
* This will throw an error if the workspace is not found,
* should not be used on the root component,
* use `rootCurrentWorkspaceIdAtom` instead
*/
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async (get, { signal }) => {
const { WorkspaceAdapters } = await import('../adapters/workspace');
const metadata = await get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {
throw new Error(
'current workspace id is null. this should not happen. If you see this error, please report it to the developer.'
);
}
const targetWorkspace = metadata.find(meta => meta.id === targetId);
if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
if (!('version' in targetWorkspace)) {
// wait until the workspace has migrated to v2
await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject);
setTimeout(resolve, 1000);
}).catch(() => {
// do nothing
});
}
const adapter = WorkspaceAdapters[
targetWorkspace.flavour
] as WorkspaceAdapter<WorkspaceFlavour>;
assertExists(adapter);
const workspace = await adapter.CRUD.get(targetWorkspace.id);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
const providers = workspace.blockSuiteWorkspace.providers.filter(
(provider): provider is ActiveDocProvider =>
'active' in provider && provider.active === true
);
for (const provider of providers) {
provider.sync();
// we will wait for the necessary providers to be ready
await provider.whenReady;
}
logger.info('current workspace', workspace);
globalThis.currentWorkspace = workspace;
globalThis.dispatchEvent(
new CustomEvent('affine:workspace:change', {
detail: { id: workspace.id },
})
);
return workspace;
}
);
declare global {
/**
* @internal debug only
*/
// eslint-disable-next-line no-var
var currentWorkspace: AllWorkspace | undefined;
interface WindowEventMap {
'affine:workspace:change': CustomEvent<{ id: string }>;
}
}
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
//#endregion

View File

@@ -1,6 +1,6 @@
import { useAtom } from 'jotai';
import { isDesktop } from '@affine/env/constant';
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useCallback } from 'react';
export type DateFormats =
| 'MM/dd/YYYY'
@@ -15,10 +15,11 @@ export type AppSetting = {
clientBorder: boolean;
fullWidthLayout: boolean;
windowFrameStyle: 'frameless' | 'NativeTitleBar';
fontStyle: FontFamily;
dateFormat: DateFormats;
startWeekOnMonday: boolean;
disableBlurBackground: boolean;
disableNoisyBackground: boolean;
enableBlurBackground: boolean;
enableNoisyBackground: boolean;
autoCheckUpdate: boolean;
autoDownloadUpdate: boolean;
};
@@ -37,31 +38,45 @@ export const dateFormatOptions: DateFormats[] = [
'dd MMMM YYYY',
];
export const AppSettingAtom = atomWithStorage<AppSetting>('AFFiNE settings', {
clientBorder: false,
export type FontFamily = 'Sans' | 'Serif' | 'Mono';
export const fontStyleOptions = [
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
{ key: 'Serif', value: 'var(--affine-font-serif-family)' },
{ key: 'Mono', value: 'var(--affine-font-mono-family)' },
] satisfies {
key: FontFamily;
value: string;
}[];
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
clientBorder: isDesktop,
fullWidthLayout: false,
windowFrameStyle: 'frameless',
fontStyle: 'Sans',
dateFormat: dateFormatOptions[0],
startWeekOnMonday: false,
disableBlurBackground: false,
disableNoisyBackground: false,
enableBlurBackground: true,
enableNoisyBackground: true,
autoCheckUpdate: true,
autoDownloadUpdate: true,
});
export const useAppSetting = () => {
const [settings, setSettings] = useAtom(AppSettingAtom);
type SetStateAction<Value> = Value | ((prev: Value) => Value);
return [
settings,
useCallback(
(patch: Partial<AppSetting>) => {
setSettings((prev: AppSetting) => ({
...prev,
...patch,
}));
},
[setSettings]
),
] as const;
const appSettingAtom = atom<
AppSetting,
[SetStateAction<Partial<AppSetting>>],
void
>(
get => get(appSettingBaseAtom),
(get, set, apply) => {
const prev = get(appSettingBaseAtom);
const next = typeof apply === 'function' ? apply(prev) : apply;
set(appSettingBaseAtom, { ...prev, ...next });
}
);
export const useAppSetting = () => {
return useAtom(appSettingAtom);
};

View File

@@ -129,16 +129,10 @@ if (environment.isBrowser) {
})
.finally(() => {
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
}
declare global {
// global Events
interface WindowEventMap {
'migration-done': CustomEvent;
}
}

View File

@@ -3,7 +3,6 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import { Generator } from '@blocksuite/store';
import type React from 'react';
import { useCallback } from 'react';
@@ -11,10 +10,7 @@ import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
'test',
WorkspaceFlavour.LOCAL,
{
idGenerator: Generator.AutoIncrement,
}
WorkspaceFlavour.LOCAL
);
const page = blockSuiteWorkspace.createPage({ id: 'page0' });

View File

@@ -4,9 +4,16 @@ import type {
WorkspaceNotFoundError,
} from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant';
import { RequestError } from '@affine/workspace/affine/api';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { NextRouter } from 'next/router';
import type { ErrorInfo, ReactNode } from 'react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
import type React from 'react';
import { Component } from 'react';
@@ -19,13 +26,39 @@ type AffineError =
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| RequestError
| Error;
interface AffineErrorBoundaryState {
error: AffineError | null;
}
export const DumpInfo = (props: Pick<AffineErrorBoundaryProps, 'router'>) => {
const router = props.router;
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const path = router.asPath;
const query = router.query;
return (
<>
<div>
Please copy the following information and send it to the developer.
</div>
<div
style={{
border: '1px solid red',
}}
>
<div>path: {path}</div>
<div>query: {JSON.stringify(query)}</div>
<div>currentWorkspaceId: {currentWorkspaceId}</div>
<div>currentPageId: {currentPageId}</div>
<div>metadata: {JSON.stringify(metadata)}</div>
</div>
</>
);
};
export class AffineErrorBoundary extends Component<
AffineErrorBoundaryProps,
AffineErrorBoundaryState
@@ -46,9 +79,10 @@ export class AffineErrorBoundary extends Component<
public override render(): ReactNode {
if (this.state.error) {
let errorDetail: ReactElement | null = null;
const error = this.state.error;
if (error instanceof PageNotFoundError) {
return (
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
<>
@@ -78,18 +112,20 @@ export class AffineErrorBoundary extends Component<
</>
</>
);
} else if (error instanceof RequestError) {
return (
} else {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
{error.message}
{error.message ?? error.toString()}
</>
);
}
return (
<>
<h1>Sorry.. there was an error</h1>
{error.message ?? error.toString()}
{errorDetail}
<Provider key="JotaiProvider" store={rootStore}>
<DumpInfo router={this.props.router} />
</Provider>
</>
);
}

View File

@@ -10,8 +10,8 @@ export const AppContainer = (props: WorkspaceRootProps) => {
return (
<AppContainerWithoutSettings
useNoisyBackground={!appSettings.disableNoisyBackground}
useBlurBackground={!appSettings.disableBlurBackground}
useNoisyBackground={appSettings.enableNoisyBackground}
useBlurBackground={!appSettings.enableBlurBackground}
{...props}
/>
);

View File

@@ -14,7 +14,7 @@ import { useSetAtom } from 'jotai';
import type { KeyboardEvent } from 'react';
import { useEffect } from 'react';
import { useLayoutEffect } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useState } from 'react';
import { openDisableCloudAlertModalAtom } from '../../../atoms';
import { useAppHelper } from '../../../hooks/use-workspaces';
@@ -45,7 +45,6 @@ const NameWorkspaceContent = ({
onClose,
}: NameWorkspaceContentProps) => {
const [workspaceName, setWorkspaceName] = useState('');
const isComposition = useRef(false);
const handleCreateWorkspace = useCallback(() => {
onConfirmName(workspaceName);
@@ -53,7 +52,7 @@ const NameWorkspaceContent = ({
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && workspaceName && !isComposition.current) {
if (event.key === 'Enter' && workspaceName) {
handleCreateWorkspace();
}
},
@@ -76,12 +75,6 @@ const NameWorkspaceContent = ({
maxLength={64}
minLength={0}
onChange={setWorkspaceName}
onCompositionStart={() => {
isComposition.current = true;
}}
onCompositionEnd={() => {
isComposition.current = false;
}}
/>
<div className={style.buttonGroup}>
<Button
@@ -305,7 +298,7 @@ export const CreateWorkspaceModal = ({
const onConfirmEnableCloudSyncing = useCallback(
(enableCloudSyncing: boolean) => {
(async function () {
if (!runtimeConfig.enableLegacyCloud && enableCloudSyncing) {
if (!runtimeConfig.enableCloud && enableCloudSyncing) {
setOpenDisableCloudAlertModal(true);
} else {
let id = addedId;

View File

@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import type React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
interface EnableAffineCloudModalProps {
@@ -18,7 +17,6 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
onClose,
}) => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
@@ -39,7 +37,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
type="primary"
onClick={onConfirm}
>
{user ? t.Enable() : t['Sign in and Enable']()}
{t['Sign in and Enable']()}
</StyleButton>
<StyleButton shape="round" onClick={onClose}>
{t['Not now']()}

View File

@@ -1,4 +1,10 @@
import { Menu, MenuItem, MenuTrigger, styled } from '@affine/component';
import {
type ButtonProps,
Menu,
MenuItem,
MenuTrigger,
styled,
} from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useI18N } from '@affine/i18n';
import type { FC, ReactElement } from 'react';
@@ -41,7 +47,9 @@ const LanguageMenuContent: FC<{
</>
);
};
export const LanguageMenu: FC = () => {
export const LanguageMenu: FC<{ triggerProps: ButtonProps }> = ({
triggerProps,
}) => {
const i18n = useI18N();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
@@ -62,6 +70,7 @@ export const LanguageMenu: FC = () => {
<MenuTrigger
data-testid="language-menu-button"
style={{ textTransform: 'capitalize' }}
{...triggerProps}
>
{currentLanguage?.originalName}
</MenuTrigger>

View File

@@ -20,7 +20,7 @@ interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: () => Promise<void>;
onDeleteWorkspace: (id: string) => Promise<void>;
}
export const WorkspaceDeleteModal = ({
@@ -30,23 +30,24 @@ export const WorkspaceDeleteModal = ({
onDeleteWorkspace,
}: WorkspaceDeleteProps) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
workspace.blockSuiteWorkspace
);
const [deleteStr, setDeleteStr] = useState<string>('');
const allowDelete = deleteStr === workspaceName;
const t = useAFFiNEI18N();
const handleDelete = useCallback(() => {
onDeleteWorkspace()
onDeleteWorkspace(workspace.id)
.then(() => {
toast(t['Successfully deleted'](), {
portal: document.body,
});
onClose();
})
.catch(() => {
// ignore error
});
}, [onDeleteWorkspace, t]);
}, [onClose, onDeleteWorkspace, t, workspace.id]);
return (
<Modal open={open} onClose={onClose}>
@@ -86,7 +87,6 @@ export const WorkspaceDeleteModal = ({
onChange={setDeleteStr}
data-testid="delete-workspace-input"
placeholder={t['Placeholder of delete workspace']()}
value={deleteStr}
width={315}
height={42}
/>

View File

@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { type FC, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../../shared';
import type { WorkspaceSettingDetailProps } from '../index';
import { WorkspaceDeleteModal } from './delete';
@@ -14,7 +13,8 @@ export const DeleteLeaveWorkspace: FC<{
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
}> = ({ workspace, onDeleteWorkspace }) => {
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
// fixme: cloud regression
const isOwner = true;
const [showDelete, setShowDelete] = useState(false);
const [showLeave, setShowLeave] = useState(false);
@@ -23,10 +23,12 @@ export const DeleteLeaveWorkspace: FC<{
<SettingRow
name={
<span style={{ color: 'var(--affine-error-color)' }}>
{isOwner ? t['Delete Workspace']() : t['Leave Workspace']()}
{isOwner
? t['com.affine.settings.workspace.remove']()
: t['Leave Workspace']()}
</span>
}
desc={t['None yet']()}
desc={t['com.affine.settings.workspace.remove.message']()}
style={{ cursor: 'pointer' }}
onClick={() => {
setShowDelete(true);

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