Compare commits

...

51 Commits

Author SHA1 Message Date
himself65
627d8ef787 v0.6.0-canary.3 2023-05-17 09:48:51 -07:00
LongYinan
5563823a7a build: missing build native step in nightly build (#2421) 2023-05-17 23:52:32 +08:00
JimmFly
d6804bb0fd chore: update prompt (#2410) 2023-05-17 17:52:57 +08:00
LongYinan
1350633690 build: fix electron release build process (#2408) 2023-05-17 17:22:49 +08:00
JimmFly
50196d8fde chore: update preloading page (#2409) 2023-05-17 09:16:07 +00:00
Peng Xiao
2e0ccb53ec feat: update button enhancements (#2401) 2023-05-17 16:58:14 +08:00
Whitewater
1498ee405b feat: add dropdown button (#2407) 2023-05-17 16:32:37 +08:00
Peng Xiao
cb863c4afa chore: disable image modal by default (#2400) 2023-05-16 23:14:31 -07:00
Himself65
2629d39501 fix: infinite reloading (#2405) 2023-05-17 13:58:34 +08:00
Himself65
38305cd984 fix: hydration error (#2404) 2023-05-17 05:23:55 +00:00
LongYinan
93116c24f2 feat(electron): use affine native (#2329) 2023-05-17 12:36:51 +08:00
Whitewater
017b9c8615 feat: add block card component (#2398) 2023-05-16 18:19:28 +08:00
Whitewater
9ce3a96862 fix: unexpected undefined class in popup (#2394) 2023-05-16 10:01:27 +00:00
Peng Xiao
a0ff520ba4 fix: some style updates (#2396) 2023-05-16 09:46:51 +00:00
Whitewater
a8b8986d89 chore: disable confused storybook backgrounds addon (#2395) 2023-05-16 17:46:35 +08:00
JimmFly
8ffc096fee fix: text overflows in the header option menu (#2393) 2023-05-16 17:35:57 +08:00
JimmFly
7e457f7b4c chore: add responsive styles for workspace card (#2390) 2023-05-16 16:51:46 +08:00
xiaodong zuo
aedf2d339e Update jobs.md
Added a job posting for a full-time or internship engineer.
2023-05-16 15:35:23 +08:00
JimmFly
ffd5ae52b3 feat: add Japanese support and update translation (#2388) 2023-05-16 14:21:51 +08:00
DiamondThree
3093194da8 docs: update jobs.md (#2389) 2023-05-15 22:24:27 -07:00
Horus
68b4f792f0 fix: app updater not working for internal release (#2377) 2023-05-15 20:34:54 -07:00
himself65
e2c6e4f9fc ci: use samver 2023-05-15 09:34:04 -07:00
Whitewater
9ff7dbffb7 feat: supports sort all page (#2356) 2023-05-15 08:50:43 -07:00
JimmFly
0c561da061 chore: remove favorite page (#2372) 2023-05-15 08:41:38 -07:00
JimmFly
06951319a6 chore: remove quick search tips (#2375) 2023-05-15 08:41:10 -07:00
JimmFly
0bfcab4067 chore: add animation for tour modal (#2365) 2023-05-15 16:48:52 +08:00
himself65
2c4db4fa16 v0.6.0-canary.2 2023-05-14 23:14:36 -07:00
Himself65
23b4f9ee12 feat(electron): track router history (#2336)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-14 23:13:30 -07:00
himself65
e5330b1917 build: add app bundle id for internal 2023-05-14 22:35:40 -07:00
Peng Xiao
183611a556 fix: some style updates (#2348) 2023-05-14 21:58:13 -07:00
Himself65
7786456ba4 chore: update blocksuite to 0.0.0-20230514141009-705c0fac-nightly (#2357) 2023-05-14 19:32:27 -07:00
Ikko Eltociear Ashimine
f4bf7e3ddf fix: typo in AFFiNE-Docs.md (#2355) 2023-05-13 22:37:42 -07:00
Doma
05d88215d1 feat(electron): app menu item and hotkey for creating new page (#2267)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-13 15:45:12 +00:00
Himself65
b240a70e51 chore: update blocksuite to 0.0.0-20230512192655-e61e272b-nightly (#2352) 2023-05-12 15:39:05 -05:00
LongYinan
00fd468e9b chore(server): remove bcrypt to avoid node-gyp usage (#2349) 2023-05-12 13:48:38 -05:00
Himself65
b5a7f8b7eb chore: bump version (#2331) 2023-05-12 13:47:14 -05:00
himself65
f03277fd17 v0.6.0-canary.1 2023-05-12 01:30:54 -05:00
himself65
ee93071149 chore: update icons 2023-05-12 01:06:05 -05:00
Himself65
21fdced2bd fix: correct router logic (#2342) 2023-05-12 00:55:45 -05:00
Peng Xiao
10b4558947 feat: new sidebar (app shell) styles (#2303) 2023-05-11 22:13:51 -05:00
Himself65
0fbed5d9d6 ci: collect test coverage on electron (#2335) 2023-05-11 20:51:13 -05:00
Himself65
8d117123d7 fix: remove useEffect on router sync with atoms (#2241) 2023-05-11 16:37:42 -05:00
Himself65
063ffda09d refactor: rename WorkspacePlugin to WorkspaceAdapter (#2330) 2023-05-11 12:43:39 -05:00
Himself65
39c83bd25b fix: delay setAom on rootWorkspacesMetadataAtom (#2271) 2023-05-11 15:03:11 +00:00
Peng Xiao
4444c3d1a6 fix: updater issue 2023-05-11 14:44:54 +08:00
LongYinan
717dd93f37 fix(electron): close db before move db file 2023-05-11 14:41:51 +08:00
LongYinan
c58673c55f chore(native): license 2023-05-11 14:41:51 +08:00
LongYinan
768e55072d ci: rust build config 2023-05-11 14:41:51 +08:00
LongYinan
8c84daec2b feat(native): NotifyEvent types 2023-05-11 14:41:51 +08:00
LongYinan
e54a5b6128 feat(native): provide FSWatcher 2023-05-11 14:41:51 +08:00
LongYinan
ee1e50f391 refactor(native): rename folder name 2023-05-11 14:41:51 +08:00
235 changed files with 6919 additions and 3668 deletions

View File

@@ -17,7 +17,7 @@
"hooks",
"i18n",
"jotai",
"octobase-node",
"native",
"templates",
"y-indexeddb",
"debug",

49
.github/actions/build-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: 'AFFiNE Rust build'
description: 'Rust build setup, including cache configuration'
inputs:
target:
description: 'Cargo target'
required: true
runs:
using: 'composite'
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ inputs.target }}
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
.cargo-cache
target/${{ inputs.target }}
key: stable-${{ inputs.target }}-cargo-cache
- name: Build
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
shell: bash
run: yarn workspace @affine/native build --target ${{ inputs.target }}
env:
CARGO_BUILD_INCREMENTAL: 'false'
- name: Build
if: ${{ inputs.target == 'x86_64-unknown-linux-gnu' }}
uses: addnab/docker-run-action@v3
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
options: --user 0:0 -e CARGO_BUILD_INCREMENTAL=false -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
run: yarn workspace @affine/native build --target ${{ inputs.target }}
- name: Build
if: ${{ inputs.target == 'aarch64-unknown-linux-gnu' }}
uses: addnab/docker-run-action@v3
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
options: --user 0:0 -e CARGO_BUILD_INCREMENTAL=false -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
run: yarn workspace @affine/native build --target ${{ inputs.target }}

View File

@@ -20,6 +20,11 @@ on:
- .github/**
- '!.github/workflows/build.yml'
env:
DEBUG: napi:*
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '10.13'
jobs:
lint:
name: Lint
@@ -49,23 +54,6 @@ jobs:
path: ./packages/component/storybook-static
if-no-files-found: error
build-electron:
name: Build @affine/electron
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Electron
working-directory: apps/electron
run: yarn build-layers
- name: Upload Ubuntu desktop artifact
uses: actions/upload-artifact@v3
with:
name: affine-ubuntu
path: ./apps/electron/dist
build:
name: Build @affine/web
runs-on: ubuntu-latest
@@ -273,7 +261,7 @@ jobs:
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
@@ -281,29 +269,63 @@ jobs:
dekstop-test:
name: Desktop Test
runs-on: ubuntu-latest
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 }
- { os: macos-latest, platform: macos, arch: arm64 }
- { os: ubuntu-latest, platform: linux, arch: x64 }
- { os: windows-latest, platform: windows, arch: x64 }
needs: [build, build-electron]
- {
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]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Download Ubuntu desktop artifact
uses: actions/download-artifact@v3
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
name: affine-ubuntu
path: ./apps/electron/dist
target: ${{ matrix.spec.target }}
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: |
rm -rf apps/electron/node_modules/better-sqlite3/build
yarn --cwd apps/electron/node_modules/better-sqlite3 run install
yarn test:unit
env:
NATIVE_TEST: 'true'
- name: Build layers
run: yarn workspace @affine/electron build-layers
- name: Download static resource artifact
uses: actions/download-artifact@v3
@@ -312,18 +334,42 @@ jobs:
path: ./apps/electron/resources/web-static
- name: Rebuild Electron dependences
run: yarn rebuild:for-electron
working-directory: apps/electron
shell: bash
run: |
rm -rf apps/electron/node_modules/better-sqlite3/build
yarn workspace @affine/electron rebuild:for-electron --arch=${{ matrix.spec.arch }}
- name: Run desktop tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test
working-directory: apps/electron
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: 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: true
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.shard }}
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore

View File

@@ -22,18 +22,30 @@ concurrency:
env:
BUILD_TYPE: internal
RELEASE_VERSION: ${{ github.ref_name }}-${{ github.sha }}
jobs:
set-build-version:
runs-on: ubuntu-latest
environment: production
outputs:
version: 0.0.0-${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
- uses: toeverything/set-build-version@latest
- id: version
run: echo ::set-output name=version::${{ env.BUILD_VERSION }}
before-make:
runs-on: ubuntu-latest
environment: production
needs:
- set-build-version
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Replace Version
run: ./scripts/set-version.sh ${{ env.RELEASE_VERSION }}
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
- name: generate-assets
working-directory: apps/electron
run: yarn generate-assets
@@ -53,6 +65,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_IMAGE_PREVIEW_MODAL: false
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
@@ -60,30 +73,40 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Upload Artifact (electron dist)
uses: actions/upload-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Upload YML Build Script
uses: actions/upload-artifact@v3
with:
name: release-yml-build-script
path: apps/electron/scripts/generate-yml.js
make-distribution:
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- { os: macos-latest, platform: macos, arch: x64 }
- { os: macos-latest, platform: macos, arch: arm64 }
- { os: ubuntu-latest, platform: linux, arch: x64 }
- { os: windows-latest, platform: windows, arch: x64 }
- {
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 }}
needs: before-make
needs:
- before-make
- set-build-version
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@@ -93,36 +116,43 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
- name: Replace Version
run: ./scripts/set-version.sh ${{ env.RELEASE_VERSION }}
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- uses: actions/download-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Rebuild Electron dependences
shell: bash
run: |
rm -rf apps/electron/node_modules/better-sqlite3/build
yarn workspace @affine/electron rebuild:for-electron --arch=${{ matrix.spec.arch }}
- name: Build layers
run: yarn workspace @affine/electron build-layers
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'macos' }}
if: ${{ matrix.spec.platform == 'darwin' }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: make
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
working-directory: apps/electron
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'macos' }}
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
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 == '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
@@ -143,52 +173,51 @@ jobs:
path: builds
release:
needs: make-distribution
needs:
- make-distribution
- set-build-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-macos-x64-builds
name: affine-darwin-x64-builds
path: ./
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-macos-arm64-builds
name: affine-darwin-arm64-builds
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-windows-x64-builds
name: affine-win32-x64-builds
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-linux-x64-builds
path: ./
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
name: release-yml-build-script
path: ./
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate Release yml
run: |
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
repository: 'toeverything/AFFiNE-Releases'
name: ${{ env.RELEASE_VERSION }}
tag_name: ${{ env.RELEASE_VERSION }}
name: ${{ needs.set-build-version.outputs.version }}
tag_name: ${{ needs.set-build-version.outputs.version }}
prerelease: true
files: |
./VERSION

View File

@@ -36,6 +36,9 @@ concurrency:
env:
BUILD_TYPE: ${{ github.event.inputs.build-type }}
DEBUG: napi:*
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '10.13'
jobs:
before-make:
@@ -46,8 +49,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: generate-assets
working-directory: apps/electron
run: yarn generate-assets
run: yarn workspace @affine/electron generate-assets
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
@@ -64,6 +66,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_IMAGE_PREVIEW_MODAL: false
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
@@ -71,28 +74,36 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Upload Artifact (electron dist)
uses: actions/upload-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Upload YML Build Script
uses: actions/upload-artifact@v3
with:
name: release-yml-build-script
path: apps/electron/scripts/generate-yml.js
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- { os: macos-latest, platform: macos, arch: x64 }
- { os: macos-latest, platform: macos, arch: arm64 }
- { os: ubuntu-latest, platform: linux, arch: x64 }
- { os: windows-latest, platform: windows, arch: x64 }
- {
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 }}
needs: before-make
env:
@@ -104,34 +115,42 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- uses: actions/download-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Rebuild Electron dependences
shell: bash
run: |
rm -rf apps/electron/node_modules/better-sqlite3/build
yarn workspace @affine/electron rebuild:for-electron --arch=${{ matrix.spec.arch }}
- name: Build layers
run: yarn workspace @affine/electron build-layers
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'macos' }}
if: ${{ matrix.spec.platform == 'darwin' }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: make
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
working-directory: apps/electron
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'macos' }}
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
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 == '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
@@ -156,37 +175,36 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-macos-x64-builds
name: affine-darwin-x64-builds
path: ./
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-macos-arm64-builds
name: affine-darwin-arm64-builds
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-windows-x64-builds
name: affine-win32-x64-builds
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-linux-x64-builds
path: ./
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
name: release-yml-build-script
path: ./
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate Release yml
run: |
RELEASE_VERSION=${{ github.event.inputs.version }} node generate-yml.js
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ github.event.inputs.version }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:

4
.gitignore vendored
View File

@@ -66,3 +66,7 @@ i18n-generated.ts
# Cache
.eslintcache
next-env.d.ts
# Rust
target
*.node

9
.taplo.toml Normal file
View File

@@ -0,0 +1,9 @@
exclude = ["node_modules/**/*.toml"]
[[rule]]
keys = ["dependencies", "*-dependencies"]
[rule.formatting]
align_entries = true
indent_tables = true
reorder_keys = true

View File

@@ -26,7 +26,6 @@
"[toml]": {
"editor.defaultFormatter": "tamasfe.even-better-toml"
},
"rust-analyzer.linkedProjects": ["packages/octobase-node/Cargo.toml"],
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
@@ -38,5 +37,6 @@
"apps/electron/layers/**/*.spec.ts",
"tests/unit/**/*.spec.ts",
"tests/unit/**/*.spec.tsx"
]
],
"deepscan.enable": true
}

800
Cargo.lock generated Normal file
View File

@@ -0,0 +1,800 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "affine_native"
version = "0.0.0"
dependencies = [
"anyhow",
"napi",
"napi-build",
"napi-derive",
"notify",
"once_cell",
"parking_lot",
"serde",
"serde_json",
"tokio",
"uuid",
]
[[package]]
name = "aho-corasick"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
dependencies = [
"cfg-if",
]
[[package]]
name = "ctor"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4056f63fce3b82d852c3da92b08ea59959890813a7f4ce9c0ff85b10cf301b"
dependencies = [
"quote",
"syn 2.0.15",
]
[[package]]
name = "filetime"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.48.0",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "kqueue"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "libc"
version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mio"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.45.0",
]
[[package]]
name = "napi"
version = "2.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ac8112fe5998579b22e29903c7b277fc7f91c7860c0236f35792caf8156e18"
dependencies = [
"anyhow",
"bitflags 2.2.1",
"ctor",
"napi-derive",
"napi-sys",
"once_cell",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "napi-build"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e"
[[package]]
name = "napi-derive"
version = "2.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c47e0f395207c062e680a158f0624ec456c1dfb3c96a8cb888e0401506d50ae9"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a83afae5b4ba6f98ed6e33a52da343fdeb66474f1162a38cde5a3d46eb054e7"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn 1.0.109",
]
[[package]]
name = "napi-sys"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3"
dependencies = [
"libloading",
]
[[package]]
name = "notify"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9"
dependencies = [
"bitflags 1.3.2",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"mio",
"serde",
"walkdir",
"windows-sys 0.42.0",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.45.0",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "ryu"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
version = "1.0.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "serde_json"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
dependencies = [
"autocfg",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "uuid"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
dependencies = [
"getrandom",
"rand",
"serde",
]
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"

8
Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[workspace]
members = ["./packages/native"]
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
strip = "symbols"

View File

@@ -33,6 +33,7 @@ module.exports = {
packagerConfig: {
name: productName,
appBundleId: fromBuildIdentifier({
internal: 'pro.affine.internal',
canary: 'pro.affine.canary',
beta: 'pro.affine.beta',
stable: 'pro.affine.app',

View File

@@ -0,0 +1,135 @@
import { app, Menu } from 'electron';
import { isMacOS } from '../../utils';
import { subjects } from './events';
import { checkForUpdatesAndNotify } from './handlers/updater';
// Unique id for menuitems
const MENUITEM_NEW_PAGE = 'affine:new-page';
export function createApplicationMenu() {
const isMac = isMacOS();
// Electron menu cannot be modified
// You have to copy the complete default menu template event if you want to add a single custom item
// See https://www.electronjs.org/docs/latest/api/menu#examples
const template = [
// { role: 'appMenu' }
...(isMac
? [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
},
]
: []),
// { role: 'fileMenu' }
{
label: 'File',
submenu: [
{
id: MENUITEM_NEW_PAGE,
label: 'New Page',
accelerator: isMac ? 'Cmd+N' : 'Ctrl+N',
click: () => {
subjects.applicationMenu.newPageAction.next();
},
},
{ type: 'separator' },
isMac ? { role: 'close' } : { role: 'quit' },
],
},
// { role: 'editMenu' }
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
...(isMac
? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
},
]
: [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),
],
},
// { role: 'viewMenu' }
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
// { role: 'windowMenu' }
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac
? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' },
]
: [{ role: 'close' }]),
],
},
{
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { shell } = require('electron');
await shell.openExternal('https://affine.pro/');
},
},
{
label: 'Check for Updates',
click: async () => {
await checkForUpdatesAndNotify(true);
},
},
],
},
];
// @ts-ignore The snippet is copied from Electron official docs.
// It's working as expected. No idea why it contains type errors.
// Just ignore for now.
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}

View File

@@ -0,0 +1,22 @@
import { Subject } from 'rxjs';
import type { MainEventListener } from './type';
export const applicationMenuSubjects = {
newPageAction: new Subject<void>(),
};
/**
* Events triggered by application menu
*/
export const applicationMenuEvents = {
/**
* File -> New Page
*/
onNewPageAction: (fn: () => void) => {
const sub = applicationMenuSubjects.newPageAction.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventListener>;

View File

@@ -1,7 +1,9 @@
export * from './register';
import { applicationMenuSubjects } from './application-menu';
import { dbSubjects } from './db';
export const subjects = {
db: dbSubjects,
applicationMenu: applicationMenuSubjects,
};

View File

@@ -1,12 +1,14 @@
import { app, BrowserWindow } from 'electron';
import { logger } from '../logger';
import { applicationMenuEvents } from './application-menu';
import { dbEvents } from './db';
import { updaterEvents } from './updater';
export const allEvents = {
db: dbEvents,
updater: updaterEvents,
applicationMenu: applicationMenuEvents,
};
function getActiveWindows() {

View File

@@ -1,19 +1,34 @@
import { Subject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import type { MainEventListener } from './type';
interface UpdateMeta {
version: string;
allowAutoUpdate: boolean;
}
export const updaterSubjects = {
// means it is ready for restart and install the new version
clientUpdateReady: new Subject<UpdateMeta>(),
updateAvailable: new Subject<UpdateMeta>(),
updateReady: new Subject<UpdateMeta>(),
downloadProgress: new BehaviorSubject<number>(0),
};
export const updaterEvents = {
onClientUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
const sub = updaterSubjects.clientUpdateReady.subscribe(fn);
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => {
const sub = updaterSubjects.updateAvailable.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
const sub = updaterSubjects.updateReady.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
onDownloadProgress: (fn: (progress: number) => void) => {
const sub = updaterSubjects.downloadProgress.subscribe(fn);
return () => {
sub.unsubscribe();
};

View File

@@ -2,6 +2,8 @@ import assert from 'node:assert';
import path from 'node:path';
import fs from 'fs-extra';
import type { Subscription } from 'rxjs';
import { v4 } from 'uuid';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
@@ -99,6 +101,11 @@ const electronModule = {
handlers.push(callback);
registeredHandlers.set(name, handlers);
},
addEventListener: (...args: any[]) => {
// @ts-ignore
electronModule.app.on(...args);
},
removeEventListener: () => {},
},
BrowserWindow: {
getAllWindows: () => {
@@ -116,6 +123,8 @@ vi.doMock('electron', () => {
return electronModule;
});
let connectableSubscription: Subscription;
beforeEach(async () => {
const { registerHandlers } = await import('../register');
registerHandlers();
@@ -123,20 +132,24 @@ beforeEach(async () => {
// should also register events
const { registerEvents } = await import('../../events');
registerEvents();
await fs.mkdirp(SESSION_DATA_PATH);
const { database$ } = await import('../db/ensure-db');
connectableSubscription = database$.connect();
});
afterEach(async () => {
const { cleanupSQLiteDBs } = await import('../db/ensure-db');
await cleanupSQLiteDBs();
await fs.remove(SESSION_DATA_PATH);
// reset registered handlers
registeredHandlers.get('before-quit')?.forEach(fn => fn());
connectableSubscription.unsubscribe();
await fs.remove(SESSION_DATA_PATH);
});
describe('ensureSQLiteDB', () => {
test('should create db file on connection if it does not exist', async () => {
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
const workspaceDB = await ensureSQLiteDB(id);
const file = workspaceDB.path;
@@ -146,70 +159,76 @@ describe('ensureSQLiteDB', () => {
test('when db file is removed', async () => {
// stub webContents.send
const sendStub = vi.fn();
browserWindow.webContents.send = sendStub;
const id = 'test-workspace-id';
const sendSpy = vi.spyOn(browserWindow.webContents, 'send');
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
let workspaceDB = await ensureSQLiteDB(id);
const file = workspaceDB.path;
const fileExists = await fs.pathExists(file);
expect(fileExists).toBe(true);
// Can't remove file on Windows, because the sqlite is still holding the file handle
if (process.platform === 'win32') {
return;
}
await fs.remove(file);
// wait for 1000ms for file watcher to detect file removal
// wait for 2000ms for file watcher to detect file removal
await delay(2000);
expect(sendStub).toBeCalledWith('db:onDBFileMissing', id);
expect(sendSpy).toBeCalledWith('db:onDBFileMissing', id);
// ensureSQLiteDB should recreate the db file
workspaceDB = await ensureSQLiteDB(id);
const fileExists2 = await fs.pathExists(file);
expect(fileExists2).toBe(true);
sendSpy.mockRestore();
});
test('when db file is updated', async () => {
// stub webContents.send
const sendStub = vi.fn();
browserWindow.webContents.send = sendStub;
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
const { dbSubjects } = await import('../../events/db');
const workspaceDB = await ensureSQLiteDB(id);
const file = workspaceDB.path;
const fileExists = await fs.pathExists(file);
expect(fileExists).toBe(true);
// wait to make sure
await delay(500);
const dbUpdateSpy = vi.spyOn(dbSubjects.dbFileUpdate, 'next');
await delay(100);
// writes some data to the db file
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
// write again
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
// wait for 200ms for file watcher to detect file change
// wait for 2000ms for file watcher to detect file change
await delay(2000);
expect(sendStub).toBeCalledWith('db:onDBFileUpdate', id);
expect(dbUpdateSpy).toBeCalledWith(id);
dbUpdateSpy.mockRestore();
});
});
describe('workspace handlers', () => {
test('list all workspace ids', async () => {
const ids = ['test-workspace-id', 'test-workspace-id-2'];
const ids = [v4(), v4()];
const { ensureSQLiteDB } = await import('../db/ensure-db');
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
const list = await dispatch('workspace', 'list');
expect(list.map(([id]) => id)).toEqual(ids);
expect(list.map(([id]) => id).sort()).toEqual(ids.sort());
});
test('delete workspace', async () => {
const ids = ['test-workspace-id', 'test-workspace-id-2'];
// @TODO dispatch is hanging on Windows
if (process.platform === 'win32') {
return;
}
const ids = [v4(), v4()];
const { ensureSQLiteDB } = await import('../db/ensure-db');
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
await dispatch('workspace', 'delete', 'test-workspace-id-2');
await dispatch('workspace', 'delete', ids[1]);
const list = await dispatch('workspace', 'list');
expect(list.map(([id]) => id)).toEqual(['test-workspace-id']);
expect(list.map(([id]) => id)).toEqual([ids[0]]);
});
});
@@ -244,7 +263,7 @@ describe('UI handlers', () => {
describe('db handlers', () => {
test('apply doc and get doc updates', async () => {
const workspaceId = 'test-workspace-id';
const workspaceId = v4();
const bin = await dispatch('db', 'getDocAsUpdates', workspaceId);
// ? is this a good test?
expect(bin.every((byte: number) => byte === 0)).toBe(true);
@@ -264,13 +283,13 @@ describe('db handlers', () => {
});
test('get non existent blob', async () => {
const workspaceId = 'test-workspace-id';
const workspaceId = v4();
const bin = await dispatch('db', 'getBlob', workspaceId, 'non-existent-id');
expect(bin).toBeNull();
});
test('list blobs (empty)', async () => {
const workspaceId = 'test-workspace-id';
const workspaceId = v4();
const list = await dispatch('db', 'getPersistedBlobs', workspaceId);
expect(list).toEqual([]);
});
@@ -318,7 +337,7 @@ describe('dialog handlers', () => {
const mockShowItemInFolder = vi.fn();
electronModule.shell.showItemInFolder = mockShowItemInFolder;
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
const db = await ensureSQLiteDB(id);
@@ -334,13 +353,15 @@ describe('dialog handlers', () => {
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
electronModule.shell.showItemInFolder = mockShowItemInFolder;
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
await ensureSQLiteDB(id);
await dispatch('dialog', 'saveDBFileAs', id);
expect(mockShowSaveDialog).toBeCalled();
expect(mockShowItemInFolder).not.toBeCalled();
electronModule.dialog = {};
electronModule.shell = {};
});
test('saveDBFileAs', async () => {
@@ -352,7 +373,7 @@ describe('dialog handlers', () => {
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
electronModule.shell.showItemInFolder = mockShowItemInFolder;
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
await ensureSQLiteDB(id);
@@ -403,11 +424,13 @@ describe('dialog handlers', () => {
const res = await dispatch('dialog', 'loadDBFile');
expect(mockShowOpenDialog).toBeCalled();
expect(res.error).toBe('DB_FILE_INVALID');
electronModule.dialog = {};
});
test('loadDBFile', async () => {
// we use ensureSQLiteDB to create a valid db file
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
const db = await ensureSQLiteDB(id);
@@ -417,6 +440,11 @@ describe('dialog handlers', () => {
await fs.ensureDir(basePath);
await fs.copyFile(db.path, originDBFilePath);
// on Windows, we skip this test because we can't delete the db file
if (process.platform === 'win32') {
return;
}
// remove db
await fs.remove(db.path);
@@ -440,19 +468,19 @@ describe('dialog handlers', () => {
});
test('moveDBFile', async () => {
const newPath = path.join(SESSION_DATA_PATH, 'affine-test', 'xxx');
const newPath = path.join(SESSION_DATA_PATH, 'xxx');
const mockShowSaveDialog = vi.fn(() => {
return { filePath: newPath };
}) as any;
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
await ensureSQLiteDB(id);
const res = await dispatch('dialog', 'moveDBFile', id);
expect(mockShowSaveDialog).toBeCalled();
expect(res.filePath).toBe(newPath);
electronModule.dialog = {};
});
test('moveDBFile (skipped)', async () => {
@@ -461,12 +489,13 @@ describe('dialog handlers', () => {
}) as any;
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
const id = 'test-workspace-id';
const id = v4();
const { ensureSQLiteDB } = await import('../db/ensure-db');
await ensureSQLiteDB(id);
const res = await dispatch('dialog', 'moveDBFile', id);
expect(mockShowSaveDialog).toBeCalled();
expect(res.filePath).toBe(undefined);
electronModule.dialog = {};
});
});

View File

@@ -1,94 +1,160 @@
import { watch } from 'chokidar';
import type { NotifyEvent } from '@affine/native/event';
import { createFSWatcher } from '@affine/native/fs-watcher';
import { app } from 'electron';
import {
connectable,
defer,
from,
fromEvent,
identity,
lastValueFrom,
Observable,
ReplaySubject,
Subject,
} from 'rxjs';
import {
debounceTime,
exhaustMap,
filter,
groupBy,
ignoreElements,
mergeMap,
shareReplay,
startWith,
switchMap,
take,
takeUntil,
tap,
} from 'rxjs/operators';
import { appContext } from '../../context';
import { subjects } from '../../events';
import { logger } from '../../logger';
import { debounce, ts } from '../../utils';
import { ts } from '../../utils';
import type { WorkspaceSQLiteDB } from './sqlite';
import { openWorkspaceDatabase } from './sqlite';
const dbMapping = new Map<string, Promise<WorkspaceSQLiteDB>>();
const dbWatchers = new Map<string, () => void>();
const databaseInput$ = new Subject<string>();
export const databaseConnector$ = new ReplaySubject<WorkspaceSQLiteDB>();
const groupedDatabaseInput$ = databaseInput$.pipe(groupBy(identity));
export const database$ = connectable(
groupedDatabaseInput$.pipe(
mergeMap(workspaceDatabase$ =>
workspaceDatabase$.pipe(
// only open the first db with the same workspaceId, and emit it to the downstream
exhaustMap(workspaceId => {
logger.info('[ensureSQLiteDB] open db connection', workspaceId);
return from(openWorkspaceDatabase(appContext, workspaceId)).pipe(
switchMap(db => {
return startWatchingDBFile(db).pipe(
// ignore all events and only emit the db to the downstream
ignoreElements(),
startWith(db)
);
})
);
}),
shareReplay(1)
)
),
tap({
complete: () => {
logger.info('[FSWatcher] close all watchers');
createFSWatcher().close();
},
})
),
{
connector: () => databaseConnector$,
resetOnDisconnect: true,
}
);
export const databaseConnectableSubscription = database$.connect();
// 1. File delete
// 2. File move
// - on Linux, it's `type: { modify: { kind: 'rename', mode: 'from' } }`
// - on Windows, it's `type: { remove: { kind: 'any' } }`
// - on macOS, it's `type: { modify: { kind: 'rename', mode: 'any' } }`
export function isRemoveOrMoveEvent(event: NotifyEvent) {
return (
typeof event.type === 'object' &&
('remove' in event.type ||
('modify' in event.type &&
event.type.modify.kind === 'rename' &&
(event.type.modify.mode === 'from' ||
event.type.modify.mode === 'any')))
);
}
// if we removed the file, we will stop watching it
function startWatchingDBFile(db: WorkspaceSQLiteDB) {
if (dbWatchers.has(db.workspaceId)) {
return dbWatchers.get(db.workspaceId);
}
logger.info('watch db file', db.path);
const watcher = watch(db.path);
const debounceOnChange = debounce(() => {
logger.info(
'db file changed on disk',
db.workspaceId,
ts() - db.lastUpdateTime,
'ms'
const FSWatcher = createFSWatcher();
return new Observable<NotifyEvent>(subscriber => {
logger.info('[FSWatcher] start watching db file', db.workspaceId);
const subscription = FSWatcher.watch(db.path, {
recursive: false,
}).subscribe(
event => {
logger.info('[FSWatcher]', event);
subscriber.next(event);
// remove file or move file, complete the observable and close db
if (isRemoveOrMoveEvent(event)) {
subscriber.complete();
}
},
err => {
subscriber.error(err);
}
);
// reconnect db
db.reconnectDB();
subjects.db.dbFileUpdate.next(db.workspaceId);
}, 1000);
watcher.on('change', () => {
const currentTime = ts();
if (currentTime - db.lastUpdateTime > 100) {
debounceOnChange();
}
});
dbWatchers.set(db.workspaceId, () => {
watcher.close();
});
// todo: there is still a possibility that the file is deleted
// but we didn't get the event soon enough and another event tries to
// access the db
watcher.on('unlink', () => {
logger.info('db file missing', db.workspaceId);
subjects.db.dbFileMissing.next(db.workspaceId);
// cleanup
watcher.close().then(() => {
return () => {
// destroy on unsubscribe
logger.info('[FSWatcher] cleanup db file watcher', db.workspaceId);
db.destroy();
dbWatchers.delete(db.workspaceId);
dbMapping.delete(db.workspaceId);
});
});
subscription.unsubscribe();
};
}).pipe(
debounceTime(1000),
filter(event => !isRemoveOrMoveEvent(event)),
tap({
next: () => {
logger.info(
'[FSWatcher] db file changed on disk',
db.workspaceId,
ts() - db.lastUpdateTime,
'ms'
);
db.reconnectDB();
subjects.db.dbFileUpdate.next(db.workspaceId);
},
complete: () => {
// todo: there is still a possibility that the file is deleted
// but we didn't get the event soon enough and another event tries to
// access the db
logger.info('[FSWatcher] db file missing', db.workspaceId);
subjects.db.dbFileMissing.next(db.workspaceId);
db.destroy();
},
}),
takeUntil(defer(() => fromEvent(app, 'before-quit')))
);
}
export async function ensureSQLiteDB(id: string) {
let workspaceDB = dbMapping.get(id);
if (!workspaceDB) {
logger.info('[ensureSQLiteDB] open db connection', id);
workspaceDB = openWorkspaceDatabase(appContext, id);
dbMapping.set(id, workspaceDB);
startWatchingDBFile(await workspaceDB);
}
return await workspaceDB;
export function ensureSQLiteDB(id: string) {
const deferValue = lastValueFrom(
database$.pipe(
filter(db => db.workspaceId === id && db.db.open),
take(1),
tap({
error: err => {
logger.error('[ensureSQLiteDB] error', err);
},
})
)
);
databaseInput$.next(id);
return deferValue;
}
export async function disconnectSQLiteDB(id: string) {
const dbp = dbMapping.get(id);
if (dbp) {
const db = await dbp;
logger.info('close db connection', id);
db.destroy();
dbWatchers.get(id)?.();
dbWatchers.delete(id);
dbMapping.delete(id);
}
}
export async function cleanupSQLiteDBs() {
for (const [id] of dbMapping) {
logger.info('close db connection', id);
await disconnectSQLiteDB(id);
}
dbMapping.clear();
dbWatchers.clear();
}
app?.on('before-quit', async () => {
await cleanupSQLiteDBs();
});

View File

@@ -42,6 +42,7 @@ export class WorkspaceSQLiteDB {
ydoc = new Y.Doc();
firstConnect = false;
lastUpdateTime = ts();
destroyed = false;
constructor(public path: string, public workspaceId: string) {
this.db = this.reconnectDB();
@@ -58,7 +59,7 @@ export class WorkspaceSQLiteDB {
};
reconnectDB = () => {
logger.log('open db', this.workspaceId);
logger.log('[WorkspaceSQLiteDB] open db', this.workspaceId);
if (this.db) {
this.db.close();
}
@@ -224,8 +225,9 @@ export async function openWorkspaceDatabase(
}
export function isValidDBFile(path: string) {
let db: Database | null = null;
try {
const db = sqlite(path);
db = sqlite(path);
// check if db has two tables, one for updates and onefor blobs
const statement = db.prepare(
`SELECT name FROM sqlite_schema WHERE type='table'`
@@ -239,6 +241,7 @@ export function isValidDBFile(path: string) {
return true;
} catch (error) {
logger.error('isValidDBFile', error);
db?.close();
return false;
}
}

View File

@@ -6,7 +6,8 @@ import { nanoid } from 'nanoid';
import { appContext } from '../../context';
import { logger } from '../../logger';
import { ensureSQLiteDB } from '../db/ensure-db';
import { ensureSQLiteDB, isRemoveOrMoveEvent } from '../db/ensure-db';
import type { WorkspaceSQLiteDB } from '../db/sqlite';
import { getWorkspaceDBPath, isValidDBFile } from '../db/sqlite';
import { listWorkspaces } from '../workspace/workspace';
@@ -232,17 +233,29 @@ export async function moveDBFile(
workspaceId: string,
dbFileLocation?: string
): Promise<MoveDBFileResult> {
let db: WorkspaceSQLiteDB | null = null;
try {
const db = await ensureSQLiteDB(workspaceId);
const { moveFile, FsWatcher } = await import('@affine/native');
db = await ensureSQLiteDB(workspaceId);
// get the real file path of db
const realpath = await fs.realpath(db.path);
const isLink = realpath !== db.path;
const watcher = FsWatcher.watch(realpath, { recursive: false });
const waitForRemove = new Promise<void>(resolve => {
const subscription = watcher.subscribe(event => {
if (isRemoveOrMoveEvent(event)) {
subscription.unsubscribe();
// resolve after FSWatcher in `database$` is fired
setImmediate(() => {
resolve();
});
}
});
});
const newFilePath =
dbFileLocation ||
dbFileLocation ??
(
getFakedResult() ||
getFakedResult() ??
(await dialog.showSaveDialog({
properties: ['showOverwriteConfirmation'],
title: 'Move Workspace Storage',
@@ -263,6 +276,8 @@ export async function moveDBFile(
};
}
db.db.close();
if (await fs.pathExists(newFilePath)) {
return {
error: 'FILE_ALREADY_EXISTS',
@@ -274,21 +289,26 @@ export async function moveDBFile(
await fs.unlink(db.path);
}
await fs.move(realpath, newFilePath, {
overwrite: true,
});
logger.info(`[moveDBFile] move ${realpath} -> ${newFilePath}`);
db.db.close();
await moveFile(realpath, newFilePath);
await fs.ensureSymlink(newFilePath, db.path, 'file');
logger.info(`openMoveDBFileDialog symlink: ${realpath} -> ${newFilePath}`);
db.reconnectDB();
logger.info(`[moveDBFile] symlink: ${realpath} -> ${newFilePath}`);
// wait for the file move event emits to the FileWatcher in database$ in ensure-db.ts
// so that the db will be destroyed and we can call the `ensureSQLiteDB` in the next step
// or the FileWatcher will continue listen on the `realpath` and emit file change events
// then the database will reload while receiving these events; and the moved database file will be recreated while reloading database
await waitForRemove;
logger.info(`removed`);
await ensureSQLiteDB(workspaceId);
return {
filePath: newFilePath,
};
} catch (err) {
logger.error('moveDBFile', err);
db?.destroy();
logger.error('[moveDBFile]', err);
return {
error: 'UNKNOWN_ERROR',
};

View File

@@ -18,7 +18,7 @@ export const dialogHandlers = {
saveDBFileAs: async (_, workspaceId: string) => {
return saveDBFileAs(workspaceId);
},
moveDBFile: async (_, workspaceId: string, dbFileLocation?: string) => {
moveDBFile: (_, workspaceId: string, dbFileLocation?: string) => {
return moveDBFile(workspaceId, dbFileLocation);
},
selectDBFileLocation: async () => {

View File

@@ -1,9 +1,17 @@
import { app } from 'electron';
import type { NamespaceHandlers } from '../type';
import { checkForUpdatesAndNotify, quitAndInstall } from './updater';
export const updaterHandlers = {
updateClient: async () => {
const { updateClient } = await import('./updater');
return updateClient();
currentVersion: async () => {
return app.getVersion();
},
quitAndInstall: async () => {
return quitAndInstall();
},
checkForUpdatesAndNotify: async () => {
return checkForUpdatesAndNotify(true);
},
} satisfies NamespaceHandlers;

View File

@@ -1,3 +1,4 @@
import { app } from 'electron';
import type { AppUpdater } from 'electron-updater';
import { z } from 'zod';
@@ -21,14 +22,27 @@ const isDev = mode === 'development';
let _autoUpdater: AppUpdater | null = null;
export const updateClient = async () => {
export const quitAndInstall = async () => {
_autoUpdater?.quitAndInstall();
};
let lastCheckTime = 0;
export const checkForUpdatesAndNotify = async (force = true) => {
if (!_autoUpdater) {
return; // ?
}
// check every 30 minutes (1800 seconds) at most
if (force || lastCheckTime + 1000 * 1800 < Date.now()) {
lastCheckTime = Date.now();
return _autoUpdater.checkForUpdatesAndNotify();
}
};
export const registerUpdater = async () => {
// require it will cause some side effects and will break generate-main-exposed-meta,
// so we wrap it in a function
const { autoUpdater } = await import('electron-updater');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { autoUpdater } = require('electron-updater');
_autoUpdater = autoUpdater;
@@ -36,6 +50,9 @@ export const registerUpdater = async () => {
return;
}
// TODO: support auto update on windows and linux
const allowAutoUpdate = isMacOS();
_autoUpdater.autoDownload = false;
_autoUpdater.allowPrerelease = buildType !== 'stable';
_autoUpdater.autoInstallOnAppQuit = false;
@@ -48,24 +65,36 @@ export const registerUpdater = async () => {
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
});
if (isMacOS()) {
_autoUpdater.on('update-available', () => {
// register events for checkForUpdatesAndNotify
_autoUpdater.on('update-available', info => {
if (allowAutoUpdate) {
_autoUpdater!.downloadUpdate();
logger.info('Update available, downloading...');
logger.info('Update available, downloading...', info);
}
updaterSubjects.updateAvailable.next({
version: info.version,
allowAutoUpdate,
});
_autoUpdater.on('download-progress', e => {
logger.info(`Download progress: ${e.percent}`);
});
_autoUpdater.on('download-progress', e => {
logger.info(`Download progress: ${e.percent}`);
updaterSubjects.downloadProgress.next(e.percent);
});
_autoUpdater.on('update-downloaded', e => {
updaterSubjects.updateReady.next({
version: e.version,
allowAutoUpdate,
});
_autoUpdater.on('update-downloaded', e => {
updaterSubjects.clientUpdateReady.next({
version: e.version,
});
logger.info('Update downloaded, ready to install');
});
_autoUpdater.on('error', e => {
logger.error('Error while updating client', e);
});
_autoUpdater.forceDevUpdateConfig = isDev;
await _autoUpdater.checkForUpdatesAndNotify();
}
// I guess we can skip it?
// updaterSubjects.clientDownloadProgress.next(100);
logger.info('Update downloaded, ready to install');
});
_autoUpdater.on('error', e => {
logger.error('Error while updating client', e);
});
_autoUpdater.forceDevUpdateConfig = isDev;
app.on('activate', async () => {
await checkForUpdatesAndNotify(false);
});
};

View File

@@ -2,6 +2,7 @@ import './security-restrictions';
import { app } from 'electron';
import { createApplicationMenu } from './application-menu';
import { registerEvents } from './events';
import { registerHandlers } from './handlers';
import { registerUpdater } from './handlers/updater';
@@ -57,6 +58,7 @@ app
.then(registerHandlers)
.then(registerEvents)
.then(restoreOrCreateWindow)
.then(createApplicationMenu)
.then(registerUpdater)
.catch(e => console.error('Failed create window:', e));
/**

View File

@@ -28,7 +28,7 @@ async function createWindow() {
y: mainWindowState.y,
width: mainWindowState.width,
minWidth: 640,
transparent: isMacOS(),
minHeight: 480,
visualEffectState: 'active',
vibrancy: 'under-window',
height: mainWindowState.height,

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
interface Window {
apis?: typeof import('./src/affine-apis').apis;
events?: typeof import('./src/affine-apis').events;
appInfo?: typeof import('./src/affine-apis').appInfo;
apis: typeof import('./src/affine-apis').apis;
events: typeof import('./src/affine-apis').events;
appInfo: typeof import('./src/affine-apis').appInfo;
}

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.6.0-canary.0",
"version": "0.6.0-canary.3",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -18,10 +18,6 @@
"generate-main-exposed-meta": "zx scripts/generate-main-exposed-meta.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
"make-macos-x64": "electron-forge make --platform=darwin --arch=x64",
"make-windows-x64": "electron-forge make --platform=win32 --arch=x64",
"make-linux-x64": "electron-forge make --platform=linux --arch=x64",
"rebuild:for-unit-test": "yarn rebuild better-sqlite3",
"rebuild:for-electron": "yarn electron-rebuild",
"test": "playwright test"
@@ -32,6 +28,7 @@
"main": "./dist/layers/main/index.js",
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/native": "workspace:*",
"@electron-forge/cli": "^6.1.1",
"@electron-forge/core": "^6.1.1",
"@electron-forge/core-utils": "^6.1.1",
@@ -44,16 +41,18 @@
"@electron/remote": "2.0.9",
"@types/better-sqlite3": "^7.6.4",
"@types/fs-extra": "^11.0.1",
"@types/uuid": "^9.0.1",
"cross-env": "7.0.3",
"electron": "24.3.0",
"electron-log": "^5.0.0-beta.23",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
"esbuild": "^0.17.18",
"esbuild": "^0.17.19",
"fs-extra": "^11.1.1",
"playwright": "^1.33.0",
"ts-node": "^10.9.1",
"undici": "^5.22.0",
"undici": "^5.22.1",
"uuid": "^9.0.0",
"zx": "^7.2.2"
},
"dependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -8,6 +8,11 @@ import { config } from './common.mjs';
const NODE_ENV =
process.env.NODE_ENV === 'development' ? 'development' : 'production';
if (process.platform === 'win32') {
$.shell = true;
$.prefix = '';
}
async function buildLayers() {
const common = config();
await esbuild.build(common.preload);

View File

@@ -12,16 +12,6 @@ const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
setup(build) {
// Mark native Node.js modules as external
build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => {
return { path: args.path, external: true };
});
},
};
// List of env that will be replaced by esbuild
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
@@ -50,9 +40,13 @@ export const config = () => {
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron', 'yjs', 'better-sqlite3', 'electron-updater'],
plugins: [nativeNodeModulesPlugin],
define: define,
format: 'cjs',
loader: {
'.node': 'copy',
},
assetNames: '[name]',
treeShaking: true,
},
preload: {
entryPoints: [resolve(root, './layers/preload/src/index.ts')],
@@ -61,7 +55,6 @@ export const config = () => {
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron', '../main/exposed-meta'],
plugins: [nativeNodeModulesPlugin],
define: define,
},
};

View File

@@ -32,9 +32,6 @@ if (process.platform === 'win32') {
cd(repoRootDir);
// step 1: build electron resources
await $`yarn workspace @affine/electron build-layers`;
// step 2: build web (nextjs) dist
if (!process.env.SKIP_WEB_BUILD) {
process.env.ENABLE_LEGACY_PROVIDER = 'false';
@@ -59,6 +56,17 @@ if (!process.env.SKIP_WEB_BUILD) {
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
}
// step 3: update app-updater.yml content with build type in resources folder
if (process.env.BUILD_TYPE === 'internal') {
const appUpdaterYml = path.join(publicDistDir, 'app-update.yml');
const appUpdaterYmlContent = await fs.readFile(appUpdaterYml, 'utf-8');
const newAppUpdaterYmlContent = appUpdaterYmlContent.replace(
'AFFiNE',
'AFFiNE-Releases'
);
await fs.writeFile(appUpdaterYml, newAppUpdaterYmlContent);
}
/// --------
/// --------
/// --------

View File

@@ -1,7 +1,3 @@
#!/usr/bin/env zx
/* eslint-disable @typescript-eslint/no-restricted-imports */
import 'zx/globals';
const mainDistDir = path.resolve(__dirname, '../dist/layers/main');
// be careful and avoid any side effects in

View File

@@ -1,3 +1,5 @@
import { platform } from 'node:os';
import { expect } from '@playwright/test';
import { test } from './fixture';
@@ -11,6 +13,73 @@ test('new page', async ({ page, workspace }) => {
expect(flavour).toBe('local');
});
// macOS only
if (platform() === 'darwin') {
test('app sidebar router forward/back', async ({ page }) => {
await page.getByTestId('help-island').click();
await page.getByTestId('easy-guide').click();
await page.getByTestId('onboarding-modal-next-button').click();
await page.getByTestId('onboarding-modal-close-button').click();
{
// create pages
await page.waitForTimeout(500);
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.focus('.affine-default-page-block-title');
await page.type('.affine-default-page-block-title', 'test1', {
delay: 100,
});
await page.waitForTimeout(500);
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.focus('.affine-default-page-block-title');
await page.type('.affine-default-page-block-title', 'test2', {
delay: 100,
});
await page.waitForTimeout(500);
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.focus('.affine-default-page-block-title');
await page.type('.affine-default-page-block-title', 'test3', {
delay: 100,
});
}
{
const title = (await page
.locator('.affine-default-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('test3');
}
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
await page.waitForTimeout(1000);
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
await page.waitForTimeout(1000);
{
const title = (await page
.locator('.affine-default-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('test1');
}
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
await page.waitForTimeout(1000);
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
await page.waitForTimeout(1000);
{
const title = (await page
.locator('.affine-default-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('test3');
}
});
}
test('app theme', async ({ page, electronApp }) => {
const root = page.locator('html');
{

View File

@@ -3,9 +3,14 @@
/* eslint-disable no-empty-pattern */
import crypto from 'node:crypto';
import { resolve } from 'node:path';
import { join, resolve } from 'node:path';
import { test as base } from '@affine-test/kit/playwright';
import {
enableCoverage,
istanbulTempDir,
test as base,
testResultDir,
} from '@affine-test/kit/playwright';
import fs from 'fs-extra';
import type { ElectronApplication, Page } from 'playwright';
import { _electron as electron } from 'playwright';
@@ -44,7 +49,29 @@ export const test = base.extend<{
});
// wat for blocksuite to be loaded
await page.waitForSelector('v-line');
if (enableCoverage) {
await fs.promises.mkdir(istanbulTempDir, { recursive: true });
await page.exposeFunction(
'collectIstanbulCoverage',
(coverageJSON?: string) => {
if (coverageJSON)
fs.writeFileSync(
join(
istanbulTempDir,
`playwright_coverage_${generateUUID()}.json`
),
coverageJSON
);
}
);
}
await use(page);
if (enableCoverage) {
await page.evaluate(() =>
// @ts-expect-error
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
);
}
await page.close();
if (logFilePath) {
const logs = await fs.readFile(logFilePath, 'utf-8');
@@ -54,9 +81,19 @@ export const test = base.extend<{
electronApp: async ({}, use) => {
// a random id to avoid conflicts between tests
const id = generateUUID();
const ext = process.platform === 'win32' ? '.cmd' : '';
const electronApp = await electron.launch({
args: [resolve(__dirname, '..'), '--app-name', 'affine-test-' + id],
executablePath: resolve(__dirname, '../node_modules/.bin/electron'),
executablePath: resolve(
__dirname,
'..',
'node_modules',
'.bin',
`electron${ext}`
),
recordVideo: {
dir: testResultDir,
},
colorScheme: 'light',
});
await use(electronApp);

View File

@@ -1,7 +1,8 @@
import { execSync } from 'node:child_process';
import { join } from 'node:path';
export default async function () {
execSync('yarn ts-node-esm scripts/', {
cwd: path.join(__dirname, '..'),
cwd: join(__dirname, '..'),
});
}

View File

@@ -9,14 +9,16 @@
"types": ["node"],
"outDir": "dist",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
"resolveJsonModule": true
},
"include": ["**/*.ts", "**/*.tsx", "package.json"],
"exclude": ["out", "dist", "node_modules"],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "../../packages/native"
}
],
"ts-node": {

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
provider = "postgresql"

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.6.0-canary.0",
"version": "0.6.0-canary.3",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -20,8 +20,8 @@
"@nestjs/core": "^9.4.0",
"@nestjs/graphql": "^11.0.5",
"@nestjs/platform-express": "^9.4.0",
"@node-rs/bcrypt": "^1.7.1",
"@prisma/client": "^4.14.0",
"bcrypt": "^5.1.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"graphql": "^16.6.0",
@@ -34,11 +34,10 @@
},
"devDependencies": {
"@nestjs/testing": "^9.4.0",
"@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.7",
"@types/node": "^18.16.9",
"@types/supertest": "^2.0.12",
"c8": "^7.13.0",
"nodemon": "^2.0.22",

View File

@@ -1,6 +1,6 @@
import crypto from 'node:crypto';
import { genSalt } from 'bcrypt';
import { genSalt } from '@node-rs/bcrypt';
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1',

View File

@@ -169,10 +169,6 @@ export interface AFFiNEConfig {
* authentication config
*/
auth: {
/**
* Application sign key secret
*/
readonly salt: string;
/**
* Application access token expiration time
*/

View File

@@ -56,7 +56,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
debug: true,
},
auth: {
salt: '$2b$10$x4VDo2nmlo74yB5jflNhlu',
accessTokenExpiresIn: '1h',
refreshTokenExpiresIn: '7d',
publicKey: examplePublicKey,

View File

@@ -3,8 +3,8 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { compare, hash } from '@node-rs/bcrypt';
import { User } from '@prisma/client';
import { compare, hash } from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Config } from '../../config';
@@ -69,7 +69,7 @@ export class AuthService {
}
async register(name: string, email: string, password: string): Promise<User> {
const hashedPassword = await hash(password, this.config.auth.salt);
const hashedPassword = await hash(password);
const user = await this.prisma.user.findFirst({
where: {

View File

@@ -3,8 +3,8 @@ import { afterEach, beforeEach, describe, test } from 'node:test';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { hash } from '@node-rs/bcrypt';
import { PrismaClient } from '@prisma/client';
import { hash } from 'bcrypt';
import request from 'supertest';
import { AppModule } from '../app';
@@ -27,7 +27,7 @@ describe('AppModule', () => {
id: '1',
name: 'Alex Yang',
email: 'alex.yang@example.org',
password: await hash('123456', globalThis.AFFiNE.auth.salt),
password: await hash('123456'),
},
});
});

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/web",
"private": true,
"version": "0.6.0-canary.0",
"version": "0.6.0-canary.3",
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -19,11 +19,12 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230509150141-055b5702-nightly",
"@blocksuite/editor": "0.0.0-20230509150141-055b5702-nightly",
"@blocksuite/global": "0.0.0-20230509150141-055b5702-nightly",
"@blocksuite/icons": "^2.1.15",
"@blocksuite/store": "0.0.0-20230509150141-055b5702-nightly",
"@blocksuite/blocks": "0.0.0-20230514141009-705c0fac-nightly",
"@blocksuite/editor": "0.0.0-20230514141009-705c0fac-nightly",
"@blocksuite/global": "0.0.0-20230514141009-705c0fac-nightly",
"@blocksuite/icons": "^2.1.16",
"@blocksuite/lit": "0.0.0-20230514141009-705c0fac-nightly",
"@blocksuite/store": "0.0.0-20230514141009-705c0fac-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
@@ -39,12 +40,12 @@
"dayjs": "^1.11.7",
"graphql": "^16.6.0",
"jotai": "^2.1.0",
"jotai-devtools": "^0.5.2",
"jotai-devtools": "^0.5.3",
"lit": "^2.7.4",
"lottie-web": "^5.11.0",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "18.3.0-canary-16d053d59-20230506",
"react-dom": "18.3.0-canary-16d053d59-20230506",
"react-is": "^18.2.0",
"rxjs": "^7.8.1",
"swr": "^2.1.5",
@@ -66,9 +67,9 @@
"@vanilla-extract/next-plugin": "^2.1.2",
"dotenv": "^16.0.3",
"eslint": "^8.40.0",
"eslint-config-next": "^13.4.1",
"eslint-config-next": "^13.4.2",
"eslint-plugin-unicorn": "^47.0.0",
"next": "^13.4.1",
"next": "^13.4.2",
"next-debug-local": "^0.1.5",
"next-router-mock": "^0.9.3",
"raw-loader": "^4.0.2",

View File

@@ -19,7 +19,7 @@ import type { Page } from '@blocksuite/store';
import { createStore } from 'jotai';
import { describe, expect, test } from 'vitest';
import { WorkspacePlugins } from '../../plugins';
import { WorkspaceAdapters } from '../../plugins';
import { rootCurrentWorkspaceAtom } from '../root';
describe('currentWorkspace atom', () => {
@@ -45,7 +45,7 @@ describe('currentWorkspace atom', () => {
const provider = createIndexedDBDownloadProvider(workspace);
provider.sync();
await provider.whenReady;
const workspaceId = await WorkspacePlugins[
const workspaceId = await WorkspaceAdapters[
WorkspaceFlavour.LOCAL
].CRUD.create(workspace);
store.set(rootWorkspacesMetadataAtom, [

View File

@@ -0,0 +1,99 @@
import { atom, useAtom, useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useCallback, useEffect } from 'react';
export type History = {
stack: string[];
current: number;
skip: boolean;
};
export const MAX_HISTORY = 50;
export const historyBaseAtom = atom<History>({
stack: [],
current: 0,
skip: false,
});
// fixme(himself65): don't use hooks, use atom lifecycle instead
export function useTrackRouterHistoryEffect() {
const setBase = useSetAtom(historyBaseAtom);
const router = useRouter();
useEffect(() => {
const callback = (url: string) => {
setBase(prev => {
console.log('push', url, prev.skip, prev.stack.length, prev.current);
if (prev.skip) {
return {
stack: [...prev.stack],
current: prev.current,
skip: false,
};
} else {
if (prev.current < prev.stack.length - 1) {
const newStack = prev.stack.slice(0, prev.current);
newStack.push(url);
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
} else {
const newStack = [...prev.stack, url];
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
}
}
});
};
router.events.on('routeChangeComplete', callback);
return () => {
router.events.off('routeChangeComplete', callback);
};
}, [router.events, setBase]);
}
export function useHistoryAtom() {
const router = useRouter();
const [base, setBase] = useAtom(historyBaseAtom);
return [
base,
useCallback(
(forward: boolean) => {
setBase(prev => {
if (forward) {
const target = Math.min(prev.stack.length - 1, prev.current + 1);
const url = prev.stack[target];
void router.push(url);
return {
...prev,
current: target,
skip: true,
};
} else {
const target = Math.max(0, prev.current - 1);
const url = prev.stack[target];
void router.push(url);
return {
...prev,
current: target,
skip: true,
};
}
});
},
[router, setBase]
),
] as const;
}

View File

@@ -12,7 +12,7 @@ import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
import { WorkspacePlugins } from '../plugins';
import { WorkspaceAdapters } from '../plugins';
const logger = new DebugLogger('web:atoms');
@@ -25,7 +25,7 @@ export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] {
const Plugins = Object.values(WorkspacePlugins).sort(
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
@@ -40,17 +40,24 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
}
setAtom(metadata => {
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
const abortController = new AbortController();
// next tick to make sure the hydration is correct
const id = setTimeout(() => {
setAtom(metadata => {
if (abortController.signal.aborted) return metadata;
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
}, 0);
if (environment.isDesktop) {
window.apis?.workspace.list().then(workspaceIDs => {
if (abortController.signal.aborted) return;
const newMetadata = workspaceIDs.map(w => ({
id: w[0],
flavour: WorkspaceFlavour.LOCAL,
@@ -63,6 +70,11 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
});
});
}
return () => {
clearTimeout(id);
abortController.abort();
};
};
/**

View File

@@ -13,7 +13,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import { WorkspaceAdapters } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
@@ -22,7 +22,7 @@ const logger = new DebugLogger('web:atoms:root');
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
const flavours: string[] = Object.values(WorkspaceAdapters).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
@@ -38,7 +38,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
WorkspaceAdapters[workspace.flavour as keyof typeof WorkspaceAdapters];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id).then(workspace => {
@@ -93,7 +93,7 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
const workspace = await WorkspacePlugins[targetWorkspace.flavour].CRUD.get(
const workspace = await WorkspaceAdapters[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
if (!workspace) {

View File

@@ -30,6 +30,7 @@ export const createAffineDownloadProvider = (
new Uint8Array(hashMap.get(id) as ArrayBuffer)
);
connected = true;
callbacks.forEach(cb => cb());
return;
}
affineApis
@@ -41,6 +42,8 @@ export const createAffineDownloadProvider = (
blockSuiteWorkspace.doc,
new Uint8Array(binary)
);
connected = true;
callbacks.forEach(cb => cb());
})
.catch(e => {
providerLogger.error('downloadWorkspace', e);

View File

@@ -74,7 +74,7 @@ const NameWorkspaceContent = ({
data-testid="create-workspace-input"
onKeyDown={handleKeyDown}
placeholder={t['Set a Workspace name']()}
maxLength={15} // TODO: the max workspace name length?
maxLength={64}
minLength={0}
onChange={value => {
setWorkspaceName(value);

View File

@@ -7,7 +7,7 @@ export const StyledSidebarSwitch = styled(IconButton, {
})<{ visible: boolean }>(({ visible }) => {
return {
opacity: visible ? 1 : 0,
WebkitAppRegion: 'no-drag',
WebkitAppRegion: visible ? 'no-drag' : 'drag',
transition: 'all 0.2s ease-in-out',
};
});

View File

@@ -1,4 +1,6 @@
import { Empty, IconButton, Modal, ModalWrapper } from '@affine/component';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import type React from 'react';
@@ -20,6 +22,7 @@ interface TmpDisableAffineCloudModalProps {
export const TmpDisableAffineCloudModal: React.FC<
TmpDisableAffineCloudModalProps
> = ({ open, onClose }) => {
const t = useAFFiNEI18N();
return (
<Modal
data-testid="disable-affine-cloud-modal"
@@ -37,21 +40,25 @@ export const TmpDisableAffineCloudModal: React.FC<
</IconButton>
</Header>
<Content>
<ContentTitle>AFFiNE Cloud is upgrading now.</ContentTitle>
<ContentTitle>
{t['com.affine.cloudTempDisable.title']()}
</ContentTitle>
<StyleTips>
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 join the &nbsp;
<a
href="https://community.affine.pro"
target="_blank"
style={{
color: 'var(--affine-link-color)',
}}
>
AFFiNE Community
</a>
.
<Trans i18nKey="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
<a
href="https://6dxre9ihosp.typeform.com/to/B8IHwuyy"
target="_blank"
style={{
color: 'var(--affine-link-color)',
}}
>
AFFiNE Cloud Signup
</a>
.
</Trans>
</StyleTips>
<StyleImage>
<Empty
@@ -69,7 +76,7 @@ export const TmpDisableAffineCloudModal: React.FC<
onClose();
}}
>
Got it
{t['Got it']()}
</StyleButton>
</StyleButtonContainer>
</Content>

View File

@@ -2,7 +2,10 @@ import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
import { config } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PermissionType } from '@affine/workspace/affine/api';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
@@ -37,7 +40,7 @@ import {
const AffineRemoteCollaborationPanel: React.FC<
Omit<PanelProps, 'workspace'> & {
workspace: AffineWorkspace;
workspace: AffineLegacyCloudWorkspace;
}
> = ({ workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
@@ -214,7 +217,7 @@ const LocalCollaborationPanel: React.FC<
export const CollaborationPanel: React.FC<PanelProps> = props => {
switch (props.workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
const workspace = props.workspace as AffineWorkspace;
const workspace = props.workspace as AffineLegacyCloudWorkspace;
return (
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
);

View File

@@ -137,7 +137,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
value={input}
data-testid="workspace-name-input"
placeholder={t['Workspace Name']()}
maxLength={50}
maxLength={64}
minLength={0}
onChange={newName => {
setInput(newName);

View File

@@ -7,7 +7,10 @@ import {
} from '@affine/component';
import { config } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { Box } from '@mui/material';
import type React from 'react';
@@ -26,7 +29,7 @@ export type PublishPanelProps = WorkspaceSettingDetailProps & {
};
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
workspace: AffineWorkspace;
workspace: AffineLegacyCloudWorkspace;
};
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({

View File

@@ -19,7 +19,7 @@ import { pageListEmptyStyle } from './index.css';
export type BlockSuitePageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
listType: 'all' | 'trash' | 'favorite' | 'shared' | 'public';
listType: 'all' | 'trash' | 'shared' | 'public';
isPublic?: true;
onOpenPage: (pageId: string, newTab?: boolean) => void;
};
@@ -31,7 +31,6 @@ const filter = {
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
return !parentMeta?.trash && pageMeta.trash;
},
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
@@ -52,9 +51,6 @@ const PageListEmpty = (props: {
if (listType === 'all') {
return t['emptyAllPages']();
}
if (listType === 'favorite') {
return t['emptyFavorite']();
}
if (listType === 'trash') {
return t['emptyTrash']();
}
@@ -102,7 +98,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
pageId: pageMeta.id,
title: pageMeta.title,
createDate: formatDate(pageMeta.createDate),
updatedDate: formatDate(pageMeta.updatedDate),
updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
onClickPage: () => onOpenPage(pageMeta.id),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
@@ -129,7 +125,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
favorite: !!pageMeta.favorite,
isPublicPage: !!pageMeta.isPublic,
createDate: formatDate(pageMeta.createDate),
updatedDate: formatDate(pageMeta.updatedDate),
updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
onClickPage: () => onOpenPage(pageMeta.id),
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
onClickRestore: () => {

View File

@@ -40,7 +40,6 @@ const CommonMenu = () => {
return (
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
width={276}
content={content}
placement="bottom"
disablePortal={true}
@@ -137,7 +136,6 @@ const PageMenu = () => {
<>
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
width={276}
content={EditMenu}
placement="bottom-end"
disablePortal={true}

View File

@@ -1,6 +1,9 @@
import { ShareMenu } from '@affine/component/share-menu';
import { config } from '@affine/env';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { Page } from '@blocksuite/store';
import { assertEquals } from '@blocksuite/store';
@@ -19,12 +22,12 @@ import type { BaseHeaderProps } from '../header';
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
// todo: these hooks should be moved to the top level
const togglePublish = useToggleWorkspacePublish(
props.workspace as AffineWorkspace
props.workspace as AffineLegacyCloudWorkspace
);
const helper = useRouterHelper(useRouter());
return (
<ShareMenu
workspace={props.workspace as AffineWorkspace}
workspace={props.workspace as AffineLegacyCloudWorkspace}
currentPage={props.currentPage as Page}
onEnableAffineCloud={useCallback(async () => {
throw new Unreachable(

View File

@@ -30,10 +30,12 @@ export const StyledThemeButton = styled('button')<{
active: boolean;
}>(({ active }) => {
return {
padding: '0 8px',
height: '100%',
flex: 1,
cursor: 'pointer',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
whiteSpace: 'nowrap',
};
});
export const StyledVerticalDivider = styled('div')(() => {

View File

@@ -1,10 +1,13 @@
import { BrowserWarning } from '@affine/component/affine-banner';
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import {
appSidebarFloatingAtom,
appSidebarOpenAtom,
} from '@affine/component/app-sidebar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import {
forwardRef,
@@ -161,9 +164,11 @@ export const Header = forwardRef<
setShowWarning(shouldShowWarning());
setShowGuideDownloadClientTip(shouldShowGuideDownloadClientTip);
}, [shouldShowGuideDownloadClientTip]);
const [open] = useAtom(appSidebarOpenAtom);
const open = useAtomValue(appSidebarOpenAtom);
const t = useAFFiNEI18N();
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
const mode = useCurrentMode();
return (
<div
@@ -171,6 +176,7 @@ export const Header = forwardRef<
ref={ref}
data-has-warning={showWarning}
data-open={open}
data-sidebar-floating={appSidebarFloating}
{...props}
>
{showGuideDownloadClientTip ? (
@@ -189,7 +195,6 @@ export const Header = forwardRef<
className={styles.header}
data-has-warning={showWarning}
data-testid="editor-header-items"
data-tauri-drag-region
data-is-edgeless={mode === 'edgeless'}
>
<Suspense>

View File

@@ -1,16 +1,15 @@
import type { PopperProps } from '@affine/component';
import { QuickSearchTips } from '@affine/component';
import { getEnvironment } from '@affine/env';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useRef } from 'react';
import { useSetAtom } from 'jotai';
import type {
FC,
HTMLAttributes,
PropsWithChildren,
ReactElement,
} from 'react';
import { useRef } from 'react';
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
import { guideQuickSearchTipsAtom } from '../../../atoms/guide';
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
import { openQuickSearchModalAtom } from '../../../atoms';
import { QuickSearchButton } from '../../pure/quick-search-button';
import { EditorModeSwitch } from './editor-mode-switch';
import type { BaseHeaderProps } from './header';
@@ -19,69 +18,21 @@ import * as styles from './styles.css';
export type WorkspaceHeaderProps = BaseHeaderProps;
export const WorkspaceHeader = forwardRef<
HTMLDivElement,
export const WorkspaceHeader: FC<
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
>((props, ref) => {
> = (props): ReactElement => {
const { workspace, currentPage, children, isPublic } = props;
// fixme(himself65): remove this atom and move it to props
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
meta => meta.id === currentPage?.id
);
const headerRef = useRef<HTMLDivElement>(null);
assertExists(pageMeta);
const title = pageMeta.title;
const isMac = () => {
const env = getEnvironment();
return env.isBrowser && env.isMacOs;
};
const popperRef: PopperProps['popperRef'] = useRef(null);
const [showQuickSearchTips, setShowQuickSearchTips] = useAtom(
guideQuickSearchTipsAtom
);
useElementResizeEffect(
useAtomValue(currentEditorAtom),
useCallback(() => {
if (showQuickSearchTips || !popperRef.current) {
return;
}
popperRef.current.update();
}, [showQuickSearchTips])
);
const TipsContent = (
<div className={styles.quickSearchTipContent}>
<div>
Click button
{
<span
style={{
fontSize: '24px',
verticalAlign: 'middle',
}}
>
<ArrowDownSmallIcon />
</span>
}
or use
{isMac() ? ' ⌘ + K' : ' Ctrl + K'} to activate Quick Search. Then you
can search keywords or quickly open recently viewed pages.
</div>
<div
className={styles.quickSearchTipButton}
data-testid="quick-search-got-it"
onClick={() => setShowQuickSearchTips(false)}
>
Got it
</div>
</div>
);
return (
<Header ref={ref} {...props}>
<Header ref={headerRef} {...props}>
{children}
{!isPublic && currentPage && (
<div className={styles.titleContainer}>
@@ -96,27 +47,19 @@ export const WorkspaceHeader = forwardRef<
/>
</div>
<div className={styles.title}>{title || 'Untitled'}</div>
<QuickSearchTips
data-testid="quick-search-tips"
content={TipsContent}
placement="bottom"
popperRef={popperRef}
open={showQuickSearchTips}
offset={[0, -5]}
>
<div className={styles.searchArrowWrapper}>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</div>
</QuickSearchTips>
<div className={styles.searchArrowWrapper}>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</div>
</div>
</div>
)}
</Header>
);
});
};
WorkspaceHeader.displayName = 'BlockSuiteEditorHeader';

View File

@@ -1,3 +1,4 @@
import type { ComplexStyleRule } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
export const headerContainer = style({
@@ -6,24 +7,16 @@ export const headerContainer = style({
position: 'sticky',
top: 0,
background: 'var(--affine-background-primary-color)',
WebkitAppRegion: 'drag',
zIndex: 'var(--affine-z-index-popover)',
'@media': {
'(max-width: 768px)': {
selectors: {
'&[data-open="true"]': {
// @ts-ignore
WebkitAppRegion: 'no-drag',
},
},
},
},
selectors: {
'&[data-has-warning="true"]': {
height: '96px',
},
'&[data-sidebar-floating="false"]': {
WebkitAppRegion: 'drag',
},
},
});
} as ComplexStyleRule);
export const header = style({
flexShrink: 0,
@@ -68,13 +61,12 @@ export const title = style({
'(max-width: 768px)': {
selectors: {
'&[data-open="true"]': {
// @ts-ignore
WebkitAppRegion: 'no-drag',
},
},
},
},
});
} as ComplexStyleRule);
export const titleWrapper = style({
height: '100%',
@@ -196,7 +188,6 @@ export const windowAppControlsWrapper = style({
});
export const windowAppControl = style({
// @ts-ignore
WebkitAppRegion: 'no-drag',
cursor: 'pointer',
display: 'inline-flex',
@@ -213,4 +204,4 @@ export const windowAppControl = style({
background: 'var(--affine-background-tertiary-color)',
},
},
});
} as ComplexStyleRule);

View File

@@ -1,4 +1,3 @@
import { styled } from '@affine/component';
import { AffineLoading } from '@affine/component/affine-loading';
import { memo, Suspense } from 'react';
@@ -17,34 +16,9 @@ export const Loading = memo(function Loading() {
);
});
// Used for the full page loading
const StyledLoadingContainer = styled('div')(() => {
return {
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#6880FF',
flexDirection: 'column',
h1: {
fontSize: '2em',
marginTop: '15px',
fontWeight: '600',
},
};
});
/**
* @deprecated use skeleton instead
*/
export const PageLoading = () => {
// We disable the loading on desktop, because want it looks faster.
// This is a design requirement.
if (environment.isDesktop) {
return null;
}
return (
<StyledLoadingContainer>
<Loading />
</StyledLoadingContainer>
);
return null;
};
export default PageLoading;

View File

@@ -1,7 +1,6 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
FavoriteIcon,
FolderIcon,
SettingsIcon,
} from '@blocksuite/icons';
@@ -24,11 +23,6 @@ export const useSwitchToConfig = (
href: pathGenerator.all(workspaceId),
icon: FolderIcon,
},
{
title: t['Favorites'](),
href: pathGenerator.favorite(workspaceId),
icon: FavoriteIcon,
},
{
title: t['Workspace Settings'](),
href: pathGenerator.setting(workspaceId),

View File

@@ -9,7 +9,10 @@ import {
import { WorkspaceList } from '@affine/component/workspace-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core';
@@ -109,7 +112,7 @@ export const WorkspaceListModal = ({
items={
workspaces.filter(
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
) as (AffineWorkspace | LocalWorkspace)[]
) as (AffineLegacyCloudWorkspace | LocalWorkspace)[]
}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}

View File

@@ -6,7 +6,7 @@ export const StyledSelectorContainer = styled('div')(() => {
display: 'flex',
alignItems: 'center',
padding: '0 6px',
marginBottom: '16px',
margin: '0 -6px',
borderRadius: '8px',
color: 'var(--affine-text-primary-color)',
':hover': {

View File

@@ -1,13 +1,10 @@
import { MenuItem } from '@affine/component/app-sidebar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { StyledCollapseItem } from '../shared-styles';
export const EmptyItem = () => {
const t = useAFFiNEI18N();
return (
<StyledCollapseItem disable={true} textWrap={true}>
{t['Favorite pages for easy access']()}
</StyledCollapseItem>
<MenuItem disabled={true}>{t['Favorite pages for easy access']()}</MenuItem>
);
};

View File

@@ -1,20 +1,18 @@
import { MuiCollapse } from '@affine/component';
import { MenuLinkItem } from '@affine/component/app-sidebar';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import type { FavoriteListProps } from '../index';
import { StyledCollapseItem } from '../shared-styles';
import EmptyItem from './empty-item';
export const FavoriteList = ({
pageMeta,
openPage,
showList,
}: FavoriteListProps) => {
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
const router = useRouter();
const record = useAtomValue(workspacePreferredModeAtom);
const pageMeta = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const workspaceId = currentWorkspace.id;
const favoriteList = useMemo(
() => pageMeta.filter(p => p.favorite && !p.trash),
@@ -22,45 +20,25 @@ export const FavoriteList = ({
);
return (
<MuiCollapse
in={showList}
style={{
maxHeight: 300,
overflowY: 'auto',
marginLeft: '16px',
}}
>
<>
{favoriteList.map((pageMeta, index) => {
const active = router.query.pageId === pageMeta.id;
const icon =
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
return (
<div key={`${pageMeta}-${index}`}>
<StyledCollapseItem
data-testid={`favorite-list-item-${pageMeta.id}`}
active={active}
ref={ref => {
if (ref && active) {
ref.scrollIntoView({ behavior: 'smooth' });
}
}}
onClick={() => {
if (active) {
return;
}
openPage(pageMeta.id);
}}
>
{record[pageMeta.id] === 'edgeless' ? (
<EdgelessIcon />
) : (
<PageIcon />
)}
<span>{pageMeta.title || 'Untitled'}</span>
</StyledCollapseItem>
</div>
<MenuLinkItem
key={`${pageMeta}-${index}`}
data-testid={`favorite-list-item-${pageMeta.id}`}
active={active}
href={`/workspace/${workspaceId}/${pageMeta.id}`}
icon={icon}
>
<span>{pageMeta.title || 'Untitled'}</span>
</MenuLinkItem>
);
})}
{favoriteList.length === 0 && <EmptyItem />}
</MuiCollapse>
</>
);
};

View File

@@ -1,66 +1 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownSmallIcon, FavoriteIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useCallback, useState } from 'react';
import type { AllWorkspace } from '../../../../shared';
import type { WorkSpaceSliderBarProps } from '../index';
import { StyledCollapseButton, StyledListItem } from '../shared-styles';
import { StyledLink } from '../style';
import FavoriteList from './favorite-list';
export const Favorite = ({
currentPath,
paths,
currentPageId,
openPage,
currentWorkspace,
}: Pick<
WorkSpaceSliderBarProps,
'currentPath' | 'paths' | 'currentPageId' | 'openPage'
> & {
currentWorkspace: AllWorkspace;
}) => {
const currentWorkspaceId = currentWorkspace.id;
const pageMeta = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const [showSubFavorite, setOpenSubFavorite] = useState(true);
const t = useAFFiNEI18N();
return (
<>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.favorite(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.favorite(currentWorkspaceId),
}}
>
<FavoriteIcon />
{t['Favorites']()}
</StyledLink>
<StyledCollapseButton
onClick={useCallback(() => {
setOpenSubFavorite(!showSubFavorite);
}, [showSubFavorite])}
collapse={showSubFavorite}
>
<ArrowDownSmallIcon />
</StyledCollapseButton>
</StyledListItem>
<FavoriteList
currentPageId={currentPageId}
showList={showSubFavorite}
openPage={openPage}
pageMeta={pageMeta}
/>
</>
);
};
export default Favorite;
export * from './favorite-list';

View File

@@ -1,13 +0,0 @@
export const Arrow = () => {
return (
<svg
width="6"
height="10"
viewBox="0 0 6 10"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0.354 9.22997C0.201333 9.0773 0.125 8.91764 0.125 8.75097C0.125 8.5843 0.201333 8.42464 0.354 8.27197L3.625 5.00097L0.354 1.72997C0.201333 1.5773 0.125 1.41764 0.125 1.25097C0.125 1.0843 0.201333 0.924636 0.354 0.771969C0.506667 0.619302 0.666333 0.542969 0.833 0.542969C0.999667 0.542969 1.15933 0.619302 1.312 0.771969L4.979 4.43897C5.06233 4.52164 5.125 4.61164 5.167 4.70897C5.20833 4.8063 5.229 4.90364 5.229 5.00097C5.229 5.0983 5.20833 5.19564 5.167 5.29297C5.125 5.3903 5.06233 5.4803 4.979 5.56297L1.312 9.22997C1.15933 9.38264 0.999667 9.45897 0.833 9.45897C0.666333 9.45897 0.506667 9.38264 0.354 9.22997Z" />
</svg>
);
};

View File

@@ -1,12 +1,9 @@
import type { Page, PageMeta } from '@blocksuite/store';
import type { Page } from '@blocksuite/store';
import type { AllWorkspace } from '../../../shared';
export type FavoriteListProps = {
currentPageId: string | null;
openPage: (pageId: string) => void;
showList: boolean;
pageMeta: PageMeta[];
currentWorkspace: AllWorkspace;
};
export type WorkSpaceSliderBarProps = {

View File

@@ -1,31 +0,0 @@
import { IconButton } from '@affine/component';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
import { StyledRouteNavigationWrapper } from './shared-styles';
export const RouteNavigation = () => {
if (!environment.isDesktop) {
return <></>;
}
return (
<StyledRouteNavigationWrapper>
<IconButton
size="middle"
onClick={() => {
window.history.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
size="middle"
onClick={() => {
window.history.forward();
}}
style={{ marginLeft: '32px' }}
>
<ArrowRightSmallIcon />
</IconButton>
</StyledRouteNavigationWrapper>
);
};

View File

@@ -1,7 +1,13 @@
import {
AddPageButton,
AppSidebar,
appSidebarOpenAtom,
ResizeIndicator,
AppUpdaterButton,
CategoryDivider,
MenuLinkItem,
QuickSearchInput,
SidebarContainer,
SidebarScrollableContainer,
} from '@affine/component/app-sidebar';
import { config } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -9,27 +15,18 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
FolderIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
ShareIcon,
} from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import type { ReactElement, UIEvent } from 'react';
import { useAtom } from 'jotai';
import type { ReactElement } from 'react';
import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useHistoryAtom } from '../../atoms/history';
import type { AllWorkspace } from '../../shared';
import ChangeLog from '../pure/workspace-slider-bar/changeLog';
import Favorite from '../pure/workspace-slider-bar/favorite';
import { StyledListItem } from '../pure/workspace-slider-bar/shared-styles';
import {
StyledLink,
StyledNewPageButton,
StyledScrollWrapper,
StyledSliderBarInnerWrapper,
} from '../pure/workspace-slider-bar/style';
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
export type RootAppSidebarProps = {
@@ -37,19 +34,37 @@ export type RootAppSidebarProps = {
onOpenQuickSearchModal: () => void;
onOpenWorkspaceListModal: () => void;
currentWorkspace: AllWorkspace | null;
currentPageId: string | null;
openPage: (pageId: string) => void;
createPage: () => Page;
currentPath: string;
paths: {
all: (workspaceId: string) => string;
favorite: (workspaceId: string) => string;
trash: (workspaceId: string) => string;
setting: (workspaceId: string) => string;
shared: (workspaceId: string) => string;
};
};
const RouteMenuLinkItem = ({
currentPath,
path,
icon,
children,
...props
}: {
currentPath: string; // todo: pass through useRouter?
path?: string | null;
icon: ReactElement;
children?: ReactElement;
} & React.HTMLAttributes<HTMLDivElement>) => {
const active = currentPath === path;
return (
<MenuLinkItem {...props} active={active} href={path ?? ''} icon={icon}>
{children}
</MenuLinkItem>
);
};
/**
* This is for the whole affine app sidebar.
* This component wraps the app sidebar in `@affine/component` with logic and data.
@@ -58,7 +73,6 @@ export type RootAppSidebarProps = {
*/
export const RootAppSidebar = ({
currentWorkspace,
currentPageId,
openPage,
createPage,
currentPath,
@@ -69,171 +83,118 @@ export const RootAppSidebar = ({
const currentWorkspaceId = currentWorkspace?.id || null;
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
const onClickNewPage = useCallback(async () => {
const page = await createPage();
openPage(page.id);
}, [createPage, openPage]);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
return window.events?.applicationMenu.onNewPageAction(onClickNewPage);
}
}, [onClickNewPage]);
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop && typeof sidebarOpen === 'boolean') {
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen);
}
}, [sidebarOpen]);
const [ref, setRef] = useState<HTMLElement | null>(null);
const handleQuickSearchButtonKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onOpenQuickSearchModal();
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if ((e.key === '/' && e.metaKey) || (e.key === '/' && e.ctrlKey)) {
setSidebarOpen(!sidebarOpen);
}
},
[onOpenQuickSearchModal]
);
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [sidebarOpen, setSidebarOpen]);
const [history, setHistory] = useHistoryAtom();
const router = useMemo(() => {
return {
forward: () => {
setHistory(true);
},
back: () => {
setHistory(false);
},
history,
};
}, [history, setHistory]);
return (
<>
<AppSidebar
ref={setRef}
footer={
<StyledNewPageButton
data-testid="new-page-button"
onClick={onClickNewPage}
>
<PlusIcon /> {t['New Page']()}
</StyledNewPageButton>
}
>
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
<AppSidebar router={router}>
<SidebarContainer>
<WorkspaceSelector
currentWorkspace={currentWorkspace}
onClick={onOpenWorkspaceListModal}
/>
<ChangeLog />
<StyledListItem
<QuickSearchInput
data-testid="slider-bar-quick-search-button"
onClick={useCallback(() => {
onOpenQuickSearchModal();
}, [onOpenQuickSearchModal])}
onKeyDown={handleQuickSearchButtonKeyDown}
onClick={onOpenQuickSearchModal}
/>
<RouteMenuLinkItem
icon={<FolderIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.all(currentWorkspaceId)}
>
<div
role="button"
tabIndex={0}
style={{
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<SearchIcon />
{t['Quick search']()}
</div>
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.setting(currentWorkspaceId))
}
<span data-testid="all-pages">{t['All pages']()}</span>
</RouteMenuLinkItem>
<RouteMenuLinkItem
data-testid="slider-bar-workspace-setting-button"
style={{
marginBottom: '16px',
}}
icon={<SettingsIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<SettingsIcon />
<div>{t['Workspace Settings']()}</div>
</StyledLink>
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.all(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.all(currentWorkspaceId),
}}
>
<FolderIcon />
<span data-testid="all-pages">{t['All pages']()}</span>
</StyledLink>
</StyledListItem>
<StyledScrollWrapper
showTopBorder={!isScrollAtTop}
onScroll={(e: UIEvent<HTMLDivElement>) => {
(e.target as HTMLDivElement).scrollTop === 0
? setIsScrollAtTop(true)
: setIsScrollAtTop(false);
}}
>
{blockSuiteWorkspace && (
<Favorite
currentPath={currentPath}
paths={paths}
currentPageId={currentPageId}
openPage={openPage}
currentWorkspace={currentWorkspace}
/>
)}
</StyledScrollWrapper>
<div style={{ height: 16 }}></div>
<span data-testid="settings">{t['Settings']()}</span>
</RouteMenuLinkItem>
</SidebarContainer>
<SidebarScrollableContainer>
<CategoryDivider label={t['Favorites']()} />
{blockSuiteWorkspace && (
<FavoriteList currentWorkspace={currentWorkspace} />
)}
{config.enableLegacyCloud &&
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<StyledListItem>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="Published-to-web">Published to web</span>
</StyledLink>
</StyledListItem>
) : (
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.shared(currentWorkspaceId))
}
<RouteMenuLinkItem
icon={<ShareIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.shared(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</StyledLink>
</StyledListItem>
<span data-testid="Published-to-web">Published to web</span>
</RouteMenuLinkItem>
) : (
<RouteMenuLinkItem
icon={<ShareIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.shared(currentWorkspaceId)}
>
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</RouteMenuLinkItem>
))}
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.trash(currentWorkspaceId))
}
<CategoryDivider label={t['others']()} />
<RouteMenuLinkItem
icon={<DeleteTemporarilyIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.trash(currentWorkspaceId)}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
}}
>
<DeleteTemporarilyIcon /> {t['Trash']()}
</StyledLink>
</StyledListItem>
</StyledSliderBarInnerWrapper>
<span data-testid="trash-page">{t['Trash']()}</span>
</RouteMenuLinkItem>
</SidebarScrollableContainer>
<SidebarContainer>
{environment.isDesktop && <AppUpdaterButton />}
<div />
<AddPageButton onClick={onClickNewPage} />
</SidebarContainer>
</AppSidebar>
<ResizeIndicator targetElement={ref} />
</>
);
};

View File

@@ -5,11 +5,7 @@ import 'fake-indexeddb/auto';
import assert from 'node:assert';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { PageBlockModel } from '@blocksuite/blocks';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
@@ -21,23 +17,17 @@ import {
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { createStore, Provider } from 'jotai';
import { useRouter } from 'next/router';
import routerMock from 'next-router-mock';
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
import type React from 'react';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { currentWorkspaceIdAtom, workspacesAtom } from '../../atoms';
import { LocalPlugin } from '../../plugins/local';
import { workspacesAtom } from '../../atoms';
import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../current/use-current-workspace';
import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
} from '../use-recent-views';
import { useAppHelper, useWorkspaces } from '../use-workspaces';
vi.mock(
@@ -202,6 +192,8 @@ describe('useWorkspaces', () => {
const { result } = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
// next tick
await new Promise(resolve => setTimeout(resolve, 100));
{
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
@@ -221,58 +213,3 @@ describe('useWorkspaces', () => {
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
});
});
describe('useRecentlyViewed', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceId = blockSuiteWorkspace.id;
const pageId = 'page0';
store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
},
]);
LocalPlugin.CRUD.get = vi.fn().mockResolvedValue({
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace,
providers: [],
} satisfies LocalWorkspace);
store.set(currentWorkspaceIdAtom, blockSuiteWorkspace.id);
const workspace = await store.get(currentWorkspaceAtom);
expect(workspace?.id).toBe(blockSuiteWorkspace.id);
const currentHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
expect(currentHook.result.current[0]?.id).toEqual(workspaceId);
await store.get(currentWorkspaceAtom);
const recentlyViewedHook = renderHook(() => useRecentlyViewed(), {
wrapper: ProviderWrapper,
});
expect(recentlyViewedHook.result.current).toEqual([]);
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId,
pageId,
},
});
routerHook.rerender();
const syncHook = renderHook(
router => useSyncRecentViewsWithRouter(router, blockSuiteWorkspace),
{
wrapper: ProviderWrapper,
initialProps: routerHook.result.current,
}
);
syncHook.rerender(routerHook.result.current);
expect(recentlyViewedHook.result.current).toEqual([
{
id: 'page0',
mode: 'page',
},
]);
});
});

View File

@@ -0,0 +1,139 @@
/**
* @vitest-environment happy-dom
*/
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { renderHook } from '@testing-library/react';
import { createStore, Provider } from 'jotai/index';
import { useRouter } from 'next/router';
import routerMock from 'next-router-mock';
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { workspacesAtom } from '../../atoms';
import { LocalPlugin } from '../../plugins/local';
import { BlockSuiteWorkspace } from '../../shared';
import { WorkspaceSubPath } from '../../shared';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../current/use-current-workspace';
import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
} from '../use-recent-views';
let blockSuiteWorkspace: BlockSuiteWorkspace;
beforeAll(() => {
routerMock.useParser(
createDynamicRouteParser([
`/workspace/[workspaceId/${WorkspaceSubPath.ALL}`,
`/workspace/[workspaceId/${WorkspaceSubPath.SETTING}`,
`/workspace/[workspaceId/${WorkspaceSubPath.TRASH}`,
`/workspace/[workspaceId/${WorkspaceSubPath.FAVORITE}`,
'/workspace/[workspaceId]/[pageId]',
])
);
});
async function getJotaiContext() {
const store = createStore();
const ProviderWrapper: React.FC<React.PropsWithChildren> =
function ProviderWrapper({ children }) {
return <Provider store={store}>{children}</Provider>;
};
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(0);
return {
store,
ProviderWrapper,
initialWorkspaces: workspaces,
} as const;
}
beforeEach(async () => {
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test' })
.register(AffineSchemas)
.register(__unstableSchemas);
const initPage = (page: Page) => {
expect(page).not.toBeNull();
assertExists(page);
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
});
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
};
initPage(
blockSuiteWorkspace.createPage({
id: 'page0',
})
);
initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
});
describe('useRecentlyViewed', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceId = blockSuiteWorkspace.id;
const pageId = 'page0';
store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
},
]);
LocalPlugin.CRUD.get = vi.fn().mockResolvedValue({
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace,
providers: [],
} satisfies LocalWorkspace);
store.set(rootCurrentWorkspaceIdAtom, blockSuiteWorkspace.id);
const workspace = await store.get(currentWorkspaceAtom);
expect(workspace?.id).toBe(blockSuiteWorkspace.id);
const currentHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
expect(currentHook.result.current[0]?.id).toEqual(workspaceId);
store.set(rootCurrentWorkspaceIdAtom, blockSuiteWorkspace.id);
await store.get(currentWorkspaceAtom);
const recentlyViewedHook = renderHook(() => useRecentlyViewed(), {
wrapper: ProviderWrapper,
});
expect(recentlyViewedHook.result.current).toEqual([]);
const routerHook = renderHook(() => useRouter(), {
wrapper: ProviderWrapper,
});
await routerHook.result.current.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId,
pageId,
},
});
routerHook.rerender();
const syncHook = renderHook(
router => useSyncRecentViewsWithRouter(router, blockSuiteWorkspace),
{
wrapper: ProviderWrapper,
initialProps: routerHook.result.current,
}
);
syncHook.rerender(routerHook.result.current);
expect(recentlyViewedHook.result.current).toEqual([
{
id: 'page0',
mode: 'page',
},
]);
});
});

View File

@@ -15,7 +15,6 @@ beforeAll(() => {
createDynamicRouteParser([
'/workspace/[workspaceId]/[pageId]',
'/workspace/[workspaceId]/all',
'/workspace/[workspaceId]/favorite',
'/workspace/[workspaceId]/trash',
'/workspace/[workspaceId]/setting',
'/workspace/[workspaceId]/shared',
@@ -54,19 +53,6 @@ describe('useRouterHelper', () => {
// routerHook.result.current.back()
// routerHook.rerender()
// expect(routerHook.result.current.pathname).toBe('/')
await hook.jumpToSubPath(
'workspace1',
WorkspaceSubPath.FAVORITE,
RouteLogic.REPLACE
);
routerHook.rerender();
expect(routerHook.result.current.pathname).toBe(
'/workspace/[workspaceId]/favorite'
);
expect(routerHook.result.current.asPath).toBe(
'/workspace/workspace1/favorite'
);
});
test('should jump to the expected page', async () => {

View File

@@ -2,12 +2,12 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspacePlugins } from '../../plugins';
import { WorkspaceAdapters } from '../../plugins';
export function useAffineLogIn() {
const router = useRouter();
return useCallback(async () => {
await WorkspacePlugins[WorkspaceFlavour.AFFINE].Events[
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
'workspace:access'
]?.();
// todo: remove reload page requirement

View File

@@ -2,12 +2,12 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspacePlugins } from '../../plugins';
import { WorkspaceAdapters } from '../../plugins';
export function useAffineLogOut() {
const router = useRouter();
return useCallback(async () => {
await WorkspacePlugins[WorkspaceFlavour.AFFINE].Events[
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
'workspace:revoke'
]?.();
router.reload();

View File

@@ -1,7 +0,0 @@
import { useAtomValue } from 'jotai';
import { lastWorkspaceIdAtom } from '../current/use-current-workspace';
export function useLastWorkspaceId() {
return useAtomValue(lastWorkspaceIdAtom);
}

View File

@@ -1,12 +1,14 @@
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
import { useCallback } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../plugins/affine/fetcher';
import { affineApis } from '../../shared/apis';
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
export function useToggleWorkspacePublish(
workspace: AffineLegacyCloudWorkspace
) {
const { mutate } = useSWR(QueryKey.getWorkspaces);
return useCallback(
async (isPublish: boolean) => {

View File

@@ -1,5 +1,4 @@
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useAtom, useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
@@ -11,11 +10,6 @@ import type { AllWorkspace } from '../../shared';
*/
export const currentWorkspaceAtom = rootCurrentWorkspaceAtom;
export const lastWorkspaceIdAtom = atomWithStorage<string | null>(
'last_workspace_id',
null
);
export function useCurrentWorkspace(): [
AllWorkspace,
(id: string | null) => void
@@ -23,16 +17,17 @@ export function useCurrentWorkspace(): [
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [, setId] = useAtom(currentWorkspaceIdAtom);
const [, setPageId] = useAtom(currentPageIdAtom);
const setLast = useSetAtom(lastWorkspaceIdAtom);
return [
currentWorkspace,
useCallback(
(id: string | null) => {
if (typeof window !== 'undefined' && id) {
localStorage.setItem('last_workspace_id', id);
}
setPageId(null);
setLast(id);
setId(id);
},
[setId, setLast, setPageId]
[setId, setPageId]
),
];
}

View File

@@ -6,6 +6,7 @@ import {
SignMethod,
storageChangeSlot,
} from '@affine/workspace/affine/login';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
@@ -17,6 +18,7 @@ import { useTransformWorkspace } from '../use-transform-workspace';
export function useOnTransformWorkspace() {
const transformWorkspace = useTransformWorkspace();
const setUser = useSetAtom(currentAffineUserAtom);
const setWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
@@ -43,8 +45,9 @@ export function useOnTransformWorkspace() {
},
})
);
setWorkspaceId(workspaceId);
},
[setUser, transformWorkspace]
[setUser, setWorkspaceId, transformWorkspace]
);
}

View File

@@ -1,88 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect, useRef } from 'react';
import { rootCurrentWorkspaceAtom } from '../atoms/root';
export const HALT_PROBLEM_TIMEOUT = 1000;
const logger = new DebugLogger('useRouterWithWorkspaceIdDefense');
export function useRouterAndWorkspaceWithPageIdDefense(router: NextRouter) {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [currentPageId, setCurrentPageId] = useAtom(rootCurrentPageIdAtom);
const fallbackModeRef = useRef(false);
useEffect(() => {
if (!router.isReady) {
return;
}
const { workspaceId, pageId } = router.query;
if (typeof pageId !== 'string') {
logger.warn('pageId is not a string', pageId);
return;
}
if (typeof workspaceId !== 'string') {
logger.warn('workspaceId is not a string', workspaceId);
return;
}
if (currentWorkspace?.id !== workspaceId) {
logger.warn('workspaceId is not currentWorkspace', workspaceId);
return;
}
if (currentPageId !== pageId && !fallbackModeRef.current) {
logger.info('set pageId', pageId, 'for workspace', workspaceId);
setCurrentPageId(pageId);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId,
pageId,
},
});
}
}, [currentPageId, currentWorkspace.id, router, setCurrentPageId]);
useEffect(() => {
if (fallbackModeRef.current) {
return;
}
const id = setTimeout(() => {
if (currentPageId) {
const page =
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
const firstOne =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0);
if (firstOne) {
logger.warn(
'cannot find page',
currentPageId,
'so redirect to',
firstOne.id
);
setCurrentPageId(firstOne.id);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: firstOne.id,
},
});
fallbackModeRef.current = true;
}
}
}
}, HALT_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [
currentPageId,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
router,
setCurrentPageId,
]);
}

View File

@@ -1,53 +0,0 @@
import { DebugLogger } from '@affine/debug';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
const logger = new DebugLogger('useRouterWithWorkspaceIdDefense');
export function useRouterWithWorkspaceIdDefense(router: NextRouter) {
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspaceId) {
return;
}
const exist = metadata.find(m => m.id === currentWorkspaceId);
if (!exist) {
console.warn('workspace not exist, redirect to first one');
// clean up
setCurrentWorkspaceId(null);
setCurrentPageId(null);
const firstOne = metadata.at(0);
if (!firstOne) {
throw new Error('no workspace');
}
logger.debug('redirect to', firstOne.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
...router.query,
workspaceId: firstOne.id,
},
});
}
}, [
currentWorkspaceId,
metadata,
router,
router.isReady,
setCurrentPageId,
setCurrentWorkspaceId,
]);
}

View File

@@ -1,21 +0,0 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentPageId(router: NextRouter) {
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const pageId = router.query.pageId;
if (typeof pageId === 'string') {
console.log('set page id', pageId);
setCurrentPageId(pageId);
} else if (pageId === undefined) {
console.log('cleanup page');
setCurrentPageId(null);
}
}, [router.isReady, router.query.pageId, setCurrentPageId]);
}

View File

@@ -1,72 +0,0 @@
import { DebugLogger } from '@affine/debug';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
const logger = new DebugLogger('useSyncRouterWithCurrentWorkspaceId');
export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (currentWorkspaceId) {
if (currentWorkspaceId !== workspaceId) {
const target = metadata.find(workspace => workspace.id === workspaceId);
if (!target) {
logger.debug('workspace not exist, redirect to current one');
// workspaceId is invalid, redirect to currentWorkspaceId
void router.push({
pathname: router.pathname,
query: {
...router.query,
workspaceId: currentWorkspaceId,
},
});
}
}
return;
}
const targetWorkspace = metadata.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
logger.debug('redirect to', targetWorkspace.id);
void router.push({
pathname: router.pathname,
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});
} else {
const targetWorkspace = metadata.at(0);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
logger.debug('redirect to', targetWorkspace.id);
void router.push({
pathname: router.pathname,
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});
}
}
}, [currentWorkspaceId, metadata, router, setCurrentWorkspaceId]);
}

View File

@@ -1,13 +1,10 @@
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { WorkspaceFlavour } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { WorkspacePlugins } from '../plugins';
import { WorkspaceAdapters } from '../plugins';
/**
* Transform workspace from one flavour to another
@@ -15,7 +12,6 @@ import { WorkspacePlugins } from '../plugins';
* The logic here is to delete the old workspace and create a new one.
*/
export function useTransformWorkspace() {
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
@@ -23,10 +19,11 @@ export function useTransformWorkspace() {
to: To,
workspace: WorkspaceRegistry[From]
): Promise<string> => {
await WorkspacePlugins[from].CRUD.delete(workspace as any);
const newId = await WorkspacePlugins[to].CRUD.create(
// create first, then delete, in case of failure
const newId = await WorkspaceAdapters[to].CRUD.create(
workspace.blockSuiteWorkspace
);
await WorkspaceAdapters[from].CRUD.delete(workspace as any);
set(workspaces => {
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
workspaces.splice(idx, 1, {
@@ -35,9 +32,8 @@ export function useTransformWorkspace() {
});
return [...workspaces];
});
setCurrentWorkspaceId(newId);
return newId;
},
[set, setCurrentWorkspaceId]
[set]
);
}

View File

@@ -6,10 +6,10 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { nanoid } from '@blocksuite/store';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { workspacesAtom } from '../atoms';
import { WorkspacePlugins } from '../plugins';
import { WorkspaceAdapters } from '../plugins';
import { LocalPlugin } from '../plugins/local';
import type { AllWorkspace } from '../shared';
@@ -86,7 +86,7 @@ export function useAppHelper() {
}
// delete workspace from plugin
await WorkspacePlugins[targetWorkspace.flavour].CRUD.delete(
await WorkspaceAdapters[targetWorkspace.flavour].CRUD.delete(
// fixme: type casting
targetWorkspace as any
);
@@ -97,25 +97,3 @@ export function useAppHelper() {
),
};
}
export const useElementResizeEffect = (
element: Element | null,
fn: () => void | (() => () => void),
// TODO: add throttle
_throttle = 0
) => {
useEffect(() => {
if (!element) {
return;
}
let dispose: void | (() => void);
const resizeObserver = new ResizeObserver(() => {
dispose = fn();
});
resizeObserver.observe(element);
return () => {
dispose?.();
resizeObserver.disconnect();
};
}, [element, fn]);
};

View File

@@ -1,3 +1,4 @@
import { appSidebarResizingAtom } from '@affine/component/app-sidebar';
import {
AppContainer,
MainContainer,
@@ -26,6 +27,7 @@ import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import { useTrackRouterHistoryEffect } from '../atoms/history';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
@@ -35,11 +37,8 @@ import { RootAppSidebar } from '../components/root-app-sidebar';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useRouterTitle } from '../hooks/use-router-title';
import { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
import { useSyncRouterWithCurrentPageId } from '../hooks/use-sync-router-with-current-page-id';
import { useSyncRouterWithCurrentWorkspaceId } from '../hooks/use-sync-router-with-current-workspace-id';
import { useWorkspaces } from '../hooks/use-workspaces';
import { WorkspacePlugins } from '../plugins';
import { WorkspaceAdapters } from '../plugins';
import { ModalProvider } from '../providers/modal-provider';
import { pathGenerator, publicPathGenerator } from '../shared';
@@ -100,7 +99,7 @@ export const QuickSearch: FC = () => {
const logger = new DebugLogger('workspace-layout');
const affineGlobalChannel = createAffineGlobalChannel(
WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD
WorkspaceAdapters[WorkspaceFlavour.AFFINE].CRUD
);
export const AllWorkspaceContext = ({
@@ -130,16 +129,36 @@ export const AllWorkspaceContext = ({
return <>{children}</>;
};
declare global {
// eslint-disable-next-line no-var
var HALTING_PROBLEM_TIMEOUT: number;
}
if (globalThis.HALTING_PROBLEM_TIMEOUT === undefined) {
globalThis.HALTING_PROBLEM_TIMEOUT = 1000;
}
export const CurrentWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const router = useRouter();
const workspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
useSyncRouterWithCurrentWorkspaceId(router);
useSyncRouterWithCurrentPageId(router);
useRouterWithWorkspaceIdDefense(router);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
const router = useRouter();
const push = router.push;
// fixme(himself65): this is not a good way to handle this,
// need a better way to check whether this workspace really exist.
useEffect(() => {
const id = setTimeout(() => {
if (!exist) {
void push('/');
globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
}
}, globalThis.HALTING_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [push, exist]);
if (!router.isReady) {
return <WorkspaceFallback key="router-is-loading" />;
}
@@ -160,6 +179,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n);
}, [i18n]);
useTrackRouterHistoryEffect();
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const meta = useMemo(
@@ -170,7 +190,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
useEffect(() => {
logger.info('mount');
const controller = new AbortController();
const lists = Object.values(WorkspacePlugins)
const lists = Object.values(WorkspaceAdapters)
.sort((a, b) => a.loadPriority - b.loadPriority)
.map(({ CRUD }) => CRUD.list);
@@ -220,7 +240,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
}, [currentWorkspaceId, jotaiWorkspaces]);
const Provider =
(meta && WorkspacePlugins[meta.flavour].UI.Provider) ?? DefaultProvider;
(meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider;
return (
<>
{/* fixme(himself65): don't re-render whole modals */}
@@ -330,17 +350,18 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
setOpenQuickSearchModalAtom(true);
}, [setOpenQuickSearchModalAtom]);
const resizing = useAtomValue(appSidebarResizingAtom);
return (
<>
<Head>
<title>{title}</title>
</Head>
<AppContainer>
<AppContainer resizing={resizing}>
<RootAppSidebar
isPublicWorkspace={isPublicWorkspace}
onOpenQuickSearchModal={handleOpenQuickSearchModal}
currentWorkspace={currentWorkspace}
currentPageId={currentPageId}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback(
(pageId: string) => {
@@ -354,7 +375,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
/>
<MainContainer>
<Suspense fallback={<WorkspaceFallback />}>{children}</Suspense>
{children}
<ToolContainer>
{/* fixme(himself65): remove this */}
<div id="toolWrapper" style={{ marginBottom: '12px' }}>

View File

@@ -4,7 +4,6 @@ import { useRouter } from 'next/router';
import { Suspense, useEffect } from 'react';
import { PageLoading } from '../components/pure/loading';
import { useLastWorkspaceId } from '../hooks/affine/use-last-leave-workspace-id';
import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper';
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared';
@@ -15,16 +14,15 @@ const IndexPageInner = () => {
const router = useRouter();
const { jumpToPage, jumpToSubPath } = useRouterHelper(router);
const workspaces = useWorkspaces();
const lastWorkspaceId = useLastWorkspaceId();
const helper = useAppHelper();
useEffect(() => {
if (!router.isReady) {
return;
}
const lastId = localStorage.getItem('last_workspace_id');
const targetWorkspace =
(lastWorkspaceId &&
workspaces.find(({ id }) => id === lastWorkspaceId)) ||
(lastId && workspaces.find(({ id }) => id === lastId)) ||
workspaces.at(0);
if (targetWorkspace) {
@@ -56,7 +54,7 @@ const IndexPageInner = () => {
} else {
console.warn('No target workspace. This should not happen in production');
}
}, [helper, jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
}, [helper, jumpToPage, jumpToSubPath, router, workspaces]);
return <PageLoading key="IndexPageInfinitePageLoading" />;
};

View File

@@ -20,10 +20,9 @@ import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
import { useRouterAndWorkspaceWithPageIdDefense } from '../../../hooks/use-router-and-workspace-with-page-id-defense';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import { WorkspaceAdapters } from '../../../plugins';
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
function setEditorFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
@@ -85,7 +84,8 @@ const WorkspaceDetail: React.FC = () => {
}
}, [currentWorkspace]);
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
const PageDetail =
WorkspaceAdapters[currentWorkspace.flavour].UI.PageDetail;
return (
<PageDetail
currentWorkspace={currentWorkspace}
@@ -93,7 +93,8 @@ const WorkspaceDetail: React.FC = () => {
/>
);
} else if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
const PageDetail =
WorkspaceAdapters[currentWorkspace.flavour].UI.PageDetail;
return (
<PageDetail
currentWorkspace={currentWorkspace}
@@ -108,7 +109,6 @@ const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
useRouterAndWorkspaceWithPageIdDefense(router);
const page = useBlockSuiteWorkspacePage(
currentWorkspace.blockSuiteWorkspace,
currentPageId

View File

@@ -14,9 +14,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import { WorkspaceAdapters } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
const AllPage: NextPageWithLayout = () => {
@@ -24,7 +23,6 @@ const AllPage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
@@ -43,7 +41,7 @@ const AllPage: NextPageWithLayout = () => {
throw new QueryParamError('workspaceId', router.query.workspaceId);
}
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const PageList = WorkspacePlugins[currentWorkspace.flavour].UI.PageList;
const PageList = WorkspaceAdapters[currentWorkspace.flavour].UI.PageList;
return (
<>
<Head>
@@ -65,7 +63,7 @@ const AllPage: NextPageWithLayout = () => {
</>
);
} else if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
const PageList = WorkspacePlugins[currentWorkspace.flavour].UI.PageList;
const PageList = WorkspaceAdapters[currentWorkspace.flavour].UI.PageList;
return (
<>
<Head>

View File

@@ -1,66 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FavoriteIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { BlockSuitePageList } from '../../../components/blocksuite/block-suite-page-list';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const FavouritePage: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
jumpToPage(currentWorkspace.id, pageId);
}
},
[currentWorkspace, jumpToPage]
);
if (currentWorkspace === null) {
return <PageLoading />;
}
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);
return (
<>
<Head>
<title>{t['Favorites']()} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
currentPage={null}
isPreview={false}
isPublic={false}
icon={<FavoriteIcon />}
>
{t['Favorites']()}
</WorkspaceTitle>
<BlockSuitePageList
blockSuiteWorkspace={blockSuiteWorkspace}
onOpenPage={onClickPage}
listType="favorite"
/>
</>
);
};
export default FavouritePage;
FavouritePage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};

View File

@@ -11,6 +11,7 @@ import { assertExists } from '@blocksuite/store';
import { useAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import Head from 'next/head';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
@@ -19,10 +20,9 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import { WorkspaceAdapters } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
import { toast } from '../../../utils';
@@ -31,18 +31,64 @@ const settingPanelAtom = atomWithStorage<SettingPanel>(
settingPanel.General
);
function useTabRouterSync(
router: NextRouter,
currentTab: SettingPanel,
setCurrentTab: (tab: SettingPanel) => void
) {
if (!router.isReady) {
return;
}
const queryCurrentTab =
typeof router.query.currentTab === 'string'
? router.query.currentTab
: null;
if (
queryCurrentTab !== null &&
settingPanelValues.indexOf(queryCurrentTab as SettingPanel) === -1
) {
setCurrentTab(settingPanel.General);
void router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
});
return;
} else if (settingPanelValues.indexOf(currentTab as SettingPanel) === -1) {
setCurrentTab(settingPanel.General);
void router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
});
return;
} else if (queryCurrentTab !== currentTab) {
void router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: currentTab,
},
});
return;
}
}
const SettingPage: NextPageWithLayout = () => {
const router = useRouter();
const workspaceIds = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspace] = useCurrentWorkspace();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
useEffect(() => {});
const onChangeTab = useCallback(
(tab: SettingPanel) => {
setCurrentTab(tab as SettingPanel);
router.push({
void router.push({
pathname: router.pathname,
query: {
...router.query,
@@ -52,48 +98,7 @@ const SettingPage: NextPageWithLayout = () => {
},
[router, setCurrentTab]
);
useEffect(() => {
if (!router.isReady) {
return;
}
const queryCurrentTab =
typeof router.query.currentTab === 'string'
? router.query.currentTab
: null;
if (
queryCurrentTab !== null &&
settingPanelValues.indexOf(queryCurrentTab as SettingPanel) === -1
) {
setCurrentTab(settingPanel.General);
router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
});
return;
} else if (settingPanelValues.indexOf(currentTab as SettingPanel) === -1) {
setCurrentTab(settingPanel.General);
router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
});
return;
} else if (queryCurrentTab !== currentTab) {
router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: currentTab,
},
});
return;
}
}, [currentTab, router, setCurrentTab]);
useTabRouterSync(router, currentTab, setCurrentTab);
const helper = useAppHelper();
@@ -116,7 +121,7 @@ const SettingPage: NextPageWithLayout = () => {
return <PageLoading />;
} else if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const Setting =
WorkspacePlugins[currentWorkspace.flavour].UI.SettingsDetail;
WorkspaceAdapters[currentWorkspace.flavour].UI.SettingsDetail;
return (
<>
<Head>
@@ -142,7 +147,7 @@ const SettingPage: NextPageWithLayout = () => {
);
} else if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
const Setting =
WorkspacePlugins[currentWorkspace.flavour].UI.SettingsDetail;
WorkspaceAdapters[currentWorkspace.flavour].UI.SettingsDetail;
return (
<>
<Head>

View File

@@ -10,7 +10,6 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
@@ -19,7 +18,6 @@ const SharedPages: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -10,7 +10,6 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
@@ -19,7 +18,6 @@ const TrashPage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -1,5 +1,5 @@
import { rootStore } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { assertExists } from '@blocksuite/store';
@@ -70,7 +70,7 @@ export const fetcher = async (
workspaceApis: affineApis,
}
);
const remWorkspace: AffineWorkspace = {
const remWorkspace: AffineLegacyCloudWorkspace = {
...workspace,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,

View File

@@ -11,8 +11,13 @@ import {
SignMethod,
} from '@affine/workspace/affine/login';
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
import {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/workspace/type';
import {
cleanupWorkspace,
createEmptyBlockSuiteWorkspace,
@@ -24,6 +29,7 @@ import { mutate } from 'swr';
import { z } from 'zod';
import { createAffineProviders } from '../../blocksuite';
import { createAffineDownloadProvider } from '../../blocksuite/providers/affine';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
@@ -33,7 +39,7 @@ import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh
import { BlockSuiteWorkspace } from '../../shared';
import { affineApis } from '../../shared/apis';
import { toast } from '../../utils';
import type { WorkspacePlugin } from '..';
import type { WorkspaceAdapter } from '..';
import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
@@ -46,7 +52,7 @@ const schema = z.object({
const getPersistenceAllWorkspace = () => {
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
const allWorkspaces: AffineWorkspace[] = [];
const allWorkspaces: AffineLegacyCloudWorkspace[] = [];
if (
Array.isArray(items) &&
items.every(item => schema.safeParse(item).success)
@@ -60,7 +66,7 @@ const getPersistenceAllWorkspace = () => {
workspaceApis: affineApis,
}
);
const affineWorkspace: AffineWorkspace = {
const affineWorkspace: AffineLegacyCloudWorkspace = {
...item,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
@@ -89,7 +95,8 @@ function AuthContext({ children }: PropsWithChildren): ReactElement {
return <>{children}</>;
}
export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
export const AffinePlugin: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
releaseType: ReleaseType.STABLE,
flavour: WorkspaceFlavour.AFFINE,
loadPriority: LoadPriority.HIGH,
Events: {
@@ -143,6 +150,27 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
);
}
}
{
const bs = createEmptyBlockSuiteWorkspace(id, WorkspaceFlavour.AFFINE, {
workspaceApis: affineApis,
});
// fixme:
// force to download workspace binary
// to make sure the workspace is synced
const provider = createAffineDownloadProvider(bs);
const indexedDBProvider = createIndexedDBBackgroundProvider(bs);
await new Promise<void>(resolve => {
indexedDBProvider.callbacks.add(() => {
resolve();
});
provider.callbacks.add(() => {
indexedDBProvider.connect();
});
provider.connect();
});
provider.disconnect();
indexedDBProvider.disconnect();
}
await mutate(matcher => matcher === QueryKey.getWorkspaces);
// refresh the local storage
@@ -178,7 +206,8 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
cleanupWorkspace(WorkspaceFlavour.AFFINE);
return null;
}
const workspaces: AffineWorkspace[] = await AffinePlugin.CRUD.list();
const workspaces: AffineLegacyCloudWorkspace[] =
await AffinePlugin.CRUD.list();
return (
workspaces.find(workspace => workspace.id === workspaceId) ?? null
);
@@ -239,7 +268,7 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
storage.setItem(AFFINE_STORAGE_KEY, [...data]);
}
const affineWorkspace: AffineWorkspace = {
const affineWorkspace: AffineLegacyCloudWorkspace = {
...workspace,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,

View File

@@ -3,12 +3,17 @@ import type {
WorkspaceCRUD,
WorkspaceUISchema,
} from '@affine/workspace/type';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/workspace/type';
import { AffinePlugin } from './affine';
import { LocalPlugin } from './local';
export interface WorkspacePlugin<Flavour extends WorkspaceFlavour> {
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
@@ -21,10 +26,32 @@ export interface WorkspacePlugin<Flavour extends WorkspaceFlavour> {
const unimplemented = () => {
throw new Error('Not implemented');
};
export const WorkspacePlugins = {
export const WorkspaceAdapters = {
[WorkspaceFlavour.AFFINE]: AffinePlugin,
[WorkspaceFlavour.LOCAL]: LocalPlugin,
[WorkspaceFlavour.AFFINE_CLOUD]: {
releaseType: ReleaseType.UNRELEASED,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
loadPriority: LoadPriority.HIGH,
Events: {} as Partial<AppEvents>,
// todo: implement this
CRUD: {
get: unimplemented,
list: unimplemented,
delete: unimplemented,
create: unimplemented,
},
// todo: implement this
UI: {
Provider: unimplemented,
PageDetail: unimplemented,
PageList: unimplemented,
SettingsDetail: unimplemented,
},
},
[WorkspaceFlavour.PUBLIC]: {
releaseType: ReleaseType.UNRELEASED,
flavour: WorkspaceFlavour.PUBLIC,
loadPriority: LoadPriority.LOW,
Events: {} as Partial<AppEvents>,
@@ -44,5 +71,5 @@ export const WorkspacePlugins = {
},
},
} satisfies {
[Key in WorkspaceFlavour]: WorkspacePlugin<Key>;
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
};

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