Compare commits

...

39 Commits

Author SHA1 Message Date
Alex Yang
e4543ef3b7 v0.8.0 2023-08-25 00:05:07 -05:00
Peng Xiao
b7f024eeb2 fix: update changelog 2023-08-25 00:05:07 -05:00
Peng Xiao
ebc98002ba fix: add missing matrix value (#3937) 2023-08-24 23:57:49 -05:00
Peng Xiao
9a89c08fd1 fix: incorrect workflow file (#3935) 2023-08-24 23:57:49 -05:00
Peng Xiao
d749e8a284 fix: disable windows signing for nightly (#3933) 2023-08-24 23:57:49 -05:00
Peng Xiao
0b54d82ddb fix: remove use of glob (#3932) 2023-08-24 23:57:49 -05:00
Alex Yang
a76d99381d fix: add missing package (#3927) 2023-08-24 23:57:49 -05:00
Peng Xiao
d89be5804f fix: support windows auto update (#3911) 2023-08-24 23:57:49 -05:00
Alex Yang
3ddc76a703 v0.8.0-beta.3 2023-08-23 00:41:21 -05:00
Alex Yang
9d6d5594b5 fix(core): search feature not working (#3902) 2023-08-22 19:43:09 -05:00
Alex Yang
96f3bbd484 fix(y-provider): syncing status (#3903) 2023-08-22 19:43:09 -05:00
fourdim
561970f7ff fix: make media print overflow visible (#3893) 2023-08-22 19:43:09 -05:00
Alex Yang
357b403073 chore: bump version (#3901) 2023-08-22 19:43:09 -05:00
Alex Yang
77d1dd674b v0.8.0-beta.2 2023-08-22 13:01:48 -05:00
Peng Xiao
7035584203 build: sign windows app (#3809)
(cherry picked from commit 7d6e91f56e)
2023-08-22 13:01:20 -05:00
Alex Yang
c55df09db0 test: loose cmdk result check (#3888)
(cherry picked from commit 507b5dcfb3)
2023-08-22 13:01:20 -05:00
Alex Yang
ddbc37dd45 chore: bump version (#3885)
(cherry picked from commit 8ec005f7de)
2023-08-22 13:01:20 -05:00
Alex Yang
e59ec2de62 test: fix flaky title insert (#3884)
(cherry picked from commit b5afbe385f)
2023-08-22 13:01:19 -05:00
Noothan am
73aea1e2d1 fix(core): add toast message (#3847)
(cherry picked from commit 2a5ef04397)
2023-08-22 13:01:19 -05:00
Alex Yang
38a50b4fe8 fix(cli): read environment variable (#3883)
(cherry picked from commit 58184679ca)
2023-08-22 13:01:19 -05:00
Alex Yang
c2d901c245 ci: do not build core in e2e test (#3882)
(cherry picked from commit bf00299bc7)
2023-08-22 13:01:19 -05:00
Camol
9eee00ddf3 fix: timers type in browser env (#3875)
(cherry picked from commit fc9981335b)
2023-08-22 13:01:19 -05:00
danielchim
96dcd84ee1 feat: e2e for recent search list (#3872)
(cherry picked from commit eda5ff4d3f)
2023-08-22 13:01:19 -05:00
KaranPant
4d047db7ec fix: recent pages list doesn't update (#3848)
Co-authored-by: Alex Yang <himself65@outlook.com>
(cherry picked from commit 54d74f6f0b)
2023-08-22 13:01:14 -05:00
dependabot[bot]
aa254fc8fd chore: bump @storybook/jest from 0.1.0 to 0.2.1 (#3859)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit c689c08b9a)
2023-08-22 13:01:11 -05:00
Alex Yang
8a69efe899 v0.8.0-beta.1 2023-08-21 10:35:06 -05:00
Alex Yang
ae6171709a chore: bump version (#3865)
(cherry picked from commit 0e99f25fea)
2023-08-21 00:30:16 -05:00
danielchim
b147fc54cc fix: remove tooltip (#3862)
(cherry picked from commit a6d5bde059)
2023-08-21 00:30:16 -05:00
xiaodong zuo
531abb149a chore: bump blocksuite version (#3852)
(cherry picked from commit 72de11b8ca)
2023-08-21 00:30:13 -05:00
xiaodong zuo
a603105175 fix: jump to the correct url after importing notion (#3844)
(cherry picked from commit cae6133f7e)
2023-08-21 00:30:13 -05:00
Alex Yang
e3bf83e107 ci: add cancel id
(cherry picked from commit a348df4c47)
2023-08-21 00:30:12 -05:00
Alex Yang
a22f0c3380 ci: split desktop test (#3849)
(cherry picked from commit 940dbbe9c3)
2023-08-21 00:30:12 -05:00
Alex Yang
4e492cd515 feat(storybook): avoid refresh (#3841)
(cherry picked from commit 956cde308e)
2023-08-21 00:30:06 -05:00
Alex Yang
03ba5b3fbb fix(infra): dynamic import (#3842)
(cherry picked from commit 37c1d9bab1)
2023-08-21 00:30:06 -05:00
Alex Yang
2302797be4 feat: run app in closure (#3790)
(cherry picked from commit e6cd193bf4)
2023-08-21 00:30:06 -05:00
Peng Xiao
82e40325b7 fix: reference page crash for deleted items (#3835)
(cherry picked from commit bd826bb7f9)
2023-08-21 00:30:05 -05:00
Peng Xiao
20a1aa697f fix: page blink issue on navigation (#3833)
(cherry picked from commit ba676eb937)
2023-08-21 00:30:05 -05:00
Peng Xiao
b6c46e82d2 fix: workaround for fullscreen mode (#3829)
(cherry picked from commit 0ae6c977aa)
2023-08-21 00:30:05 -05:00
JimmFly
88648a018c chore: change divider style (#3826)
(cherry picked from commit e389bf902f)
2023-08-21 00:30:05 -05:00
106 changed files with 2233 additions and 1470 deletions

View File

@@ -21,6 +21,7 @@
"native",
"templates",
"y-indexeddb",
"y-provider",
"debug",
"storage",
"infra",

View File

@@ -121,3 +121,7 @@ runs:
- name: Build Infra
shell: bash
run: yarn run build:infra
- name: Build Plugins
shell: bash
run: yarn run build:plugins

177
.github/workflows/build-desktop.yml vendored Normal file
View File

@@ -0,0 +1,177 @@
name: Build(Desktop) & Test
on:
push:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-desktop.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-desktop.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
DISTRIBUTION: desktop
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
run: yarn run build:plugins
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:
name: core
path: ./apps/core/dist
if-no-files-found: error
desktop-test:
name: Desktop Test
runs-on: ${{ matrix.spec.os }}
environment: development
strategy:
fail-fast: false
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: true,
}
- {
os: macos-latest,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: false,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
test: true,
}
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
needs: build-core
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn vitest
working-directory: ./apps/electron
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Upload desktop dist
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: ./apps/electron/dist
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
env:
COVERAGE: true
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
- name: Bundle output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
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
- name: Upload e2e test coverage results
if: ${{ matrix.spec.test }}
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore

View File

@@ -30,6 +30,7 @@ env:
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
DISTRIBUTION: browser
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
@@ -133,26 +134,6 @@ jobs:
path: ./apps/storybook/storybook-static
if-no-files-found: error
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
run: yarn run build:plugins
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:
name: core
path: ./apps/core/dist
if-no-files-found: error
build-storage:
name: Build Storage
runs-on: ubuntu-latest
@@ -239,7 +220,6 @@ jobs:
name: E2E Plugin Test
runs-on: ubuntu-latest
environment: development
needs: build-core
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
@@ -247,11 +227,6 @@ jobs:
with:
playwright-install: true
electron-install: false
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: ./apps/core/dist
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-plugin
@@ -328,8 +303,6 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
environment: development
needs: build-core
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
@@ -337,11 +310,6 @@ jobs:
with:
playwright-install: true
electron-install: false
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: ./apps/core/dist
- name: Run playwright tests
run: yarn e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
@@ -373,7 +341,6 @@ jobs:
name: E2E Migration Test
runs-on: ubuntu-latest
environment: development
needs: build-core
strategy:
matrix:
spec:
@@ -387,12 +354,6 @@ jobs:
playwright-install: true
electron-install: false
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: ./apps/core/dist
- name: Unzip
run: yarn unzip
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
@@ -409,127 +370,6 @@ jobs:
path: ./tests/affine-legacy/${{ matrix.spec.package }}/test-results
if-no-files-found: ignore
desktop-test:
name: Desktop Test
runs-on: ${{ matrix.spec.os }}
environment: development
strategy:
fail-fast: false
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: true,
}
- {
os: macos-latest,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: false,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
test: true,
}
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
needs: build-core
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn vitest
working-directory: ./apps/electron
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Upload desktop dist
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: ./apps/electron/dist
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
env:
COVERAGE: true
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
- name: Bundle output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
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
- name: Upload e2e test coverage results
if: ${{ matrix.spec.test }}
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
unit-test:
name: Unit Test
runs-on: ubuntu-latest
@@ -552,79 +392,3 @@ jobs:
flags: unittest
name: affine
fail_ci_if_error: false
build-docker:
if: github.ref == 'refs/heads/master'
name: Build Docker
runs-on: ubuntu-latest
needs:
- build-server
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: ./apps/core/dist
- name: Download server dist
uses: actions/download-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Setup Git short hash
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
logout: false
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build front Dockerfile
uses: docker/build-push-action@v4
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:latest
# setup node without cache configuration
# Prisma cache is not compatible with docker build cache
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
- name: Install Node.js dependencies
run: yarn workspaces focus @affine/server --production
- name: Generate Prisma client
run: yarn workspace @affine/server prisma generate
- name: Build graphql Dockerfile
uses: docker/build-push-action@v4
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/node/Dockerfile
tags: ghcr.io/toeverything/affine-graphql:${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-graphql:latest

View File

@@ -14,5 +14,5 @@ jobs:
- uses: styfle/cancel-workflow-action@0.11.0
with:
# See https://api.github.com/repos/toeverything/AFFiNE/actions/workflows
workflow_id: 44038251, 61883931, 65188160
workflow_id: 44038251, 61883931, 65188160, 66789140
access_token: ${{ github.token }}

View File

@@ -73,34 +73,27 @@ jobs:
make-distribution:
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- {
os: macos-latest,
platform: darwin,
arch: x64,
target: x86_64-apple-darwin,
}
- {
os: macos-latest,
platform: darwin,
arch: arm64,
target: aarch64-apple-darwin,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
}
- {
os: windows-latest,
platform: win32,
arch: x64,
target: x86_64-pc-windows-msvc,
}
runs-on: ${{ matrix.spec.os }}
- runner: macos-latest
platform: darwin
arch: x64
target: x86_64-apple-darwin
- runner: macos-latest
platform: darwin
arch: arm64
target: aarch64-apple-darwin
- runner: ubuntu-latest
platform: linux
arch: x64
target: x86_64-unknown-linux-gnu
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
needs:
- before-make
- set-build-version

View File

@@ -77,34 +77,23 @@ jobs:
make-distribution:
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- {
os: macos-latest,
platform: darwin,
arch: x64,
target: x86_64-apple-darwin,
}
- {
os: macos-latest,
platform: darwin,
arch: arm64,
target: aarch64-apple-darwin,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
}
- {
os: windows-latest,
platform: win32,
arch: x64,
target: x86_64-pc-windows-msvc,
}
runs-on: ${{ matrix.spec.os }}
- runner: macos-latest
platform: darwin
arch: x64
target: x86_64-apple-darwin
- runner: macos-latest
platform: darwin
arch: arm64
target: aarch64-apple-darwin
- runner: ubuntu-latest
platform: linux
arch: x64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.spec.runner }}
needs: before-make
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
@@ -151,15 +140,6 @@ jobs:
mkdir -p builds
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
mv apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
- name: Save artifacts (windows)
if: ${{ matrix.spec.platform == 'win32' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe
mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi
mv apps/electron/out/*/make/squirrel.windows/x64/*.nupkg ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.nupkg
- name: Save artifacts (linux)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
@@ -173,8 +153,166 @@ jobs:
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
package-distribution-windows:
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
needs: before-make
outputs:
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
env:
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
- name: Setup Maker
timeout-minutes: 10
uses: ./.github/actions/setup-maker
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- uses: actions/download-artifact@v3
with:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: package
run: yarn workspace @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: get all files to be signed
id: get_files_to_be_signed
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\apps\electron\out\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Zip artifacts for faster upload
run: Compress-Archive -CompressionLevel Fastest -Path apps/electron/out/* -DestinationPath archive.zip
- name: Save packaged artifacts for signing
uses: actions/upload-artifact@v3
with:
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: |
archive.zip
!**/*.map
sign-packaged-artifacts-windows:
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED }}
artifact-name: packaged-win32-x64
make-windows-installer:
environment: production
needs: sign-packaged-artifacts-windows
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
outputs:
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
- name: Download and overwrite packaged artifacts
uses: actions/download-artifact@v3
with:
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: .
- name: unzip file
run: Expand-Archive -Path signed.zip -DestinationPath apps/electron/out
- name: Make squirrel.windows installer
run: yarn workspace @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Zip artifacts for faster upload
run: Compress-Archive -CompressionLevel Fastest -Path apps/electron/out/${{ env.BUILD_TYPE }}/make/* -DestinationPath archive.zip
- name: get all files to be signed
id: get_files_to_be_signed
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path apps/electron/out/${{ env.BUILD_TYPE }}/make -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\apps\electron\out\${{ env.BUILD_TYPE }}\make\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Save installer for signing
uses: actions/upload-artifact@v3
with:
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: archive.zip
sign-installer-artifacts-windows:
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED }}
artifact-name: installer-win32-x64
finalize-installer-windows:
environment: production
needs: sign-installer-artifacts-windows
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
steps:
- name: Download and overwrite installer artifacts
uses: actions/download-artifact@v3
with:
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: .
- name: unzip file
run: Expand-Archive -Path signed.zip -DestinationPath apps/electron/out/${{ env.BUILD_TYPE }}/make
- name: Save artifacts
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe
mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
needs: [before-make, make-distribution]
needs: [before-make, make-distribution, finalize-installer-windows]
runs-on: ubuntu-latest
steps:
@@ -222,8 +360,6 @@ jobs:
./*.zip
./*.dmg
./*.exe
./*.nupkg
./RELEASES
./*.AppImage
./*.apk
./*.yml

View File

@@ -5,6 +5,13 @@ on:
branches:
- master
env:
BUILD_TYPE: stable
APP_NAME: affine
COVERAGE: false
DISTRIBUTION: browser
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
release:
name: Try publishing npm@latest release
@@ -17,3 +24,142 @@ jobs:
run: ./scripts/publish.sh
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
run: yarn run build:plugins
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:
name: core
path: ./apps/core/dist
if-no-files-found: error
build-server:
name: Build Server
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Build Server
run: yarn nx build @affine/server
- name: Upload server dist
uses: actions/upload-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
if-no-files-found: error
build-storage:
name: Build Storage
runs-on: ubuntu-latest
env:
RUSTFLAGS: '-C debuginfo=1'
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
target: 'x86_64-unknown-linux-gnu'
- name: Build Storage
run: yarn build:storage
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
build-docker:
if: github.ref == 'refs/heads/master'
name: Build Docker
runs-on: ubuntu-latest
needs:
- build-server
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: ./apps/core/dist
- name: Download server dist
uses: actions/download-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Setup Git short hash
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
logout: false
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build front Dockerfile
uses: docker/build-push-action@v4
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:latest
# setup node without cache configuration
# Prisma cache is not compatible with docker build cache
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
- name: Install Node.js dependencies
run: yarn workspaces focus @affine/server --production
- name: Generate Prisma client
run: yarn workspace @affine/server prisma generate
- name: Build graphql Dockerfile
uses: docker/build-push-action@v4
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/node/Dockerfile
tags: ghcr.io/toeverything/affine-graphql:${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-graphql:latest

42
.github/workflows/windows-signer.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Windows Signer
on:
workflow_call:
inputs:
artifact-name:
required: true
type: string
files:
required: true
type: string
jobs:
sign:
runs-on: [self-hosted, win-signer]
env:
ARCHIVE_DIR: ${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.artifact-name }}
steps:
- uses: actions/download-artifact@v3
with:
name: ${{ inputs.artifact-name }}
path: ${{ env.ARCHIVE_DIR }}
- name: unzip file
shell: cmd
# 7za is pre-installed on the signer machine
run: |
cd ${{ env.ARCHIVE_DIR }}
md out
7za x archive.zip -y -oout
- name: sign
shell: cmd
run: |
cd ${{ env.ARCHIVE_DIR }}/out
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: zip file
shell: cmd
run: |
cd ${{ env.ARCHIVE_DIR }}
7za a signed.zip .\out\*
- name: upload
uses: actions/upload-artifact@v3
with:
name: signed-${{ inputs.artifact-name }}
path: ${{ env.ARCHIVE_DIR }}/signed.zip

View File

@@ -30,7 +30,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableTestProperties: false,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0818',
changelogUrl: 'https://affine.pro/blog/affine-080-launch-week-day5',
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
enablePreloading: true,
enableNewSettingModal: true,

View File

@@ -2,11 +2,11 @@
"name": "@affine/core",
"type": "module",
"private": true,
"version": "0.8.0-beta.0",
"version": "0.8.0",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
"static-server": "ts-node-esm ./server.mts"
"static-server": "yarn -T run dev-core --static"
},
"exports": {
"./app": "./src/app.tsx",
@@ -24,13 +24,13 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/icons": "^2.1.31",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
@@ -39,7 +39,7 @@
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.5",
"@react-hookz/web": "^23.1.0",
"@toeverything/components": "^0.0.11",
"@toeverything/components": "^0.0.12",
"async-call-rpc": "^6.3.1",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
@@ -84,7 +84,6 @@
"swc-loader": "^0.2.3",
"swc-plugin-coverage-instrument": "^0.0.20",
"thread-loader": "^4.0.2",
"ts-node": "^10.9.1",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",

View File

@@ -1,18 +0,0 @@
// static server for web app
import express from 'express';
const app = express();
const PORT = process.env.PORT || 8080;
app.use('/', express.static('dist'));
app.get('/*', (req, res) => {
if (req.url.startsWith('/plugins')) {
res.sendFile(req.url, { root: 'dist' });
}
res.sendFile('index.html', { root: 'dist' });
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

View File

@@ -1,17 +1,23 @@
import { assertExists } from '@blocksuite/global/utils';
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
import {
getCurrentStore,
loadedPluginNameAtom,
} from '@toeverything/infra/atom';
import { use } from 'foxact/use';
import { useAtomValue } from 'jotai';
import { Provider } from 'jotai/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { _pluginNestedImportsMap } from '../bootstrap/plugins/setup';
import { pluginRegisterPromise } from '../bootstrap/register-plugins';
import { createSetup } from '../bootstrap/plugins/setup';
import { bootstrapPluginSystem } from '../bootstrap/register-plugins';
async function main() {
const { setup } = await import('../bootstrap/setup');
await setup();
const rootStore = getCurrentStore();
await setup(rootStore);
const { _pluginNestedImportsMap } = createSetup(rootStore);
const pluginRegisterPromise = bootstrapPluginSystem(rootStore);
const root = document.getElementById('app');
assertExists(root);

View File

@@ -20,8 +20,9 @@ import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { nanoid } from '@blocksuite/store';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import { rootStore } from '@toeverything/infra/atom';
import { getCurrentStore } from '@toeverything/infra/atom';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { useCallback } from 'react';
import { setPageModeAtom } from '../../atoms';
import {
@@ -46,7 +47,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
buildShowcaseWorkspace(blockSuiteWorkspace, {
store: rootStore,
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},
@@ -91,7 +92,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
<>
<PageDetailEditor
pageId={currentPageId}
onInit={initEmptyPage}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor}
workspace={workspace}
/>

View File

@@ -5,6 +5,7 @@ import '@toeverything/components/style.css';
import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace';
import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { use } from 'foxact/use';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense } from 'react';
@@ -47,7 +48,7 @@ export const App = memo(function App() {
use(languageLoadingPromise);
return (
<CacheProvider value={cache}>
<AffineContext>
<AffineContext store={getCurrentStore()}>
<DebugProvider>
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}

View File

@@ -18,20 +18,22 @@ import {
contentLayoutAtom,
currentPageAtom,
currentWorkspaceAtom,
rootStore,
} from '@toeverything/infra/atom';
import { atom } from 'jotai';
import { Provider } from 'jotai/react';
import type { createStore } from 'jotai/vanilla';
import { createElement, type PropsWithChildren } from 'react';
import { createFetch } from './endowments/fercher';
import { createTimers } from './endowments/timer';
import { setupImportsMap } from './setup-imports-map';
const dynamicImportKey = '$h_import';
// DO NOT REMOVE INVISIBLE CHARACTERS
const dynamicImportKey = '$h_import';
const permissionLogger = new DebugLogger('plugins:permission');
const importLogger = new DebugLogger('plugins:import');
const entryLogger = new DebugLogger('plugins:entry');
const pushLayoutAtom = atom<
null,
@@ -39,7 +41,11 @@ const pushLayoutAtom = atom<
[
pluginName: string,
create: (root: HTMLElement) => () => void,
options: { maxWidth: (number | undefined)[] } | undefined,
options:
| {
maxWidth: (number | undefined)[];
}
| undefined,
],
void
>(null, (_, set, pluginName, callback, options) => {
@@ -106,438 +112,469 @@ const deleteLayoutAtom = atom<null, [string], void>(null, (_, set, id) => {
});
});
// module -> importName -> updater[]
export const _rootImportsMap = new Map<string, Map<string, any>>();
const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
react: import('react'),
'react/jsx-runtime': import('react/jsx-runtime'),
'react-dom': import('react-dom'),
'react-dom/client': import('react-dom/client'),
jotai: import('jotai'),
'jotai/utils': import('jotai/utils'),
swr: import('swr'),
'@affine/component': import('@affine/component'),
'@blocksuite/icons': import('@blocksuite/icons'),
'@blocksuite/blocks': import('@blocksuite/blocks'),
'@affine/sdk/entry': {
rootStore: rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageAtom: currentPageAtom,
pushLayoutAtom: pushLayoutAtom,
deleteLayoutAtom: deleteLayoutAtom,
},
'@blocksuite/global/utils': import('@blocksuite/global/utils'),
'@toeverything/infra/atom': import('@toeverything/infra/atom'),
'@toeverything/components/button': import('@toeverything/components/button'),
});
// pluginName -> module -> importName -> updater[]
export const _pluginNestedImportsMap = new Map<
string,
Map<string, Map<string, any>>
const setupWeakMap = new WeakMap<
ReturnType<typeof createStore>,
ReturnType<typeof createSetupImpl>
>();
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
export const createImports = (pluginName: string) => {
if (pluginImportsFunctionMap.has(pluginName)) {
export function createSetup(rootStore: ReturnType<typeof createStore>) {
if (setupWeakMap.has(rootStore)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return pluginImportsFunctionMap.get(pluginName)!;
return setupWeakMap.get(rootStore)!;
}
const imports = (
newUpdaters: [string, [string, ((val: any) => void)[]][]][]
const setup = createSetupImpl(rootStore);
setupWeakMap.set(rootStore, setup);
return setup;
}
function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
// clean up plugin windows when switching to other pages
rootStore.sub(currentPageAtom, () => {
rootStore.set(contentLayoutAtom, 'editor');
});
// module -> importName -> updater[]
const _rootImportsMap = new Map<string, Map<string, any>>();
const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
react: import('react'),
'react/jsx-runtime': import('react/jsx-runtime'),
'react-dom': import('react-dom'),
'react-dom/client': import('react-dom/client'),
jotai: import('jotai'),
'jotai/utils': import('jotai/utils'),
swr: import('swr'),
'@affine/component': import('@affine/component'),
'@blocksuite/icons': import('@blocksuite/icons'),
'@blocksuite/blocks': import('@blocksuite/blocks'),
'@affine/sdk/entry': {
rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageAtom: currentPageAtom,
pushLayoutAtom: pushLayoutAtom,
deleteLayoutAtom: deleteLayoutAtom,
},
'@blocksuite/global/utils': import('@blocksuite/global/utils'),
'@toeverything/infra/atom': import('@toeverything/infra/atom'),
'@toeverything/components/button': import(
'@toeverything/components/button'
),
});
// pluginName -> module -> importName -> updater[]
const _pluginNestedImportsMap = new Map<
string,
Map<string, Map<string, any>>
>();
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
const createImports = (pluginName: string) => {
if (pluginImportsFunctionMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return pluginImportsFunctionMap.get(pluginName)!;
}
const imports = (
newUpdaters: [string, [string, ((val: any) => void)[]][]][]
) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
importLogger.debug('currentImportMap', pluginName, currentImportMap);
for (const [module, moduleUpdaters] of newUpdaters) {
importLogger.debug('imports module', module, moduleUpdaters);
let moduleImports = _rootImportsMap.get(module);
if (!moduleImports) {
moduleImports = currentImportMap.get(module);
}
if (moduleImports) {
for (const [importName, importUpdaters] of moduleUpdaters) {
const updateImport = (value: any) => {
for (const importUpdater of importUpdaters) {
importUpdater(value);
}
};
if (moduleImports.has(importName)) {
const val = moduleImports.get(importName);
updateImport(val);
}
}
} else {
console.error(
'cannot find module in plugin import map',
module,
currentImportMap,
_pluginNestedImportsMap
);
}
}
};
pluginImportsFunctionMap.set(pluginName, imports);
return imports;
};
const abortController = new AbortController();
const pluginFetch = createFetch({});
const timer = createTimers(abortController.signal);
const sharedGlobalThis = Object.assign(Object.create(null), timer, {
Object: globalThis.Object,
fetch: pluginFetch,
ReadableStream: globalThis.ReadableStream,
Symbol: globalThis.Symbol,
Error: globalThis.Error,
TypeError: globalThis.TypeError,
RangeError: globalThis.RangeError,
console: globalThis.console,
crypto: globalThis.crypto,
});
const dynamicImportMap = new Map<
string,
(moduleName: string) => Promise<any>
>();
const createOrGetDynamicImport = (baseUrl: string, pluginName: string) => {
if (dynamicImportMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return dynamicImportMap.get(pluginName)!;
}
const dynamicImport = async (moduleName: string): Promise<any> => {
const codeUrl = `${baseUrl}/${moduleName}`;
const analysisUrl = `${baseUrl}/${moduleName}.json`;
const response = await fetch(codeUrl);
const analysisResponse = await fetch(analysisUrl);
const analysis = await analysisResponse.json();
const exports = analysis.exports as string[];
const code = await response.text();
const moduleCompartment = new Compartment(
createOrGetGlobalThis(
pluginName,
// use singleton here to avoid infinite loop
createOrGetDynamicImport(pluginName, baseUrl)
)
);
const entryPoint = moduleCompartment.evaluate(code, {
__evadeHtmlCommentTest__: true,
});
const moduleExports = {} as Record<string, any>;
const setVarProxy = new Proxy(
{},
{
get(_, p: string): any {
return (newValue: any) => {
moduleExports[p] = newValue;
};
},
}
);
entryPoint({
imports: createImports(pluginName),
liveVar: setVarProxy,
onceVar: setVarProxy,
});
importLogger.debug('import', moduleName, exports, moduleExports);
return moduleExports;
};
dynamicImportMap.set(pluginName, dynamicImport);
return dynamicImport;
};
const globalThisMap = new Map<string, any>();
const createOrGetGlobalThis = (
pluginName: string,
dynamicImport: (moduleName: string) => Promise<any>
) => {
if (globalThisMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return globalThisMap.get(pluginName)!;
}
const pluginGlobalThis = Object.assign(
Object.create(null),
sharedGlobalThis,
{
// fixme: vite build output bundle will have this, we should remove it
process: Object.freeze({
env: {
NODE_ENV: process.env.NODE_ENV,
},
}),
// dynamic import function
[dynamicImportKey]: dynamicImport,
// UNSAFE: React will read `window` and `document`
window: new Proxy(
{},
{
get(_, key) {
permissionLogger.debug(`${pluginName} is accessing window`, key);
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
const result = Reflect.get(window, key);
if (typeof result === 'function') {
if (result === ShadowRoot) {
return result;
}
return function (...args: any[]) {
permissionLogger.debug(
`${pluginName} is calling window`,
key,
args
);
return result.apply(window, args);
};
}
permissionLogger.debug('window', key, result);
return result;
},
}
),
document: new Proxy(
{},
{
get(_, key) {
permissionLogger.debug(
`${pluginName} is accessing document`,
key
);
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
const result = Reflect.get(document, key);
if (typeof result === 'function') {
return function (...args: any[]) {
permissionLogger.debug(
`${pluginName} is calling window`,
key,
args
);
return result.apply(document, args);
};
}
permissionLogger.debug('document', key, result);
return result;
},
}
),
navigator: {
userAgent: navigator.userAgent,
},
MouseEvent: globalThis.MouseEvent,
KeyboardEvent: globalThis.KeyboardEvent,
CustomEvent: globalThis.CustomEvent,
// copilot uses these
Date: globalThis.Date,
Math: globalThis.Math,
URL: globalThis.URL,
URLSearchParams: globalThis.URLSearchParams,
Headers: globalThis.Headers,
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
Request: globalThis.Request,
// image-preview uses these
Blob: globalThis.Blob,
ClipboardItem: globalThis.ClipboardItem,
// vue uses these
Element: globalThis.Element,
SVGElement: globalThis.SVGElement,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
IDBRequest: globalThis.IDBRequest,
IDBDatabase: globalThis.IDBDatabase,
IDBCursorWithValue: globalThis.IDBCursorWithValue,
IDBFactory: globalThis.IDBFactory,
IDBKeyRange: globalThis.IDBKeyRange,
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
IDBTransaction: globalThis.IDBTransaction,
IDBObjectStore: globalThis.IDBObjectStore,
IDBIndex: globalThis.IDBIndex,
IDBCursor: globalThis.IDBCursor,
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
}
);
pluginGlobalThis.global = pluginGlobalThis;
globalThisMap.set(pluginName, pluginGlobalThis);
return pluginGlobalThis;
};
const setupPluginCode = async (
baseUrl: string,
pluginName: string,
filename: string
) => {
await rootImportsMapSetupPromise;
if (!_pluginNestedImportsMap.has(pluginName)) {
_pluginNestedImportsMap.set(pluginName, new Map());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
importLogger.debug('currentImportMap', pluginName, currentImportMap);
const isMissingPackage = (name: string) =>
_rootImportsMap.has(name) && !currentImportMap.has(name);
for (const [module, moduleUpdaters] of newUpdaters) {
importLogger.debug('imports module', module, moduleUpdaters);
let moduleImports = _rootImportsMap.get(module);
if (!moduleImports) {
moduleImports = currentImportMap.get(module);
}
if (moduleImports) {
for (const [importName, importUpdaters] of moduleUpdaters) {
const updateImport = (value: any) => {
for (const importUpdater of importUpdaters) {
importUpdater(value);
}
};
if (moduleImports.has(importName)) {
const val = moduleImports.get(importName);
updateImport(val);
}
const bundleAnalysis = await fetch(`${baseUrl}/${filename}.json`).then(
res => res.json()
);
const moduleExports = bundleAnalysis.exports as Record<string, [string]>;
const moduleImports = bundleAnalysis.imports as string[];
const moduleReexports = bundleAnalysis.reexports as Record<
string,
[localName: string, exportedName: string][]
>;
await Promise.all(
moduleImports.map(name => {
if (isMissingPackage(name)) {
return Promise.resolve();
} else {
importLogger.debug('missing package', name);
return setupPluginCode(baseUrl, pluginName, name);
}
} else {
console.error(
'cannot find module in plugin import map',
module,
currentImportMap,
_pluginNestedImportsMap
);
}
}
};
pluginImportsFunctionMap.set(pluginName, imports);
return imports;
};
const abortController = new AbortController();
const pluginFetch = createFetch({});
const timer = createTimers(abortController.signal);
const sharedGlobalThis = Object.assign(Object.create(null), timer, {
Object: globalThis.Object,
fetch: pluginFetch,
ReadableStream: globalThis.ReadableStream,
Symbol: globalThis.Symbol,
Error: globalThis.Error,
TypeError: globalThis.TypeError,
RangeError: globalThis.RangeError,
console: globalThis.console,
crypto: globalThis.crypto,
});
const dynamicImportMap = new Map<
string,
(moduleName: string) => Promise<any>
>();
export const createOrGetDynamicImport = (
baseUrl: string,
pluginName: string
) => {
if (dynamicImportMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return dynamicImportMap.get(pluginName)!;
}
const dynamicImport = async (moduleName: string): Promise<any> => {
const codeUrl = `${baseUrl}/${moduleName}`;
const analysisUrl = `${baseUrl}/${moduleName}.json`;
const response = await fetch(codeUrl);
const analysisResponse = await fetch(analysisUrl);
const analysis = await analysisResponse.json();
const exports = analysis.exports as string[];
const code = await response.text();
})
);
const code = await fetch(
`${baseUrl}/${filename.replace(/^\.\//, '')}`
).then(res => res.text());
importLogger.debug('evaluating', filename);
const moduleCompartment = new Compartment(
createOrGetGlobalThis(
pluginName,
// use singleton here to avoid infinite loop
createOrGetDynamicImport(pluginName, baseUrl)
createOrGetDynamicImport(baseUrl, pluginName)
)
);
const entryPoint = moduleCompartment.evaluate(code, {
__evadeHtmlCommentTest__: true,
});
const moduleExports = {} as Record<string, any>;
const moduleExportsMap = new Map<string, any>();
const setVarProxy = new Proxy(
{},
{
get(_, p: string): any {
return (newValue: any) => {
moduleExports[p] = newValue;
moduleExportsMap.set(p, newValue);
};
},
}
);
currentImportMap.set(filename, moduleExportsMap);
entryPoint({
imports: createImports(pluginName),
liveVar: setVarProxy,
onceVar: setVarProxy,
});
importLogger.debug('import', moduleName, exports, moduleExports);
return moduleExports;
for (const [newExport, [originalExport]] of Object.entries(moduleExports)) {
if (newExport === originalExport) continue;
const value = moduleExportsMap.get(originalExport);
moduleExportsMap.set(newExport, value);
moduleExportsMap.delete(originalExport);
}
for (const [name, reexports] of Object.entries(moduleReexports)) {
const targetExports = currentImportMap.get(filename);
const moduleExports = currentImportMap.get(name);
assertExists(targetExports);
assertExists(moduleExports);
for (const [exportedName, localName] of reexports) {
const exportedValue: any = moduleExports.get(exportedName);
assertExists(exportedValue);
targetExports.set(localName, exportedValue);
}
}
};
dynamicImportMap.set(pluginName, dynamicImport);
return dynamicImport;
};
const globalThisMap = new Map<string, any>();
const PluginProvider = ({ children }: PropsWithChildren) =>
createElement(
Provider,
{
store: rootStore,
},
children
);
export const createOrGetGlobalThis = (
pluginName: string,
dynamicImport: (moduleName: string) => Promise<any>
) => {
if (globalThisMap.has(pluginName)) {
const evaluatePluginEntry = (pluginName: string) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return globalThisMap.get(pluginName)!;
}
const pluginGlobalThis = Object.assign(
Object.create(null),
sharedGlobalThis,
{
// fixme: vite build output bundle will have this, we should remove it
process: Object.freeze({
env: {
NODE_ENV: process.env.NODE_ENV,
},
}),
// dynamic import function
[dynamicImportKey]: dynamicImport,
// UNSAFE: React will read `window` and `document`
window: new Proxy(
{},
{
get(_, key) {
permissionLogger.debug(`${pluginName} is accessing window`, key);
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
const result = Reflect.get(window, key);
if (typeof result === 'function') {
if (result === ShadowRoot) {
return result;
}
return function (...args: any[]) {
permissionLogger.debug(
`${pluginName} is calling window`,
key,
args
);
return result.apply(window, args);
};
}
permissionLogger.debug('window', key, result);
return result;
},
}
),
document: new Proxy(
{},
{
get(_, key) {
permissionLogger.debug(`${pluginName} is accessing document`, key);
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
const result = Reflect.get(document, key);
if (typeof result === 'function') {
return function (...args: any[]) {
permissionLogger.debug(
`${pluginName} is calling window`,
key,
args
);
return result.apply(document, args);
};
}
permissionLogger.debug('document', key, result);
return result;
},
}
),
navigator: {
userAgent: navigator.userAgent,
},
MouseEvent: globalThis.MouseEvent,
KeyboardEvent: globalThis.KeyboardEvent,
CustomEvent: globalThis.CustomEvent,
// copilot uses these
Date: globalThis.Date,
Math: globalThis.Math,
URL: globalThis.URL,
URLSearchParams: globalThis.URLSearchParams,
Headers: globalThis.Headers,
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
Request: globalThis.Request,
// image-preview uses these
Blob: globalThis.Blob,
ClipboardItem: globalThis.ClipboardItem,
// vue uses these
Element: globalThis.Element,
SVGElement: globalThis.SVGElement,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
IDBRequest: globalThis.IDBRequest,
IDBDatabase: globalThis.IDBDatabase,
IDBCursorWithValue: globalThis.IDBCursorWithValue,
IDBFactory: globalThis.IDBFactory,
IDBKeyRange: globalThis.IDBKeyRange,
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
IDBTransaction: globalThis.IDBTransaction,
IDBObjectStore: globalThis.IDBObjectStore,
IDBIndex: globalThis.IDBIndex,
IDBCursor: globalThis.IDBCursor,
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
}
);
pluginGlobalThis.global = pluginGlobalThis;
globalThisMap.set(pluginName, pluginGlobalThis);
return pluginGlobalThis;
};
export const setupPluginCode = async (
baseUrl: string,
pluginName: string,
filename: string
) => {
await rootImportsMapSetupPromise;
if (!_pluginNestedImportsMap.has(pluginName)) {
_pluginNestedImportsMap.set(pluginName, new Map());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const isMissingPackage = (name: string) =>
_rootImportsMap.has(name) && !currentImportMap.has(name);
const bundleAnalysis = await fetch(`${baseUrl}/${filename}.json`).then(res =>
res.json()
);
const moduleExports = bundleAnalysis.exports as Record<string, [string]>;
const moduleImports = bundleAnalysis.imports as string[];
const moduleReexports = bundleAnalysis.reexports as Record<
string,
[localName: string, exportedName: string][]
>;
await Promise.all(
moduleImports.map(name => {
if (isMissingPackage(name)) {
return Promise.resolve();
} else {
importLogger.debug('missing package', name);
return setupPluginCode(baseUrl, pluginName, name);
}
})
);
const code = await fetch(`${baseUrl}/${filename.replace(/^\.\//, '')}`).then(
res => res.text()
);
importLogger.debug('evaluating', filename);
const moduleCompartment = new Compartment(
createOrGetGlobalThis(
pluginName,
// use singleton here to avoid infinite loop
createOrGetDynamicImport(baseUrl, pluginName)
)
);
const entryPoint = moduleCompartment.evaluate(code, {
__evadeHtmlCommentTest__: true,
});
const moduleExportsMap = new Map<string, any>();
const setVarProxy = new Proxy(
{},
{
get(_, p: string): any {
return (newValue: any) => {
moduleExportsMap.set(p, newValue);
};
},
}
);
currentImportMap.set(filename, moduleExportsMap);
entryPoint({
imports: createImports(pluginName),
liveVar: setVarProxy,
onceVar: setVarProxy,
});
for (const [newExport, [originalExport]] of Object.entries(moduleExports)) {
if (newExport === originalExport) continue;
const value = moduleExportsMap.get(originalExport);
moduleExportsMap.set(newExport, value);
moduleExportsMap.delete(originalExport);
}
for (const [name, reexports] of Object.entries(moduleReexports)) {
const targetExports = currentImportMap.get(filename);
const moduleExports = currentImportMap.get(name);
assertExists(targetExports);
assertExists(moduleExports);
for (const [exportedName, localName] of reexports) {
const exportedValue: any = moduleExports.get(exportedName);
assertExists(exportedValue);
targetExports.set(localName, exportedValue);
}
}
};
const PluginProvider = ({ children }: PropsWithChildren) =>
createElement(
Provider,
{
store: rootStore,
},
children
);
const entryLogger = new DebugLogger('plugin:entry');
export const evaluatePluginEntry = (pluginName: string) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const pluginExports = currentImportMap.get('index.js');
assertExists(pluginExports);
const entryFunction = pluginExports.get('entry');
const cleanup = entryFunction(<PluginContext>{
register: (part, callback) => {
entryLogger.info(`Registering ${pluginName} to ${part}`);
if (part === 'headerItem') {
rootStore.set(pluginHeaderItemAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['headerItem'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginHeaderItemAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'editor') {
rootStore.set(pluginEditorAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['editor'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginEditorAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'setting') {
rootStore.set(pluginSettingAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['setting'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginSettingAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'formatBar') {
const register = (widget: AffineFormatBarWidget) => {
const div = document.createElement('div');
const root = widget.root;
const cleanup = (callback as CallbackMap['formatBar'])(
div,
widget.page,
() => {
return root.selectionManager.value;
}
);
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const pluginExports = currentImportMap.get('index.js');
assertExists(pluginExports);
const entryFunction = pluginExports.get('entry');
const cleanup = entryFunction(<PluginContext>{
register: (part, callback) => {
entryLogger.info(`Registering ${pluginName} to ${part}`);
if (part === 'headerItem') {
rootStore.set(pluginHeaderItemAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['headerItem'],
}));
addCleanup(pluginName, () => {
AffineFormatBarWidget.customElements.delete(register);
cleanup();
rootStore.set(pluginHeaderItemAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
return div;
};
AffineFormatBarWidget.customElements.add(register);
} else {
throw new Error(`Unknown part: ${part}`);
}
},
utils: {
PluginProvider,
},
});
if (typeof cleanup !== 'function') {
throw new Error('Plugin entry must return a function');
}
addCleanup(pluginName, cleanup);
};
} else if (part === 'editor') {
rootStore.set(pluginEditorAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['editor'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginEditorAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'setting') {
rootStore.set(pluginSettingAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['setting'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginSettingAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'formatBar') {
const register = (widget: AffineFormatBarWidget) => {
const div = document.createElement('div');
const root = widget.root;
const cleanup = (callback as CallbackMap['formatBar'])(
div,
widget.page,
() => {
return root.selectionManager.value;
}
);
addCleanup(pluginName, () => {
AffineFormatBarWidget.customElements.delete(register);
cleanup();
});
return div;
};
AffineFormatBarWidget.customElements.add(register);
} else {
throw new Error(`Unknown part: ${part}`);
}
},
utils: {
PluginProvider,
},
});
if (typeof cleanup !== 'function') {
throw new Error('Plugin entry must return a function');
}
addCleanup(pluginName, cleanup);
};
return {
_rootImportsMap,
_pluginNestedImportsMap,
createImports,
createOrGetDynamicImport,
setupPluginCode,
evaluatePluginEntry,
createOrGetGlobalThis,
};
}

View File

@@ -5,11 +5,14 @@ import {
invokeCleanup,
pluginPackageJson,
} from '@toeverything/infra/__internal__/plugin';
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
import {
getCurrentStore,
loadedPluginNameAtom,
} from '@toeverything/infra/atom';
import { packageJsonOutputSchema } from '@toeverything/infra/type';
import type { z } from 'zod';
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
import { createSetup } from './plugins/setup';
const logger = new DebugLogger('register-plugins');
@@ -20,107 +23,112 @@ declare global {
Object.defineProperty(globalThis, '__pluginPackageJson__', {
get() {
return rootStore.get(pluginPackageJson);
return getCurrentStore().get(pluginPackageJson);
},
});
rootStore.sub(enabledPluginAtom, () => {
const added = new Set<string>();
const removed = new Set<string>();
const enabledPlugin = new Set(rootStore.get(enabledPluginAtom));
enabledPlugin.forEach(pluginName => {
if (!enabledPluginSet.has(pluginName)) {
added.add(pluginName);
}
export async function bootstrapPluginSystem(
rootStore: ReturnType<typeof getCurrentStore>
) {
const { evaluatePluginEntry, setupPluginCode } = createSetup(rootStore);
rootStore.sub(enabledPluginAtom, () => {
const added = new Set<string>();
const removed = new Set<string>();
const enabledPlugin = new Set(rootStore.get(enabledPluginAtom));
enabledPlugin.forEach(pluginName => {
if (!enabledPluginSet.has(pluginName)) {
added.add(pluginName);
}
});
enabledPluginSet.forEach(pluginName => {
if (!enabledPlugin.has(pluginName)) {
removed.add(pluginName);
}
});
// update plugins
enabledPluginSet.clear();
enabledPlugin.forEach(pluginName => {
enabledPluginSet.add(pluginName);
});
added.forEach(pluginName => {
evaluatePluginEntry(pluginName);
});
removed.forEach(pluginName => {
invokeCleanup(pluginName);
});
});
enabledPluginSet.forEach(pluginName => {
if (!enabledPlugin.has(pluginName)) {
removed.add(pluginName);
}
});
// update plugins
enabledPluginSet.clear();
enabledPlugin.forEach(pluginName => {
enabledPluginSet.add(pluginName);
});
added.forEach(pluginName => {
evaluatePluginEntry(pluginName);
});
removed.forEach(pluginName => {
invokeCleanup(pluginName);
});
});
const enabledPluginSet = new Set(rootStore.get(enabledPluginAtom));
const loadedAssets = new Set<string>();
const enabledPluginSet = new Set(rootStore.get(enabledPluginAtom));
const loadedAssets = new Set<string>();
// we will load all plugins in parallel from builtinPlugins
export const pluginRegisterPromise = Promise.all(
[...builtinPluginPaths].map(url => {
return fetch(`${url}/package.json`)
.then(async res => {
const packageJson = (await res.json()) as z.infer<
typeof packageJsonOutputSchema
>;
packageJsonOutputSchema.parse(packageJson);
const {
name: pluginName,
affinePlugin: {
release,
entry: { core },
assets,
},
} = packageJson;
rootStore.set(pluginPackageJson, json => [...json, packageJson]);
logger.debug(`registering plugin ${pluginName}`);
logger.debug(`package.json: ${packageJson}`);
if (!release && !runtimeConfig.enablePlugin) {
return Promise.resolve();
}
const baseURL = url;
const entryURL = `${baseURL}/${core}`;
rootStore.set(loadedPluginNameAtom, prev => [...prev, pluginName]);
await setupPluginCode(baseURL, pluginName, core);
console.log(`prepareImports for ${pluginName} done`);
await fetch(entryURL).then(async () => {
if (assets.length > 0) {
await Promise.all(
assets.map(async (asset: string) => {
const loadedAssetName = `${pluginName}_${asset}`;
// todo(himself65): add assets into shadow dom
if (loadedAssets.has(loadedAssetName)) {
return Promise.resolve();
}
if (asset.endsWith('.css')) {
loadedAssets.add(loadedAssetName);
const res = await fetch(`${baseURL}/${asset}`);
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', pluginName);
style.textContent = text;
document.head.appendChild(style);
});
// we will load all plugins in parallel from builtinPlugins
return Promise.all(
[...builtinPluginPaths].map(url => {
return fetch(`${url}/package.json`)
.then(async res => {
const packageJson = (await res.json()) as z.infer<
typeof packageJsonOutputSchema
>;
packageJsonOutputSchema.parse(packageJson);
const {
name: pluginName,
affinePlugin: {
release,
entry: { core },
assets,
},
} = packageJson;
rootStore.set(pluginPackageJson, json => [...json, packageJson]);
logger.debug(`registering plugin ${pluginName}`);
logger.debug(`package.json: ${packageJson}`);
if (!release && !runtimeConfig.enablePlugin) {
return Promise.resolve();
}
const baseURL = url;
const entryURL = `${baseURL}/${core}`;
rootStore.set(loadedPluginNameAtom, prev => [...prev, pluginName]);
await setupPluginCode(baseURL, pluginName, core);
console.log(`prepareImports for ${pluginName} done`);
await fetch(entryURL).then(async () => {
if (assets.length > 0) {
await Promise.all(
assets.map(async (asset: string) => {
const loadedAssetName = `${pluginName}_${asset}`;
// todo(himself65): add assets into shadow dom
if (loadedAssets.has(loadedAssetName)) {
return Promise.resolve();
}
return null;
} else {
return Promise.resolve();
}
})
);
}
if (!enabledPluginSet.has(pluginName)) {
logger.debug(`plugin ${pluginName} is not enabled`);
} else {
logger.debug(`plugin ${pluginName} is enabled`);
evaluatePluginEntry(pluginName);
}
if (asset.endsWith('.css')) {
loadedAssets.add(loadedAssetName);
const res = await fetch(`${baseURL}/${asset}`);
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', pluginName);
style.textContent = text;
document.head.appendChild(style);
});
}
return null;
} else {
return Promise.resolve();
}
})
);
}
if (!enabledPluginSet.has(pluginName)) {
logger.debug(`plugin ${pluginName} is not enabled`);
} else {
logger.debug(`plugin ${pluginName} is enabled`);
evaluatePluginEntry(pluginName);
}
});
})
.catch(e => {
console.error(`error when fetch plugin from ${url}`, e);
});
})
.catch(e => {
console.error(`error when fetch plugin from ${url}`, e);
});
})
).then(() => {
console.info('All plugins loaded');
});
})
).then(() => {
console.info('All plugins loaded');
});
}

View File

@@ -21,7 +21,7 @@ import {
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/infra/atom';
import type { createStore } from 'jotai/vanilla';
import { WorkspaceAdapters } from '../adapters/workspace';
@@ -143,7 +143,7 @@ async function tryMigration() {
}
}
function createFirstAppData() {
function createFirstAppData(store: ReturnType<typeof createStore>) {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
@@ -166,18 +166,11 @@ function createFirstAppData() {
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result);
store.set(rootWorkspacesMetadataAtom, result);
}
let isSetup = false;
export async function setup() {
if (isSetup) {
console.warn('already setup');
return;
}
isSetup = true;
rootStore.set(
export async function setup(store: ReturnType<typeof createStore>) {
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
@@ -188,8 +181,8 @@ export async function setup() {
console.log('setup global');
setupGlobal();
createFirstAppData();
createFirstAppData(store);
await tryMigration();
await rootStore.get(rootWorkspacesMetadataAtom);
await store.get(rootWorkspacesMetadataAtom);
console.log('setup done');
}

View File

@@ -1,39 +0,0 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import { useCallback } from 'react';
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
const blockSuiteWorkspace = getOrCreateWorkspace(
'test',
WorkspaceFlavour.LOCAL
);
const page = blockSuiteWorkspace.createPage({ id: 'page0' });
const Editor = () => {
const onLoad = useCallback((page: Page, editor: EditorContainer) => {
// @ts-expect-error
globalThis.page = page;
// @ts-expect-error
globalThis.editor = editor;
return () => void 0;
}, []);
if (!page) {
return <>loading...</>;
}
return (
<BlockSuiteEditor
page={page}
mode="page"
onInit={initEmptyPage}
onLoad={onLoad}
/>
);
};
export default Editor;

View File

@@ -8,7 +8,7 @@ import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
rootStore,
getCurrentStore,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
@@ -102,7 +102,7 @@ export class AffineErrorBoundary extends Component<
return (
<>
{errorDetail}
<Provider key="JotaiProvider" store={rootStore}>
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo />
</Provider>
</>

View File

@@ -70,7 +70,7 @@ const NameWorkspaceContent = ({
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
window.setTimeout(() => ref.focus(), 0);
}
}}
data-testid="create-workspace-input"

View File

@@ -82,7 +82,7 @@ export const WorkspaceDeleteModal = ({
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
window.setTimeout(() => ref.focus(), 0);
}
}}
onChange={setDeleteStr}

View File

@@ -144,7 +144,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
{t['com.affine.header.option.add-tag']()}
</MenuItem> */}
<Divider />
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
<MenuItem
icon={<DuplicateIcon />}
data-testid="editor-option-menu-duplicate"
@@ -162,7 +162,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
{t['Import']()}
</MenuItem>
<Export />
<Divider />
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {

View File

@@ -1,4 +1,6 @@
import { toast } from '@affine/component';
import { initEmptyPage } from '@affine/env/blocksuite';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
@@ -8,7 +10,7 @@ import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
const { openPage } = useNavigateHelper();
const { openPage, jumpToSubPath } = useNavigateHelper();
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const pageSettings = useAtomValue(pageSettingsAtom);
const isPreferredEdgeless = useCallback(
@@ -20,9 +22,7 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
(id?: string, mode?: 'page' | 'edgeless') => {
const page = createPage(id);
initEmptyPage(page); // we don't need to wait it to be loaded right?
if (mode) {
setPageMode(page.id, mode);
}
setPageMode(page.id, mode || 'page');
openPage(blockSuiteWorkspace.id, page.id);
},
[blockSuiteWorkspace.id, createPage, openPage, setPageMode]
@@ -35,8 +35,25 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
);
const importFileAndOpen = useCallback(async () => {
const { showImportModal } = await import('@blocksuite/blocks');
showImportModal({ workspace: blockSuiteWorkspace });
}, [blockSuiteWorkspace]);
const onSuccess = (pageIds: string[], isWorkspaceFile: boolean) => {
toast(
`Successfully imported ${pageIds.length} Page${
pageIds.length > 1 ? 's' : ''
}.`
);
if (isWorkspaceFile) {
jumpToSubPath(blockSuiteWorkspace.id, WorkspaceSubPath.ALL);
return;
}
if (pageIds.length === 0) {
return;
}
const pageId = pageIds[0];
openPage(blockSuiteWorkspace.id, pageId);
};
showImportModal({ workspace: blockSuiteWorkspace, onSuccess });
}, [blockSuiteWorkspace, openPage, jumpToSubPath]);
return {
createPage: createPageAndOpen,
createEdgeless: createEdgelessAndOpen,

View File

@@ -13,7 +13,7 @@ import {
pluginEditorAtom,
pluginWindowAtom,
} from '@toeverything/infra/__internal__/plugin';
import { contentLayoutAtom, rootStore } from '@toeverything/infra/atom';
import { contentLayoutAtom, getCurrentStore } from '@toeverything/infra/atom';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties, ReactElement } from 'react';
@@ -103,9 +103,10 @@ const EditorWrapper = memo(function EditorWrapper({
if (onLoad) {
dispose = onLoad(page, editor);
}
const rootStore = getCurrentStore();
const editorItems = rootStore.get(pluginEditorAtom);
let disposes: (() => void)[] = [];
const renderTimeout = setTimeout(() => {
const renderTimeout = window.setTimeout(() => {
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
@@ -122,7 +123,7 @@ const EditorWrapper = memo(function EditorWrapper({
return () => {
dispose();
clearTimeout(renderTimeout);
setTimeout(() => {
window.setTimeout(() => {
disposes.forEach(dispose => dispose());
});
};
@@ -164,7 +165,7 @@ const PluginContentAdapter = memo<PluginContentAdapterProps>(
};
const dispose = addCleanup(pluginName, cl);
abortController.signal.addEventListener('abort', () => {
setTimeout(() => {
window.setTimeout(() => {
dispose();
cl();
});

View File

@@ -1,6 +1,5 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageBlockModel } from '@blocksuite/blocks';
import { assertEquals } from '@blocksuite/global/utils';
import { PlusIcon } from '@blocksuite/icons';
import { nanoid } from '@blocksuite/store';
@@ -39,15 +38,7 @@ export const Footer = ({
const id = nanoid();
const page = createPage(id);
assertEquals(page.id, id);
await initEmptyPage(page);
const block = page.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
} else {
console.warn('No page block found');
}
await initEmptyPage(page, query);
blockSuiteWorkspace.setPageMeta(page.id, {
title: query,
});

View File

@@ -1,7 +1,7 @@
import { Modal, ModalWrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Command } from 'cmdk';
import { startTransition } from 'react';
import { startTransition, Suspense } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AllWorkspace } from '../../../shared';
@@ -13,6 +13,7 @@ import {
StyledModalDivider,
StyledModalFooter,
StyledModalHeader,
StyledNotFound,
StyledShortcut,
} from './style';
@@ -41,7 +42,7 @@ export const QuickSearchModal = ({
setOpen(false);
}, [setOpen]);
// Add ‘⌘+K shortcut keys as switches
// Add ‘⌘+K shortcut keys as switches
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
@@ -131,12 +132,20 @@ export const QuickSearchModal = ({
<StyledModalDivider />
<Command.List>
<StyledContent>
<Results
query={query}
onClose={handleClose}
workspace={workspace}
setShowCreatePage={setShowCreatePage}
/>
<Suspense
fallback={
<StyledNotFound>
<span>{t['com.affine.loading']()}</span>
</StyledNotFound>
}
>
<Results
query={query}
onClose={handleClose}
workspace={workspace}
setShowCreatePage={setShowCreatePage}
/>
</Suspense>
</StyledContent>
{showCreatePage ? (
<>

View File

@@ -2,11 +2,13 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { Command } from 'cmdk';
import { useAtomValue } from 'jotai';
import { type Atom, atom, useAtomValue } from 'jotai';
import type { Dispatch, SetStateAction } from 'react';
import { startTransition, useEffect } from 'react';
import { recentPageSettingsAtom } from '../../../atoms';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
@@ -20,6 +22,29 @@ export interface ResultsProps {
onClose: () => void;
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
}
const loadAllPageWeakMap = new WeakMap<Workspace, Atom<Promise<void>>>();
function getLoadAllPage(workspace: Workspace) {
if (loadAllPageWeakMap.has(workspace)) {
return loadAllPageWeakMap.get(workspace) as Atom<Promise<void>>;
} else {
const aAtom = atom(async () => {
// fixme: we have to load all pages here and re-index them
// there might have performance issue
await Promise.all(
[...workspace.pages.values()].map(page =>
page.waitForLoaded().then(() => {
workspace.indexer.search.refreshPageIndex(page.id, page.spaceDoc);
})
)
);
});
loadAllPageWeakMap.set(workspace, aAtom);
return aAtom;
}
}
export const Results = ({
query,
workspace,
@@ -31,14 +56,20 @@ export const Results = ({
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
assertExists(blockSuiteWorkspace.id);
const list = useSwitchToConfig(workspace.id);
useAtomValue(getLoadAllPage(blockSuiteWorkspace));
const recentPageSetting = useAtomValue(recentPageSettingsAtom);
const t = useAFFiNEI18N();
const { jumpToPage, jumpToSubPath } = useNavigateHelper();
const results = blockSuiteWorkspace.search({ query });
// remove `space:` prefix
const pageIds = [...results.values()].map(id => id.slice(6));
const pageIds = [...blockSuiteWorkspace.search({ query }).values()].map(
id => {
if (id.startsWith('space:')) {
return id.slice(6);
} else {
return id;
}
}
);
const resultsPageMeta = pageList.filter(
page => pageIds.indexOf(page.id) > -1 && !page.trash
@@ -53,7 +84,11 @@ export const Results = ({
}
});
setShowCreatePage(resultsPageMeta.length === 0);
useEffect(() => {
startTransition(() => {
setShowCreatePage(resultsPageMeta.length === 0);
});
}, [resultsPageMeta.length, setShowCreatePage]);
if (!query) {
return (
@@ -117,7 +152,12 @@ export const Results = ({
return (
<StyledNotFound>
<span>{t['Find 0 result']()}</span>
<image href="/imgs/no-result.svg" width={200} height={200} />
<img
alt="no result"
src="/imgs/no-result.svg"
width={200}
height={200}
/>
</StyledNotFound>
);
}

View File

@@ -11,6 +11,7 @@ import { useCallback, useState } from 'react';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { toast } from '../../../utils';
import { buttonContainer, group } from './styles.css';
export const TrashButtonGroup = () => {
@@ -37,6 +38,7 @@ export const TrashButtonGroup = () => {
type="primary"
onClick={() => {
restoreFromTrash(pageId);
toast(t['restored']({ title: pageMeta.title || 'Untitled' }));
}}
size="large"
>
@@ -63,7 +65,8 @@ export const TrashButtonGroup = () => {
onConfirm={useCallback(() => {
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
blockSuiteWorkspace.removePage(pageId);
}, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id])}
toast(t['Permanently deleted']());
}, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id, t])}
onCancel={() => {
setOpen(false);
}}

View File

@@ -1,4 +1,4 @@
import { Menu, MenuItem, Tooltip } from '@affine/component';
import { Menu, MenuItem } from '@affine/component';
import { WorkspaceList } from '@affine/component/workspace-list';
import type {
AffineCloudWorkspace,
@@ -9,7 +9,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
CloudWorkspaceIcon,
HelpIcon,
ImportIcon,
MoreHorizontalIcon,
PlusIcon,
@@ -27,7 +26,6 @@ import {
StyledCreateWorkspaceCardPill,
StyledCreateWorkspaceCardPillContent,
StyledCreateWorkspaceCardPillIcon,
StyledHelperContainer,
StyledImportWorkspaceCardPill,
StyledModalBody,
StyledModalContent,
@@ -61,14 +59,14 @@ const AccountMenu = () => {
return (
<div>
<div>Unlimted</div>
<Divider></Divider>
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
{t['com.affine.workspace.cloud.join']()}
</MenuItem>
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
{t['com.affine.workspace.cloud.account.settings']()}
</MenuItem>
<Divider></Divider>
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
@@ -93,15 +91,6 @@ const CloudWorkSpaceList = ({
<StyledModalTitle>
{t['com.affine.workspace.cloud.sync']()}
</StyledModalTitle>
<Tooltip
content={t['Workspace description']()}
placement="top-start"
disablePortal={true}
>
<StyledHelperContainer>
<HelpIcon />
</StyledHelperContainer>
</Tooltip>
</StyledModalHeaderLeft>
<StyledOperationWrapper>
@@ -140,7 +129,7 @@ const CloudWorkSpaceList = ({
[onMoveWorkspace]
)}
/>
<Divider />
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
</StyledModalContent>
</>
);
@@ -211,7 +200,7 @@ export const WorkspaceListModal = ({
</StyledCreateWorkspaceCardPillContent>
</MenuItem>
</StyledSignInCardPill>
<Divider />
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
</StyledModalHeaderContent>
<StyledModalBody>
{isLoggedIn ? (
@@ -230,15 +219,6 @@ export const WorkspaceListModal = ({
) : null}
<StyledModalHeader>
<StyledModalTitle>{t['Local Workspace']()}</StyledModalTitle>
<Tooltip
content={t['Workspace description']()}
placement="top-start"
disablePortal={true}
>
<StyledHelperContainer>
<HelpIcon />
</StyledHelperContainer>
</Tooltip>
</StyledModalHeader>
<StyledModalContent>
<WorkspaceList

View File

@@ -29,7 +29,11 @@ export const ReferencePage = ({
const icon = setting?.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
const references = useBlockSuitePageReferences(workspace, pageId);
const referencesToShow = useMemo(() => {
return [...new Set(references.filter(ref => !metaMapping[ref]?.trash))];
return [
...new Set(
references.filter(ref => metaMapping[ref] && !metaMapping[ref]?.trash)
),
];
}, [references, metaMapping]);
const [collapsed, setCollapsed] = useState(true);
const collapsible = referencesToShow.length > 0;

View File

@@ -5,7 +5,7 @@ import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { nanoid } from '@blocksuite/store';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { rootStore } from '@toeverything/infra/atom';
import { getCurrentStore } from '@toeverything/infra/atom';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
@@ -55,7 +55,7 @@ export function useAppHelper() {
WorkspaceFlavour.LOCAL
);
await buildShowcaseWorkspace(blockSuiteWorkspace, {
store: rootStore,
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},

View File

@@ -1,11 +1,18 @@
import { WorkspaceFallback } from '@affine/component/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { getCurrentStore } from '@toeverything/infra/atom';
import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { bootstrapPluginSystem } from './bootstrap/register-plugins';
async function main() {
const { setup } = await import('./bootstrap/setup');
await setup();
const rootStore = getCurrentStore();
await setup(rootStore);
bootstrapPluginSystem(rootStore).catch(err => {
console.error('Failed to bootstrap plugin system', err);
});
const { App } = await import('./app');
const root = document.getElementById('app');
assertExists(root);

View File

@@ -1,5 +1,8 @@
import { Content, displayFlex } from '@affine/component';
import { appSidebarResizingAtom } from '@affine/component/app-sidebar';
import {
AppSidebarFallback,
appSidebarResizingAtom,
} from '@affine/component/app-sidebar';
import { BlockHubWrapper } from '@affine/component/block-hub';
import { NotificationCenter } from '@affine/component/notification-center';
import type { DraggableTitleCellData } from '@affine/component/page-list';
@@ -135,11 +138,9 @@ export const WorkspaceLayout = function WorkspacesSuspense({
</CurrentWorkspaceContext>
</Suspense>
<CurrentWorkspaceContext>
<Suspense fallback={<WorkspaceFallback />}>
<Provider>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Provider>
</Suspense>
<Provider>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Provider>
</CurrentWorkspaceContext>
</>
);
@@ -231,30 +232,34 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
onDragEnd={handleDragEnd}
>
<AppContainer resizing={resizing}>
<RootAppSidebar
isPublicWorkspace={false}
onOpenQuickSearchModal={handleOpenQuickSearchModal}
onOpenSettingModal={handleOpenSettingModal}
currentWorkspace={currentWorkspace}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback(
(pageId: string) => {
assertExists(currentWorkspace);
return openPage(currentWorkspace.id, pageId);
},
[currentWorkspace, openPage]
)}
createPage={handleCreatePage}
currentPath={location.pathname.split('?')[0]}
paths={pathGenerator}
/>
<MainContainer padding={appSetting.clientBorder}>
{children}
<ToolContainer>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
<HelpIsland showList={pageId ? undefined : showList} />
</ToolContainer>
</MainContainer>
<Suspense fallback={<AppSidebarFallback />}>
<RootAppSidebar
isPublicWorkspace={false}
onOpenQuickSearchModal={handleOpenQuickSearchModal}
onOpenSettingModal={handleOpenSettingModal}
currentWorkspace={currentWorkspace}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback(
(pageId: string) => {
assertExists(currentWorkspace);
return openPage(currentWorkspace.id, pageId);
},
[currentWorkspace, openPage]
)}
createPage={handleCreatePage}
currentPath={location.pathname.split('?')[0]}
paths={pathGenerator}
/>
</Suspense>
<Suspense fallback={<MainContainer />}>
<MainContainer padding={appSetting.clientBorder}>
{children}
<ToolContainer>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
<HelpIsland showList={pageId ? undefined : showList} />
</ToolContainer>
</MainContainer>
</Suspense>
</AppContainer>
<PageListTitleCellDragOverlay />
</DndContext>

View File

@@ -2,7 +2,7 @@ import { DebugLogger } from '@affine/debug';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { rootStore } from '@toeverything/infra/atom';
import { getCurrentStore } from '@toeverything/infra/atom';
import { lazy } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
@@ -16,6 +16,7 @@ const AllWorkspaceModals = lazy(() =>
const logger = new DebugLogger('index-page');
export const loader: LoaderFunction = async () => {
const rootStore = getCurrentStore();
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id');

View File

@@ -3,7 +3,7 @@ import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { rootStore } from '@toeverything/infra/atom';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
@@ -13,6 +13,7 @@ import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
export const loader: LoaderFunction = async args => {
const rootStore = getCurrentStore();
const workspaceId = args.params.workspaceId;
assertExists(workspaceId);
const workspaceAtom = getActiveBlockSuiteWorkspaceAtom(workspaceId);

View File

@@ -12,14 +12,16 @@ import {
currentPageIdAtom,
currentWorkspaceAtom,
currentWorkspaceIdAtom,
rootStore,
getCurrentStore,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useAtomValue, useSetAtom } from 'jotai';
import { type ReactElement, useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { setPageModeAtom } from '../../atoms';
import { currentModeAtom } from '../../atoms/mode';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
@@ -31,8 +33,12 @@ const DetailPageImpl = (): ReactElement => {
assertExists(currentPageId);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const collectionManager = useCollectionManager(currentWorkspace.id);
const mode = useAtomValue(currentModeAtom);
const setPageMode = useSetAtom(setPageModeAtom);
const onLoad = useCallback(
(_: Page, editor: EditorContainer) => {
setPageMode(currentPageId, mode);
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId);
});
@@ -49,9 +55,12 @@ const DetailPageImpl = (): ReactElement => {
[
blockSuiteWorkspace.id,
collectionManager,
currentPageId,
currentWorkspace.id,
jumpToSubPath,
mode,
openPage,
setPageMode,
]
);
@@ -87,6 +96,7 @@ export const DetailPage = (): ReactElement => {
};
export const loader: LoaderFunction = async args => {
const rootStore = getCurrentStore();
rootStore.set(contentLayoutAtom, 'editor');
if (args.params.workspaceId) {
localStorage.setItem('last_workspace_id', args.params.workspaceId);

View File

@@ -2,7 +2,7 @@ import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
rootStore,
getCurrentStore,
} from '@toeverything/infra/atom';
import type { ReactElement } from 'react';
import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
@@ -10,6 +10,7 @@ import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
export const loader: LoaderFunction = async args => {
const rootStore = getCurrentStore();
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
if (!meta.some(({ id }) => id === args.params.workspaceId)) {
return redirect('/404');

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"type": "module",
"private": true,
"scripts": {
@@ -10,12 +10,12 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"express": "^4.18.2",
"jotai": "^2.3.1",
"react": "18.3.0-canary-7118f5dd7-20230705",

View File

@@ -1,37 +1,20 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { z } = require('zod');
const {
utils: { fromBuildIdentifier },
} = require('@electron-forge/core');
const path = require('node:path');
const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
const buildType = ReleaseTypeSchema.parse(envBuildType);
const stableBuild = buildType === 'stable';
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
const icoPath = !stableBuild
? `./resources/icons/icon_${buildType}.ico`
: './resources/icons/icon.ico';
const icnsPath = !stableBuild
? `./resources/icons/icon_${buildType}.icns`
: './resources/icons/icon.icns';
const arch =
process.argv.indexOf('--arch') > 0
? process.argv[process.argv.indexOf('--arch') + 1]
: process.arch;
const platform =
process.argv.indexOf('--platform') > 0
? process.argv[process.argv.indexOf('--platform') + 1]
: process.platform;
const windowsIconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`;
const {
arch,
buildType,
icnsPath,
icoPath,
platform,
productName,
iconUrl,
} = require('./scripts/make-env');
const makers = [
!process.env.SKIP_BUNDLE &&
@@ -84,7 +67,7 @@ const makers = [
config: {
name: productName,
setupIcon: icoPath,
iconUrl: windowsIconUrl,
iconUrl: iconUrl,
loadingGif: './resources/icons/affine_installing.gif',
},
},

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.8.0-beta.0",
"version": "0.8.0",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -17,7 +17,8 @@
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts"
"test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts",
"make-squirrel": "yarn ts-node-esm -T scripts/make-squirrel.mts"
},
"config": {
"forge": "./forge.config.js"
@@ -28,17 +29,17 @@
"@affine/env": "workspace:*",
"@affine/native": "workspace:*",
"@affine/sdk": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@electron-forge/cli": "^6.3.0",
"@electron-forge/core": "^6.3.0",
"@electron-forge/core-utils": "^6.3.0",
"@electron-forge/maker-deb": "^6.3.0",
"@electron-forge/maker-squirrel": "^6.3.0",
"@electron-forge/maker-zip": "^6.3.0",
"@electron-forge/shared-types": "^6.3.0",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"@electron-forge/cli": "^6.4.0",
"@electron-forge/core": "^6.4.0",
"@electron-forge/core-utils": "^6.4.0",
"@electron-forge/maker-deb": "^6.4.0",
"@electron-forge/maker-squirrel": "^6.4.0",
"@electron-forge/maker-zip": "^6.4.0",
"@electron-forge/shared-types": "^6.4.0",
"@electron/remote": "2.0.10",
"@reforged/maker-appimage": "^3.3.1",
"@types/fs-extra": "^11.0.1",
@@ -50,6 +51,7 @@
"electron-window-state": "^5.0.3",
"esbuild": "^0.19.2",
"fs-extra": "^11.1.1",
"glob": "^10.3.3",
"jotai": "^2.3.1",
"ts-node": "^10.9.1",
"undici": "^5.23.0",

View File

@@ -1,8 +1,8 @@
// do not run in your local machine
/* eslint-disable */
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
/* eslint-enable */
const yml = {
@@ -10,18 +10,10 @@ const yml = {
files: [],
};
let fileList = [];
// TODO: maybe add `beta` and `stable`
const BUILD_TYPE = process.env.BUILD_TYPE || 'canary';
const generateYml = async () => {
fileList = [
`affine-${BUILD_TYPE}-macos-arm64.dmg`,
`affine-${BUILD_TYPE}-macos-arm64.zip`,
`affine-${BUILD_TYPE}-macos-x64.zip`,
`affine-${BUILD_TYPE}-macos-x64.dmg`,
];
fileList.forEach(fileName => {
const generateYml = platform => {
const regex = new RegExp(`^affine-.*-${platform}-.*.(exe|zip|dmg|AppImage)$`);
const files = fs.readdirSync(__dirname).filter(file => regex.test(file));
files.forEach(fileName => {
const filePath = path.join(__dirname, './', fileName);
try {
const fileData = fs.readFileSync(filePath);
@@ -58,6 +50,9 @@ const generateYml = async () => {
`sha512: ${yml.sha512}\n` +
`releaseDate: ${yml.releaseDate}\n`;
fs.writeFileSync(`./latest-mac.yml`, ymlStr);
const fileName = platform === 'windows' ? 'latest.yml' : 'latest-mac.yml';
fs.writeFileSync(fileName, ymlStr);
};
generateYml();
generateYml('windows');
generateYml('macos');

View File

@@ -1,5 +1,5 @@
import { fileURLToPath } from 'node:url';
import { readdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const outputRoot = fileURLToPath(
new URL(

View File

@@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { z } = require('zod');
const path = require('node:path');
const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
const ROOT = path.resolve(__dirname, '..');
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
const buildType = ReleaseTypeSchema.parse(envBuildType);
const stableBuild = buildType === 'stable';
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
const icoPath = path.join(
ROOT,
!stableBuild
? `./resources/icons/icon_${buildType}.ico`
: './resources/icons/icon.ico'
);
const icnsPath = path.join(
ROOT,
!stableBuild
? `./resources/icons/icon_${buildType}.icns`
: './resources/icons/icon.icns'
);
const iconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`;
const arch =
process.argv.indexOf('--arch') > 0
? process.argv[process.argv.indexOf('--arch') + 1]
: process.arch;
const platform =
process.argv.indexOf('--platform') > 0
? process.argv[process.argv.indexOf('--platform') + 1]
: process.platform;
module.exports = {
ROOT,
buildType,
productName,
icoPath,
icnsPath,
iconUrl,
arch,
platform,
stableBuild,
};

View File

@@ -0,0 +1,84 @@
import type { Options as ElectronWinstallerOptions } from 'electron-winstaller';
import { convertVersion, createWindowsInstaller } from 'electron-winstaller';
import fs from 'fs-extra';
import path from 'path';
import {
arch,
buildType,
iconUrl,
icoPath,
platform,
productName,
ROOT,
} from './make-env';
async function ensureDirectory(dir: string) {
if (await fs.pathExists(dir)) {
await fs.remove(dir);
}
return fs.mkdirs(dir);
}
// taking from https://github.com/electron/forge/blob/main/packages/maker/squirrel/src/MakerSquirrel.ts
// it was for forge's maker, but can be used standalone as well
async function make() {
const appName = productName;
const makeDir = path.resolve(ROOT, 'out', buildType, 'make');
const outPath = path.resolve(makeDir, `squirrel.windows/${arch}`);
const appDirectory = path.resolve(
ROOT,
'out',
buildType,
`${appName}-${platform}-${arch}`
);
await ensureDirectory(outPath);
const packageJSON = await fs.readJson(path.resolve(ROOT, 'package.json'));
const winstallerConfig: ElectronWinstallerOptions = {
name: appName,
title: appName,
noMsi: true,
exe: `${appName}.exe`,
setupExe: `${appName}-${packageJSON.version} Setup.exe`,
version: packageJSON.version,
appDirectory: appDirectory,
outputDirectory: outPath,
iconUrl: iconUrl,
setupIcon: icoPath,
loadingGif: path.resolve(ROOT, './resources/icons/affine_installing.gif'),
};
await createWindowsInstaller(winstallerConfig);
const nupkgVersion = convertVersion(packageJSON.version);
const artifacts = [
path.resolve(outPath, 'RELEASES'),
path.resolve(outPath, winstallerConfig.setupExe || `${appName}Setup.exe`),
path.resolve(
outPath,
`${winstallerConfig.name}-${nupkgVersion}-full.nupkg`
),
];
const deltaPath = path.resolve(
outPath,
`${winstallerConfig.name}-${nupkgVersion}-delta.nupkg`
);
if (
(winstallerConfig.remoteReleases && !winstallerConfig.noDelta) ||
(await fs.pathExists(deltaPath))
) {
artifacts.push(deltaPath);
}
const msiPath = path.resolve(
outPath,
winstallerConfig.setupMsi || `${appName}Setup.msi`
);
if (!winstallerConfig.noMsi && (await fs.pathExists(msiPath))) {
artifacts.push(msiPath);
}
console.log('making squirrel.windows done:', artifacts);
return artifacts;
}
make();

View File

@@ -98,6 +98,13 @@ async function createWindow() {
// TODO: gracefully close the app, for example, ask user to save unsaved changes
});
browserWindow.on('leave-full-screen', () => {
// FIXME: workaround for theme bug in full screen mode
const size = browserWindow.getSize();
browserWindow.setSize(size[0] + 1, size[1] + 1);
browserWindow.setSize(size[0], size[1]);
});
/**
* URL for main window.
*/

View File

@@ -2,7 +2,7 @@ import { app } from 'electron';
import { autoUpdater } from 'electron-updater';
import { z } from 'zod';
import { isMacOS } from '../../shared/utils';
import { isMacOS, isWindows } from '../../shared/utils';
import { logger } from '../logger';
import { updaterSubjects } from './event';
@@ -40,8 +40,8 @@ export const registerUpdater = async () => {
return;
}
// TODO: support auto update on windows and linux
const allowAutoUpdate = isMacOS();
// TODO: support auto update on linux
const allowAutoUpdate = isMacOS() || isWindows();
autoUpdater.logger = logger;
autoUpdater.autoDownload = false;

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/prototype",
"private": true,
"version": "0.8.0-beta.0",
"version": "0.8.0",
"type": "module",
"scripts": {
"dev": "vite --host --port 3003",
@@ -18,13 +18,13 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/icons": "^2.1.31",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"@toeverything/hooks": "workspace:*",
"@toeverything/y-indexeddb": "workspace:*",
"react": "^18.2.0",

View File

@@ -25,11 +25,11 @@ const App = () => {
<button
data-testid="start-button"
onClick={useCallback(() => {
disposeRef.current = setInterval(() => {
disposeRef.current = window.setInterval(() => {
const counter = counterRef.current;
map.set('counter', counter + 1);
counterRef.current = counter + 1;
}, 0) as any;
}, 0);
}, [])}
>
start writing

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.8.0-beta.0",
"version": "0.8.0",
"description": "Affine Node.js server",
"type": "module",
"bin": {

View File

@@ -5,14 +5,16 @@ import '@toeverything/components/style.css';
import { createI18n } from '@affine/i18n';
import { ThemeProvider, useTheme } from 'next-themes';
import { useDarkMode } from 'storybook-dark-mode';
import { setup } from '@affine/core/bootstrap/setup';
import { AffineContext } from '@affine/component/context';
import { use } from 'foxact/use';
import useSWR from 'swr';
import type { Decorator } from '@storybook/react';
import { createStore } from 'jotai/vanilla';
import { setup } from '@affine/core/bootstrap/setup';
import { _setCurrentStore } from '@toeverything/infra/atom';
import { bootstrapPluginSystem } from '@affine/core/bootstrap/register-plugins';
import { setupGlobal } from '@affine/env/global';
const setupPromise = setup();
setupGlobal();
export const parameters = {
backgrounds: { disable: true },
actions: { argTypesRegex: '^on[A-Z].*' },
@@ -50,11 +52,30 @@ const ThemeChange = () => {
return null;
};
const storeMap = new Map<string, ReturnType<typeof createStore>>();
const withContextDecorator: Decorator = (Story, context) => {
use(setupPromise);
const { data: store } = useSWR(
context.id,
async () => {
if (storeMap.has(context.id)) {
return storeMap.get(context.id);
}
localStorage.clear();
const store = createStore();
_setCurrentStore(store);
await setup(store);
await bootstrapPluginSystem(store);
storeMap.set(context.id, store);
return store;
},
{
suspense: true,
}
);
return (
<ThemeProvider>
<AffineContext>
<AffineContext store={store}>
<ThemeChange />
<Story {...context} />
</AffineContext>

View File

@@ -16,7 +16,7 @@
"@storybook/addon-storysource": "^7.3.1",
"@storybook/blocks": "^7.3.1",
"@storybook/builder-vite": "^7.3.1",
"@storybook/jest": "^0.1.0",
"@storybook/jest": "^0.2.1",
"@storybook/react": "^7.3.1",
"@storybook/react-vite": "^7.3.1",
"@storybook/test-runner": "^0.13.0",
@@ -31,13 +31,13 @@
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/icons": "^2.1.31",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"chromatic": "^6.22.0",
"react": "18.2.0",
"react-dom": "18.2.0",
@@ -52,5 +52,5 @@
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -1,9 +1,7 @@
import { pluginRegisterPromise } from '@affine/core/bootstrap/register-plugins';
import { routes } from '@affine/core/router';
import { assertExists } from '@blocksuite/global/utils';
import type { Decorator, StoryFn } from '@storybook/react';
import { userEvent, waitFor } from '@storybook/testing-library';
import { use } from 'foxact/use';
import type { StoryFn } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { Outlet, useLocation } from 'react-router-dom';
import {
reactRouterOutlets,
@@ -11,11 +9,6 @@ import {
withRouter,
} from 'storybook-addon-react-router-v6';
const withCleanLocalStorage: Decorator = (Story, context) => {
localStorage.clear();
return <Story {...context} />;
};
const FakeApp = () => {
const location = useLocation();
// fixme: `key` is a hack to force the storybook to re-render the outlet
@@ -30,10 +23,9 @@ export default {
};
export const Index: StoryFn = () => {
use(pluginRegisterPromise);
return <FakeApp />;
};
Index.decorators = [withRouter, withCleanLocalStorage];
Index.decorators = [withRouter];
Index.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
@@ -43,23 +35,28 @@ Index.parameters = {
export const SettingPage: StoryFn = () => {
return <FakeApp />;
};
SettingPage.play = async ({ canvasElement }) => {
await waitFor(
() => {
assertExists(
canvasElement.querySelector('[data-testid="settings-modal-trigger"]')
);
},
{
timeout: 5000,
}
);
const settingModalBtn = canvasElement.querySelector(
'[data-testid="settings-modal-trigger"]'
) as Element;
await userEvent.click(settingModalBtn);
SettingPage.play = async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await waitFor(async () => {
assertExists(canvasElement.querySelector('v-line'));
});
await step('click setting modal button', async () => {
await userEvent.click(canvas.getByTestId('settings-modal-trigger'));
});
await waitFor(async () => {
assertExists(
document.body.querySelector('[data-testid="language-menu-button"]')
);
});
await step('click language menu button', async () => {
await userEvent.click(
document.body.querySelector(
'[data-testid="language-menu-button"]'
) as HTMLElement
);
});
};
SettingPage.decorators = [withRouter, withCleanLocalStorage];
SettingPage.decorators = [withRouter];
SettingPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
@@ -69,7 +66,7 @@ SettingPage.parameters = {
export const NotFoundPage: StoryFn = () => {
return <FakeApp />;
};
NotFoundPage.decorators = [withRouter, withCleanLocalStorage];
NotFoundPage.decorators = [withRouter];
NotFoundPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
@@ -99,7 +96,7 @@ WorkspaceList.play = async ({ canvasElement }) => {
) as Element;
await userEvent.click(currentWorkspace);
};
WorkspaceList.decorators = [withRouter, withCleanLocalStorage];
WorkspaceList.decorators = [withRouter];
WorkspaceList.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
@@ -108,3 +105,23 @@ WorkspaceList.parameters = {
},
}),
};
export const SearchPage: StoryFn = () => {
return <FakeApp />;
};
SearchPage.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(async () => {
assertExists(canvasElement.querySelector('v-line'));
});
await userEvent.click(canvas.getByTestId('slider-bar-quick-search-button'));
};
SearchPage.decorators = [withRouter];
SearchPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
location: {
path: '/',
},
}),
};

View File

@@ -7,6 +7,7 @@ import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Meta } from '@storybook/react';
import { useCallback } from 'react';
import { createPortal } from 'react-dom';
export default {
@@ -53,7 +54,11 @@ export const Default = () => {
overflow: 'auto',
}}
>
<BlockSuiteEditor mode="page" page={page} onInit={initEmptyPage} />
<BlockSuiteEditor
mode="page"
page={page}
onInit={useCallback(async page => initEmptyPage(page), [])}
/>
{createPortal(
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />,
document.body

View File

@@ -91,7 +91,7 @@ Basic.play = async ({ canvasElement }) => {
expect(button).not.toBeNull();
button.click();
}
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => window.setTimeout(resolve, 100));
{
const button = canvasElement.querySelector(
'[data-testid="share-menu-enable-affine-cloud-button"]'

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"private": true,
"author": "toeverything",
"license": "MPL-2.0",

View File

@@ -7,5 +7,5 @@
"@affine/env": "workspace:*",
"@toeverything/infra": "workspace:*"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -20,5 +20,5 @@
"peerDependencies": {
"ts-node": "*"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -9,6 +9,41 @@ import { config } from 'dotenv';
import { type BuildFlags, projectRoot } from '../config/index.js';
import { watchI18N } from '../util/i18n.js';
const cwd = path.resolve(projectRoot, 'apps', 'core');
const flags: BuildFlags = {
distribution: 'browser',
mode: 'development',
channel: 'canary',
coverage: process.env.COVERAGE === 'true',
localBlockSuite: undefined,
};
if (process.argv.includes('--static')) {
await awaitChildProcess(
spawn(
'node',
[
'--loader',
'ts-node/esm/transpile-only',
'../../node_modules/webpack/bin/webpack.js',
'serve',
'--mode',
'development',
'--env',
'flags=' + Buffer.from(JSON.stringify(flags), 'utf-8').toString('hex'),
].filter((v): v is string => !!v),
{
cwd,
stdio: 'inherit',
shell: true,
env: process.env,
}
)
);
process.exit(0);
}
const files = ['.env', '.env.local'];
for (const file of files) {
@@ -21,16 +56,6 @@ for (const file of files) {
}
}
const cwd = path.resolve(projectRoot, 'apps', 'core');
const flags: BuildFlags = {
distribution: 'browser',
mode: 'development',
channel: 'canary',
coverage: false,
localBlockSuite: undefined,
};
const buildFlags = await p.group(
{
distribution: () =>

View File

@@ -51,12 +51,12 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/icons": "^2.1.31",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"@types/react": "^18.2.20",
"@types/react-datepicker": "^4.15.0",
"@types/react-dnd": "^3.0.2",
@@ -66,5 +66,5 @@
"vite": "^4.4.9",
"yjs": "^13.6.7"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -35,7 +35,7 @@ export type AppSidebarProps = PropsWithChildren<
function useEnableAnimation() {
const [enable, setEnable] = useState(false);
useEffect(() => {
setTimeout(() => {
window.setTimeout(() => {
setEnable(true);
}, 500);
}, []);

View File

@@ -1,20 +1,24 @@
import { ProviderComposer } from '@affine/component/provider-composer';
import { ThemeProvider } from '@affine/component/theme-provider';
import { rootStore } from '@toeverything/infra/atom';
import type { createStore } from 'jotai';
import { Provider } from 'jotai';
import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';
export function AffineContext(props: PropsWithChildren) {
export type AffineContextProps = PropsWithChildren<{
store?: ReturnType<typeof createStore>;
}>;
export function AffineContext(props: AffineContextProps) {
return (
<ProviderComposer
contexts={useMemo(
() =>
[
<Provider key="JotaiProvider" store={rootStore} />,
<Provider key="JotaiProvider" store={props.store} />,
<ThemeProvider key="ThemeProvider" />,
].filter(Boolean),
[]
[props.store]
)}
>
{props.children}

View File

@@ -80,7 +80,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
const [animationKey, setAnimationKey] = useState(0);
const animationRef = useRef<SVGAnimateElement>(null);
const notificationRef = useRef<HTMLLIElement>(null);
const timerIdRef = useRef<NodeJS.Timeout>();
const timerIdRef = useRef<number>();
const isFront = index === 0;
const isVisible = index + 1 <= 3;
const progressDuration = notification.timeout || 3000;
@@ -164,7 +164,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
setHeights(h =>
h.filter(height => height.notificationKey !== notification.key)
);
setTimeout(() => {
window.setTimeout(() => {
if (!notification.key) {
return;
}
@@ -177,7 +177,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
clearTimeout(timerIdRef.current);
}
if (!expand) {
timerIdRef.current = setTimeout(() => {
timerIdRef.current = window.setTimeout(() => {
onClickRemove();
}, duration);
}

View File

@@ -29,7 +29,7 @@ export const DefaultAvatar = ({ name: propsName }: { name: string }) => {
return colorsSchema[index % colorsSchema.length];
}, [name]);
const timer = useRef<ReturnType<typeof setTimeout>>();
const timer = useRef<number>();
const [topColor, middleColor, bottomColor] = colors;
const [isHover, setIsHover] = useState(false);
@@ -38,7 +38,7 @@ export const DefaultAvatar = ({ name: propsName }: { name: string }) => {
<div
className={DefaultAvatarContainerStyle}
onMouseEnter={() => {
timer.current = setTimeout(() => {
timer.current = window.setTimeout(() => {
setIsHover(true);
}, 300);
}}

View File

@@ -97,6 +97,28 @@ export const mainContainerStyle = style({
},
} as ComplexStyleRule);
// These styles override the default styles of the react-resizable-panels
// as the default styles make the overflow part hidden when printing to PDF.
// See https://github.com/toeverything/AFFiNE/pull/3893
globalStyle(`${mainContainerStyle} > div[data-panel-group]`, {
'@media': {
print: {
overflow: 'visible !important',
},
},
});
// These styles override the default styles of the react-resizable-panels
// as the default styles make the overflow part hidden when printing to PDF.
// See https://github.com/toeverything/AFFiNE/pull/3893
globalStyle(`${mainContainerStyle} > div[data-panel-group] > div[data-panel]`, {
'@media': {
print: {
overflow: 'visible !important',
},
},
});
export const toolStyle = style({
position: 'fixed',
right: '30px',

View File

@@ -69,7 +69,7 @@ export const Popper = ({
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.setTimeout(() => {
pointerEnterTimer.current = window.window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
@@ -81,7 +81,7 @@ export const Popper = ({
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.setTimeout(() => {
pointerLeaveTimer.current = window.window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};

View File

@@ -8,5 +8,5 @@
"devDependencies": {
"@types/debug": "^4.1.8"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -5,7 +5,7 @@
"main": "./src/index.ts",
"module": "./src/index.ts",
"devDependencies": {
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "^3.22.1"
@@ -27,5 +27,5 @@
"dependencies": {
"lit": "^2.8.0"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -3,10 +3,10 @@ import type { Page } from '@blocksuite/store';
/**
* @deprecated
*/
export async function initEmptyPage(page: Page) {
export async function initEmptyPage(page: Page, title?: string) {
await page.waitForLoaded();
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
title: new page.Text(title || ''),
});
page.addBlock('affine:surface', {}, pageBlockId);
const noteBlockId = page.addBlock('affine:note', {}, pageBlockId);

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/graphql",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"description": "Autogenerated GraphQL client for affine.pro",
"license": "MPL-2.0",
"type": "module",

View File

@@ -11,12 +11,12 @@
"devDependencies": {
"@affine/env": "workspace:*",
"@affine/y-provider": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly"
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly"
},
"peerDependencies": {
"@affine/y-provider": "workspace:*",
@@ -53,5 +53,5 @@
"optional": true
}
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -1,4 +1,3 @@
import { assertExists } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store';
import { type Atom, atom, useAtomValue } from 'jotai';
@@ -14,7 +13,11 @@ function getPageReferences(page: Page): string[] {
.filter(Boolean);
}
const getPageReferencesAtom = (page: Page) => {
const getPageReferencesAtom = (page: Page | null) => {
if (!page) {
return atom([]);
}
if (!weakMap.has(page)) {
const baseAtom = atom<string[]>(getPageReferences(page));
baseAtom.onMount = set => {
@@ -35,6 +38,5 @@ export function useBlockSuitePageReferences(
pageId: string
): string[] {
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
assertExists(page);
return useAtomValue(getPageReferencesAtom(page));
}

View File

@@ -37,5 +37,5 @@
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -317,6 +317,7 @@
"Zoom to 100%": "Zoom to 100%",
"Zoom to fit": "Zoom to fit",
"all": "all",
"com.affine.loading": "Loading...",
"com.affine.banner.content": "This demo is limited. <1>Download the AFFiNE Client</1> for the latest features and Performance.",
"com.affine.cloudTempDisable.description": "We are upgrading the AFFiNE Cloud service and it is temporarily unavailable on the client side. If you wish to stay updated on the progress and be notified on availability, you can fill out the <1>AFFiNE Cloud Signup</1>.",
"com.affine.cloudTempDisable.title": "AFFiNE Cloud is upgrading now.",

View File

@@ -50,15 +50,15 @@
},
"dependencies": {
"@affine/sdk": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"jotai": "^2.3.1",
"zod": "^3.22.1"
},
"devDependencies": {
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"async-call-rpc": "^6.3.1",
"electron": "link:../../apps/electron/node_modules/electron",
"react": "^18.2.0",
@@ -93,5 +93,5 @@
"optional": true
}
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -6,7 +6,19 @@ import { atom, createStore } from 'jotai/vanilla';
import { getWorkspace, waitForWorkspace } from './__internal__/workspace.js';
// global store
export const rootStore = createStore();
let rootStore = createStore();
export function getCurrentStore() {
return rootStore;
}
/**
* @internal do not use this function unless you know what you are doing
*/
export function _setCurrentStore(store: ReturnType<typeof createStore>) {
rootStore = store;
}
export const loadedPluginNameAtom = atom<string[]>([]);
export const currentWorkspaceIdAtom = atom<string | null>(null);

View File

@@ -6,12 +6,12 @@
"jotai": "^2.3.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/editor": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/lit": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/editor": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/lit": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"lottie-web": "^5.12.2"
},
"peerDependencies": {
@@ -23,5 +23,5 @@
"@blocksuite/store": "*",
"lottie-web": "*"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -38,5 +38,5 @@
"test": "cross-env TS_NODE_TRANSPILE_ONLY=1 TS_NODE_PROJECT=./tsconfig.json node --test --loader ts-node/esm --experimental-specifier-resolution=node ./__tests__/**/*.mts",
"version": "napi version"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/plugin-cli",
"type": "module",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"bin": {
"af": "./src/af.mjs"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/sdk",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"type": "module",
"scripts": {
"build": "vite build",
@@ -22,9 +22,9 @@
"dist"
],
"dependencies": {
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"jotai": "^2.3.1",
"zod": "^3.22.1"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/storage",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"engines": {
"node": ">= 10.16.0 < 11 || >= 11.8.0"
},

View File

@@ -7,5 +7,5 @@
"./v1/*.json": "./v1/*.json",
"./preloading.json": "./preloading.json"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/workers",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"private": true,
"scripts": {
"dev": "wrangler dev"

View File

@@ -34,5 +34,5 @@
"@types/ws": "^8.5.5",
"ws": "^8.13.0"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "@toeverything/y-indexeddb",
"type": "module",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"description": "IndexedDB database adapter for Yjs",
"repository": "toeverything/AFFiNE",
"author": "toeverything",
@@ -37,8 +37,8 @@
},
"devDependencies": {
"@affine/y-provider": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"vite": "^4.4.9",
"vite-plugin-dts": "3.5.2",
"y-indexeddb": "^9.0.11"

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/y-provider",
"type": "module",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"description": "Yjs provider utilities for AFFiNE",
"main": "./src/index.ts",
"module": "./src/index.ts",
@@ -9,7 +9,7 @@
".": "./src/index.ts"
},
"devDependencies": {
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly"
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly"
},
"peerDependencies": {
"yjs": "^13.5.51"

View File

@@ -52,7 +52,7 @@ export const createLazyProvider = (
const changeStatus = (newStatus: Status) => {
// simulate a stack, each syncing and synced should be paired
if (newStatus.type === 'idle') {
if (syncingStack !== 0) {
if (connected && syncingStack !== 0) {
console.error('syncingStatus !== 0, this should not happen');
}
syncingStack = 0;
@@ -79,6 +79,9 @@ export const createLazyProvider = (
async function syncDoc(doc: Doc) {
const guid = doc.guid;
if (!connected) {
return;
}
changeStatus({
type: 'syncing',
@@ -87,6 +90,18 @@ export const createLazyProvider = (
.queryDocState(guid, {
stateVector: encodeStateVector(doc),
})
.then(remoteUpdate => {
if (!connected) {
changeStatus({
type: 'idle',
});
return;
}
changeStatus({
type: 'synced',
});
return remoteUpdate;
})
.catch(error => {
changeStatus({
type: 'error',
@@ -94,9 +109,6 @@ export const createLazyProvider = (
});
throw error;
});
changeStatus({
type: 'synced',
});
pendingMap.set(guid, []);
@@ -171,6 +183,9 @@ export const createLazyProvider = (
*/
function setupDatasourceListeners() {
datasourceUnsub = datasource.onDocUpdate?.((guid, update) => {
if (!connected) {
return;
}
changeStatus({
type: 'syncing',
});
@@ -244,16 +259,25 @@ export const createLazyProvider = (
});
// root doc should be already loaded,
// but we want to populate the cache for later update events
connectDoc(rootDoc).catch(error => {
changeStatus({
type: 'error',
error,
connectDoc(rootDoc)
.then(() => {
if (!connected) {
changeStatus({
type: 'idle',
});
return;
}
changeStatus({
type: 'synced',
});
})
.catch(error => {
changeStatus({
type: 'error',
error,
});
console.error(error);
});
console.error(error);
});
changeStatus({
type: 'synced',
});
setupDatasourceListeners();
}

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/bookmark-plugin",
"type": "module",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"description": "Bookmark Plugin",
"affinePlugin": {
"release": true,

View File

@@ -183,7 +183,7 @@ const BookMarkUI = ({ page }: BookMarkProps) => {
if (!shouldShowBookmarkMenu(pastedBlocks)) {
return;
}
setTimeout(() => {
window.setTimeout(() => {
setAnchor(getCurrentNativeRange());
}, 100);
});

View File

@@ -16,7 +16,7 @@
"dependencies": {
"@affine/component": "workspace:*",
"@affine/sdk": "workspace:*",
"@toeverything/components": "^0.0.11",
"@toeverything/components": "^0.0.12",
"idb": "^7.1.1",
"langchain": "^0.0.129",
"marked": "^7.0.3",
@@ -35,5 +35,5 @@
"react": "*",
"react-dom": "*"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -3,7 +3,7 @@
"type": "module",
"private": true,
"description": "Hello world plugin",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"scripts": {
"dev": "af dev",
"build": "af build"
@@ -18,7 +18,7 @@
"@affine/component": "workspace:*",
"@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.31",
"@toeverything/components": "^0.0.11"
"@toeverything/components": "^0.0.12"
},
"devDependencies": {
"@affine/plugin-cli": "workspace:*"

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/image-preview-plugin",
"type": "module",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"description": "Image preview plugin",
"affinePlugin": {
"release": true,
@@ -17,7 +17,7 @@
"@affine/component": "workspace:*",
"@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.31",
"@toeverything/components": "^0.0.11",
"@toeverything/components": "^0.0.12",
"@toeverything/theme": "^0.7.12",
"clsx": "^2.0.0",
"foxact": "^0.2.20",

View File

@@ -72,10 +72,10 @@ const ImagePreviewModalImpl = (
const [hasPlayedAnimation, setHasPlayedAnimation] = useState<boolean>(false);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
let timeoutId: number;
if (!isOpen) {
timeoutId = setTimeout(() => {
timeoutId = window.setTimeout(() => {
props.onClose();
setIsOpen(true);
}, 300);

View File

@@ -3,7 +3,7 @@
"type": "module",
"private": true,
"description": "Outline plugin",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"scripts": {
"dev": "af dev",
"build": "af build"
@@ -18,7 +18,7 @@
"@affine/component": "workspace:*",
"@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.31",
"@toeverything/components": "^0.0.11"
"@toeverything/components": "^0.0.12"
},
"devDependencies": {
"@affine/plugin-cli": "workspace:*",

View File

@@ -3,7 +3,7 @@
"type": "module",
"private": true,
"description": "Vue hello world plugin",
"version": "0.8.0-beta.0",
"version": "0.8.0",
"scripts": {
"dev": "af dev",
"build": "af build"

View File

@@ -10,14 +10,14 @@
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine-test/kit": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"@playwright/test": "^1.37.0",
"express": "^4.18.2",
"http-proxy-middleware": "^3.0.0-beta.1",
"serve": "^14.2.0"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -9,14 +9,14 @@
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine-test/kit": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/blocks": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/global": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/store": "0.0.0-20230818053411-bc330b35-nightly",
"@blocksuite/block-std": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/blocks": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/global": "0.0.0-20230822230555-98129627-nightly",
"@blocksuite/store": "0.0.0-20230822230555-98129627-nightly",
"@playwright/test": "^1.37.0",
"express": "^4.18.2",
"http-proxy-middleware": "^3.0.0-beta.1",
"serve": "^14.2.0"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

View File

@@ -1,7 +1,11 @@
import { test } from '@affine-test/kit/playwright';
import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { newPage, waitEditorLoad } from '@affine-test/kit/utils/page-logic';
import {
getBlockSuiteEditorTitle,
newPage,
waitEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Page } from '@playwright/test';
const openQuickSearchByShortcut = async (page: Page) =>
@@ -116,6 +120,16 @@ test('Create a new page and search this page', async ({ page }) => {
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
await page.reload();
await waitEditorLoad(page);
await openQuickSearchByShortcut(page);
await page.keyboard.insertText('test123456');
await page.waitForTimeout(300);
await assertResultList(page, ['test123456']);
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
});
test('Navigate to the 404 page and try to open quick search', async ({
page,
@@ -174,3 +188,77 @@ test('Not show navigation path if page is not a subpage or current page is not i
await openQuickSearchByShortcut(page);
expect(await page.getByTestId('navigation-path').count()).toBe(0);
});
test('assert the recent browse pages are on the recent list', async ({
page,
}) => {
await openHomePage(page);
await waitEditorLoad(page);
// create first page
await newPage(page);
{
const title = getBlockSuiteEditorTitle(page);
await title.type('sgtokidoki', {
delay: 50,
});
}
await page.waitForTimeout(200);
// create second page
await openQuickSearchByShortcut(page);
const addNewPage = page.getByTestId('quick-search-add-new-page');
await addNewPage.click();
{
const title = getBlockSuiteEditorTitle(page);
await title.type('theliquidhorse', {
delay: 50,
});
}
await page.waitForTimeout(200);
// create thrid page
await openQuickSearchByShortcut(page);
await addNewPage.click();
{
const title = getBlockSuiteEditorTitle(page);
await title.type('battlekot', {
delay: 50,
});
}
await page.waitForTimeout(200);
await openQuickSearchByShortcut(page);
await page.waitForTimeout(200);
{
// check does all 3 pages exists on recent page list
const quickSearchItems = page.locator('[cmdk-item]');
expect(await quickSearchItems.nth(0).textContent()).toBe('battlekot');
expect(await quickSearchItems.nth(1).textContent()).toBe('theliquidhorse');
expect(await quickSearchItems.nth(2).textContent()).toBe('sgtokidoki');
}
// create forth page, and check does the recent page list only contains three pages
await page.reload();
await waitEditorLoad(page);
await openQuickSearchByShortcut(page);
{
const addNewPage = page.getByTestId('quick-search-add-new-page');
await addNewPage.click();
}
await page.waitForTimeout(200);
{
const title = getBlockSuiteEditorTitle(page);
await title.type('affine is the best', {
delay: 50,
});
}
await page.waitForTimeout(1000);
await openQuickSearchByShortcut(page);
{
const quickSearchItems = page.locator('[cmdk-item]');
expect(await quickSearchItems.nth(0).textContent()).toBe(
'affine is the best'
);
}
});

View File

@@ -9,5 +9,5 @@
"@affine-test/kit": "workspace:*",
"@playwright/test": "^1.37.0"
},
"version": "0.8.0-beta.0"
"version": "0.8.0"
}

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