mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
Compare commits
81 Commits
v0.10.3-be
...
v0.5.4-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f5128783 | ||
|
|
1363094ce6 | ||
|
|
75c54f0af5 | ||
|
|
ec142a7189 | ||
|
|
6f859967a9 | ||
|
|
bcee63175c | ||
|
|
f62ca1822d | ||
|
|
684bbafbcf | ||
|
|
6cd0053b0c | ||
|
|
ccd3fb4925 | ||
|
|
d5c3d1b86a | ||
|
|
31e1575b5d | ||
|
|
403479996d | ||
|
|
19f7f591ce | ||
|
|
76289838d2 | ||
|
|
bb65262217 | ||
|
|
877b87aae0 | ||
|
|
0c5c1a5511 | ||
|
|
edda79c448 | ||
|
|
a4111f5550 | ||
|
|
e099734cc7 | ||
|
|
26f3380c1a | ||
|
|
4874adbf3f | ||
|
|
943e6c59e3 | ||
|
|
c0d6b8c458 | ||
|
|
26f5461f9a | ||
|
|
66303e5fd6 | ||
|
|
337fe18d4c | ||
|
|
cbcf8140e4 | ||
|
|
a998dc808a | ||
|
|
23f51a7ecc | ||
|
|
ab8cdb4222 | ||
|
|
5c6655ab0e | ||
|
|
9c6e687113 | ||
|
|
25cf2e9ba0 | ||
|
|
31bea47545 | ||
|
|
a34e2eb57d | ||
|
|
8527c5bfac | ||
|
|
599bf92c08 | ||
|
|
e8f70c6e45 | ||
|
|
c01f2d5eea | ||
|
|
581726ecc5 | ||
|
|
b15eae11cf | ||
|
|
1aef8862ad | ||
|
|
5fcaf7eef9 | ||
|
|
fac93b0328 | ||
|
|
54b8b36618 | ||
|
|
683343ad82 | ||
|
|
add5deae0f | ||
|
|
ec66b229fe | ||
|
|
5008958e84 | ||
|
|
5516c215cd | ||
|
|
7c90417b2b | ||
|
|
1922c07c00 | ||
|
|
c61c1e10a0 | ||
|
|
df93a870af | ||
|
|
6ab51b6d54 | ||
|
|
f25b75c0d8 | ||
|
|
93521f434f | ||
|
|
20fb801ecd | ||
|
|
9902892615 | ||
|
|
f8e184a6c0 | ||
|
|
66e1b5c537 | ||
|
|
37512bc18f | ||
|
|
5ba4fb8d7c | ||
|
|
5f28afa5fe | ||
|
|
270c00f021 | ||
|
|
e69831636a | ||
|
|
df60392c31 | ||
|
|
58fa9d1fb8 | ||
|
|
b4981abe4f | ||
|
|
4c230843ed | ||
|
|
c76bc34c6f | ||
|
|
8bbb9ca304 | ||
|
|
d9dbe64d9b | ||
|
|
d389e2bc43 | ||
|
|
64f4e634e8 | ||
|
|
cf6341d00b | ||
|
|
aad711c115 | ||
|
|
f787d19696 | ||
|
|
a0a22f417a |
@@ -17,7 +17,7 @@
|
||||
"hooks",
|
||||
"i18n",
|
||||
"jotai",
|
||||
"octobase-node",
|
||||
"native",
|
||||
"templates",
|
||||
"y-indexeddb",
|
||||
"debug",
|
||||
|
||||
49
.github/actions/build-rust/action.yml
vendored
Normal file
49
.github/actions/build-rust/action.yml
vendored
Normal 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 }}
|
||||
130
.github/workflows/build.yml
vendored
130
.github/workflows/build.yml
vendored
@@ -4,9 +4,26 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- v[0-9]+.[0-9]+.x-staging
|
||||
- v[0-9]+.[0-9]+.x
|
||||
paths-ignore:
|
||||
- README.md
|
||||
- .github/**
|
||||
- '!.github/workflows/build.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- v[0-9]+.[0-9]+.x-staging
|
||||
- v[0-9]+.[0-9]+.x
|
||||
paths-ignore:
|
||||
- README.md
|
||||
- .github/**
|
||||
- '!.github/workflows/build.yml'
|
||||
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
APP_NAME: affine
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -37,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
|
||||
@@ -261,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
|
||||
@@ -269,28 +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
|
||||
@@ -299,18 +334,47 @@ 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
|
||||
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: Run desktop tests
|
||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||
run: yarn test
|
||||
working-directory: apps/electron
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
232
.github/workflows/nightly-build.yml
vendored
Normal file
232
.github/workflows/nightly-build.yml
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
name: Build Canary Desktop App on Staging Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v[0-9]+.[0-9]+.x-staging
|
||||
paths-ignore:
|
||||
- README.md
|
||||
- .github/**
|
||||
- '!.github/workflows/nightly-build.yml'
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
security-events: write
|
||||
|
||||
concurrency:
|
||||
# The concurrency group contains the workflow name and the branch name for
|
||||
# pull requests or the commit hash for any other events.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
BUILD_TYPE: internal
|
||||
|
||||
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 ${{ needs.set-build-version.outputs.version }}
|
||||
- name: generate-assets
|
||||
working-directory: apps/electron
|
||||
run: yarn 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 }}
|
||||
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
|
||||
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
|
||||
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
API_SERVER_PROFILE: prod
|
||||
ENABLE_TEST_PROPERTIES: false
|
||||
ENABLE_IMAGE_PREVIEW_MODAL: false
|
||||
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
|
||||
|
||||
- name: Upload Artifact (web-static)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: before-make-web-static
|
||||
path: apps/electron/resources/web-static
|
||||
|
||||
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: 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
|
||||
- set-build-version
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
SKIP_GENERATE_ASSETS: 1
|
||||
steps:
|
||||
- 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 ${{ needs.set-build-version.outputs.version }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: before-make-web-static
|
||||
path: apps/electron/resources/web-static
|
||||
- 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 == '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 workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||
|
||||
- name: Save artifacts (mac)
|
||||
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 == 'win32' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
|
||||
mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe
|
||||
mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi
|
||||
mv apps/electron/out/*/make/squirrel.windows/x64/*.nupkg ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.nupkg
|
||||
|
||||
- name: Save artifacts (linux)
|
||||
if: ${{ matrix.spec.platform == 'linux' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
|
||||
path: builds
|
||||
|
||||
release:
|
||||
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-darwin-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (macos-arm64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-darwin-arm64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (windows-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-win32-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (linux-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-linux-x64-builds
|
||||
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: ${{ 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: ${{ needs.set-build-version.outputs.version }}
|
||||
tag_name: ${{ needs.set-build-version.outputs.version }}
|
||||
prerelease: true
|
||||
files: |
|
||||
./VERSION
|
||||
./*.zip
|
||||
./*.dmg
|
||||
./*.exe
|
||||
./*.nupkg
|
||||
./RELEASES
|
||||
./*.AppImage
|
||||
./*.apk
|
||||
./*.yml
|
||||
91
.github/workflows/release-desktop-app.yml
vendored
91
.github/workflows/release-desktop-app.yml
vendored
@@ -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,8 @@ jobs:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
API_SERVER_PROFILE: prod
|
||||
ENABLE_TEST_PROPERTIES: false
|
||||
ENABLE_IMAGE_PREVIEW_MODAL: false
|
||||
RELEASE_VERSION: ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Upload Artifact (web-static)
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -71,28 +75,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 +116,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 +176,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
4
.gitignore
vendored
@@ -66,3 +66,7 @@ i18n-generated.ts
|
||||
# Cache
|
||||
.eslintcache
|
||||
next-env.d.ts
|
||||
|
||||
# Rust
|
||||
target
|
||||
*.node
|
||||
|
||||
9
.taplo.toml
Normal file
9
.taplo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
exclude = ["node_modules/**/*.toml"]
|
||||
|
||||
[[rule]]
|
||||
keys = ["dependencies", "*-dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
align_entries = true
|
||||
indent_tables = true
|
||||
reorder_keys = true
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -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
800
Cargo.lock
generated
Normal 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
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[workspace]
|
||||
members = ["./packages/native"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = "symbols"
|
||||
@@ -140,7 +140,7 @@ Thanks a lot to the community for providing such powerful and simple libraries,
|
||||
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).
|
||||
|
||||
<a href="https://github.com/toeverything/affine/graphs/contributors">
|
||||
<img src="https://user-images.githubusercontent.com/5910926/233382206-312428ca-094a-4579-ae06-213961ed7eab.svg" />
|
||||
<img src="https://user-images.githubusercontent.com/5910926/237263745-36bb975d-83d6-4a7c-a152-d9ad020e5023.png" />
|
||||
</a>
|
||||
|
||||
## Self-Host
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { z } = require('zod');
|
||||
|
||||
const {
|
||||
utils: { fromBuildIdentifier },
|
||||
} = require('@electron-forge/core');
|
||||
|
||||
const path = require('node:path');
|
||||
|
||||
const buildType = (process.env.BUILD_TYPE || 'stable').trim().toLowerCase();
|
||||
const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
|
||||
|
||||
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
|
||||
const buildType = ReleaseTypeSchema.parse(envBuildType);
|
||||
const stableBuild = buildType === 'stable';
|
||||
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
|
||||
const icoPath = !stableBuild
|
||||
@@ -28,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',
|
||||
|
||||
142
apps/electron/layers/main/src/application-menu.ts
Normal file
142
apps/electron/layers/main/src/application-menu.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { app, Menu } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../utils';
|
||||
import { subjects } from './events';
|
||||
import { checkForUpdatesAndNotify } from './handlers/updater';
|
||||
import { revealLogFile } from './logger';
|
||||
|
||||
// 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: 'Open logs folder',
|
||||
click: async () => {
|
||||
revealLogFile();
|
||||
},
|
||||
},
|
||||
{
|
||||
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;
|
||||
}
|
||||
22
apps/electron/layers/main/src/events/application-menu.ts
Normal file
22
apps/electron/layers/main/src/events/application-menu.ts
Normal 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>;
|
||||
@@ -2,25 +2,37 @@ import { Subject } from 'rxjs';
|
||||
|
||||
import type { MainEventListener } from './type';
|
||||
|
||||
interface DBFilePathMeta {
|
||||
workspaceId: string;
|
||||
path: string;
|
||||
realPath: string;
|
||||
}
|
||||
|
||||
export const dbSubjects = {
|
||||
// emit workspace ids
|
||||
dbFileMissing: new Subject<string>(),
|
||||
// emit workspace ids
|
||||
dbFileUpdate: new Subject<string>(),
|
||||
dbFilePathChange: new Subject<DBFilePathMeta>(),
|
||||
};
|
||||
|
||||
export const dbEvents = {
|
||||
onDbFileMissing: (fn: (workspaceId: string) => void) => {
|
||||
onDBFileMissing: (fn: (workspaceId: string) => void) => {
|
||||
const sub = dbSubjects.dbFileMissing.subscribe(fn);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onDbFileUpdate: (fn: (workspaceId: string) => void) => {
|
||||
onDBFileUpdate: (fn: (workspaceId: string) => void) => {
|
||||
const sub = dbSubjects.dbFileUpdate.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onDBFilePathChange: (fn: (meta: DBFilePathMeta) => void) => {
|
||||
const sub = dbSubjects.dbFilePathChange.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
} satisfies Record<string, MainEventListener>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from './register';
|
||||
|
||||
import { applicationMenuSubjects } from './application-menu';
|
||||
import { dbSubjects } from './db';
|
||||
|
||||
export const subjects = {
|
||||
db: dbSubjects,
|
||||
applicationMenu: applicationMenuSubjects,
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -61,6 +63,9 @@ const ipcMain = {
|
||||
handlers.push(callback);
|
||||
registeredHandlers.set(key, handlers);
|
||||
},
|
||||
setMaxListeners: (_n: number) => {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
|
||||
const nativeTheme = {
|
||||
@@ -96,6 +101,11 @@ const electronModule = {
|
||||
handlers.push(callback);
|
||||
registeredHandlers.set(name, handlers);
|
||||
},
|
||||
addEventListener: (...args: any[]) => {
|
||||
// @ts-ignore
|
||||
electronModule.app.on(...args);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
},
|
||||
BrowserWindow: {
|
||||
getAllWindows: () => {
|
||||
@@ -113,6 +123,8 @@ vi.doMock('electron', () => {
|
||||
return electronModule;
|
||||
});
|
||||
|
||||
let connectableSubscription: Subscription;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { registerHandlers } = await import('../register');
|
||||
registerHandlers();
|
||||
@@ -120,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;
|
||||
@@ -143,73 +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);
|
||||
|
||||
// should only call once for multiple writes
|
||||
expect(sendStub).toBeCalledTimes(1);
|
||||
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 = {};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +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 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();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { appContext } from '../../context';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { ensureSQLiteDB } from './ensure-db';
|
||||
@@ -30,4 +32,11 @@ export const dbHandlers = {
|
||||
getDefaultStorageLocation: async () => {
|
||||
return appContext.appDataPath;
|
||||
},
|
||||
getDBFilePath: async (_, workspaceId: string) => {
|
||||
const workspaceDB = await ensureSQLiteDB(workspaceId);
|
||||
return {
|
||||
path: workspaceDB.path,
|
||||
realPath: await fs.realpath(workspaceDB.path),
|
||||
};
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
@@ -6,6 +6,7 @@ import fs from 'fs-extra';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { AppContext } from '../../context';
|
||||
import { dbSubjects } from '../../events/db';
|
||||
import { logger } from '../../logger';
|
||||
import { ts } from '../../utils';
|
||||
|
||||
@@ -41,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();
|
||||
@@ -57,11 +59,23 @@ export class WorkspaceSQLiteDB {
|
||||
};
|
||||
|
||||
reconnectDB = () => {
|
||||
logger.log('open db', this.workspaceId);
|
||||
logger.log('[WorkspaceSQLiteDB] open db', this.workspaceId);
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
fs.realpath(this.path)
|
||||
.then(realPath => {
|
||||
dbSubjects.dbFilePathChange.next({
|
||||
workspaceId: this.workspaceId,
|
||||
path: this.path,
|
||||
realPath,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// skip error
|
||||
});
|
||||
|
||||
// use cached version?
|
||||
const db = (this.db = sqlite(this.path));
|
||||
db.exec(schemas.join(';'));
|
||||
@@ -211,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'`
|
||||
@@ -226,6 +241,7 @@ export function isValidDBFile(path: string) {
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('isValidDBFile', error);
|
||||
db?.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -15,7 +16,7 @@ import { listWorkspaces } from '../workspace/workspace';
|
||||
|
||||
export async function revealDBFile(workspaceId: string) {
|
||||
const workspaceDB = await ensureSQLiteDB(workspaceId);
|
||||
shell.showItemInFolder(workspaceDB.path);
|
||||
shell.showItemInFolder(await fs.realpath(workspaceDB.path));
|
||||
}
|
||||
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
@@ -47,6 +48,7 @@ const ErrorMessages = [
|
||||
'DB_FILE_ALREADY_LOADED',
|
||||
'DB_FILE_PATH_INVALID',
|
||||
'DB_FILE_INVALID',
|
||||
'FILE_ALREADY_EXISTS',
|
||||
'UNKNOWN_ERROR',
|
||||
] as const;
|
||||
|
||||
@@ -201,7 +203,7 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
|
||||
await fs.ensureDir(path.join(appContext.appDataPath, 'workspaces'));
|
||||
|
||||
await fs.symlink(filePath, linkedFilePath);
|
||||
await fs.symlink(filePath, linkedFilePath, 'file');
|
||||
logger.info(`loadDBFile, symlink: ${filePath} -> ${linkedFilePath}`);
|
||||
|
||||
return { workspaceId };
|
||||
@@ -231,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',
|
||||
@@ -262,23 +276,39 @@ export async function moveDBFile(
|
||||
};
|
||||
}
|
||||
|
||||
db.db.close();
|
||||
|
||||
if (await fs.pathExists(newFilePath)) {
|
||||
return {
|
||||
error: 'FILE_ALREADY_EXISTS',
|
||||
};
|
||||
}
|
||||
|
||||
if (isLink) {
|
||||
// remove the old link to unblock new link
|
||||
await fs.unlink(db.path);
|
||||
}
|
||||
|
||||
await fs.move(realpath, newFilePath, {
|
||||
overwrite: true,
|
||||
});
|
||||
logger.info(`[moveDBFile] move ${realpath} -> ${newFilePath}`);
|
||||
|
||||
await moveFile(realpath, newFilePath);
|
||||
|
||||
await fs.ensureSymlink(newFilePath, db.path, 'file');
|
||||
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);
|
||||
|
||||
await fs.ensureSymlink(newFilePath, db.path);
|
||||
logger.info(`openMoveDBFileDialog symlink: ${realpath} -> ${newFilePath}`);
|
||||
db.reconnectDB();
|
||||
return {
|
||||
filePath: newFilePath,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error('moveDBFile', err);
|
||||
db?.destroy();
|
||||
logger.error('[moveDBFile]', err);
|
||||
return {
|
||||
error: 'UNKNOWN_ERROR',
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -36,6 +36,8 @@ export const allHandlers = {
|
||||
} satisfies Record<string, NamespaceHandlers>;
|
||||
|
||||
export const registerHandlers = () => {
|
||||
// TODO: listen to namespace instead of individual event types
|
||||
ipcMain.setMaxListeners(100);
|
||||
for (const [namespace, namespaceHandlers] of Object.entries(allHandlers)) {
|
||||
for (const [key, handler] of Object.entries(namespaceHandlers)) {
|
||||
const chan = `${namespace}:${key}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import { app, BrowserWindow, nativeTheme } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../../../utils';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
@@ -17,6 +17,25 @@ export const uiHandlers = {
|
||||
});
|
||||
}
|
||||
},
|
||||
handleMinimizeApp: async () => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(w => {
|
||||
w.minimize();
|
||||
});
|
||||
},
|
||||
handleMaximizeApp: async () => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(w => {
|
||||
if (w.isMaximized()) {
|
||||
w.unmaximize();
|
||||
} else {
|
||||
w.maximize();
|
||||
}
|
||||
});
|
||||
},
|
||||
handleCloseApp: async () => {
|
||||
app.quit();
|
||||
},
|
||||
getGoogleOauthCode: async () => {
|
||||
return getGoogleOauthCode();
|
||||
},
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { updateClient } from './updater';
|
||||
import { checkForUpdatesAndNotify, quitAndInstall } from './updater';
|
||||
|
||||
export const updaterHandlers = {
|
||||
updateClient: async () => {
|
||||
return updateClient();
|
||||
currentVersion: async () => {
|
||||
return app.getVersion();
|
||||
},
|
||||
quitAndInstall: async () => {
|
||||
return quitAndInstall();
|
||||
},
|
||||
checkForUpdatesAndNotify: async () => {
|
||||
return checkForUpdatesAndNotify(true);
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
|
||||
@@ -1,69 +1,100 @@
|
||||
import { app } from 'electron';
|
||||
import type { AppUpdater } from 'electron-updater';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isMacOS } from '../../../../utils';
|
||||
import { updaterSubjects } from '../../events/updater';
|
||||
import { logger } from '../../logger';
|
||||
|
||||
const buildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
|
||||
export const ReleaseTypeSchema = z.enum([
|
||||
'stable',
|
||||
'beta',
|
||||
'canary',
|
||||
'internal',
|
||||
]);
|
||||
|
||||
export const envBuildType = (process.env.BUILD_TYPE || 'canary')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
export const buildType = ReleaseTypeSchema.parse(envBuildType);
|
||||
const mode = process.env.NODE_ENV;
|
||||
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
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { autoUpdater } = await import('electron-updater');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
_autoUpdater = autoUpdater;
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
autoUpdater.setFeedURL({
|
||||
channel: buildType,
|
||||
provider: 'github',
|
||||
repo: 'AFFiNE',
|
||||
owner: 'toeverything',
|
||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
autoUpdater.setFeedURL({
|
||||
channel: buildType,
|
||||
provider: 'github',
|
||||
repo: 'AFFiNE',
|
||||
owner: 'toeverything',
|
||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||
});
|
||||
|
||||
if (isMacOS()) {
|
||||
autoUpdater.on('update-available', () => {
|
||||
autoUpdater.downloadUpdate();
|
||||
logger.info('Update available, downloading...');
|
||||
});
|
||||
autoUpdater.on('download-progress', e => {
|
||||
logger.info(`Download progress: ${e.percent}`);
|
||||
});
|
||||
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();
|
||||
if (!_autoUpdater) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: support auto update on windows and linux
|
||||
const allowAutoUpdate = isMacOS();
|
||||
|
||||
_autoUpdater.autoDownload = false;
|
||||
_autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
_autoUpdater.autoInstallOnAppQuit = false;
|
||||
_autoUpdater.autoRunAppAfterInstall = true;
|
||||
_autoUpdater.setFeedURL({
|
||||
channel: buildType,
|
||||
provider: 'github',
|
||||
repo: buildType !== 'internal' ? 'AFFiNE' : 'AFFiNE-Releases',
|
||||
owner: 'toeverything',
|
||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||
});
|
||||
|
||||
// register events for checkForUpdatesAndNotify
|
||||
_autoUpdater.on('update-available', info => {
|
||||
if (allowAutoUpdate) {
|
||||
_autoUpdater!.downloadUpdate();
|
||||
logger.info('Update available, downloading...', info);
|
||||
}
|
||||
updaterSubjects.updateAvailable.next({
|
||||
version: info.version,
|
||||
allowAutoUpdate,
|
||||
});
|
||||
});
|
||||
_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,
|
||||
});
|
||||
// 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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { isMacOS } from '../../utils';
|
||||
import { isMacOS, isWindows } from '../../utils';
|
||||
import { logger } from './logger';
|
||||
|
||||
const IS_DEV: boolean =
|
||||
@@ -18,13 +18,17 @@ async function createWindow() {
|
||||
});
|
||||
|
||||
const browserWindow = new BrowserWindow({
|
||||
titleBarStyle: isMacOS() ? 'hiddenInset' : 'default',
|
||||
titleBarStyle: isMacOS()
|
||||
? 'hiddenInset'
|
||||
: isWindows()
|
||||
? 'hidden'
|
||||
: 'default',
|
||||
trafficLightPosition: { x: 24, y: 18 },
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
minWidth: 640,
|
||||
transparent: isMacOS(),
|
||||
minHeight: 480,
|
||||
visualEffectState: 'active',
|
||||
vibrancy: 'under-window',
|
||||
height: mainWindowState.height,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../types/**/*.d.ts", "index.ts", "../utils.ts"]
|
||||
}
|
||||
6
apps/electron/layers/preload/preload.d.ts
vendored
6
apps/electron/layers/preload/preload.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ const events: MainIPCEventMap = (() => {
|
||||
const {
|
||||
events: eventsMeta,
|
||||
}: MainExposedMeta = require('../main/exposed-meta');
|
||||
|
||||
// NOTE: ui may try to listen to a lot of the same events, so we increase the limit...
|
||||
ipcRenderer.setMaxListeners(100);
|
||||
|
||||
const all = eventsMeta.map(([namespace, eventNames]) => {
|
||||
const namespaceEvents = eventNames.map(name => {
|
||||
const channel = `${namespace}:${name}`;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../types/**/*.d.ts"]
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
export const isWindows = () => {
|
||||
return process.platform === 'win32';
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.5.4-canary.30",
|
||||
"version": "0.5.4-beta.1",
|
||||
"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.2.0",
|
||||
"electron-log": "^5.0.0-beta.23",
|
||||
"electron": "24.3.1",
|
||||
"electron-log": "^5.0.0-beta.24",
|
||||
"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": {
|
||||
|
||||
BIN
apps/electron/resources/icons/icon_internal.icns
Normal file
BIN
apps/electron/resources/icons/icon_internal.icns
Normal file
Binary file not shown.
BIN
apps/electron/resources/icons/icon_internal.ico
Normal file
BIN
apps/electron/resources/icons/icon_internal.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#!/usr/bin/env zx
|
||||
import 'zx/globals';
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const repoRootDir = path.join(__dirname, '..', '..', '..');
|
||||
const electronRootDir = path.join(__dirname, '..');
|
||||
const publicDistDir = path.join(electronRootDir, 'resources');
|
||||
const affineWebDir = path.join(repoRootDir, 'apps', 'web');
|
||||
const affineWebOutDir = path.join(affineWebDir, 'out');
|
||||
const publicAffineOutDir = path.join(publicDistDir, `web-static`);
|
||||
const releaseVersionEnv = process.env.RELEASE_VERSION || '';
|
||||
|
||||
console.log('build with following dir', {
|
||||
repoRootDir,
|
||||
@@ -19,9 +23,16 @@ console.log('build with following dir', {
|
||||
publicAffineOutDir,
|
||||
});
|
||||
|
||||
// step 0: check version match
|
||||
const electronPackageJson = require(`${electronRootDir}/package.json`);
|
||||
if (releaseVersionEnv && electronPackageJson.version !== releaseVersionEnv) {
|
||||
throw new Error(
|
||||
`Version mismatch, expected ${releaseVersionEnv} but got ${electronPackageJson.version}`
|
||||
);
|
||||
}
|
||||
// copy web dist files to electron dist
|
||||
|
||||
// step 0: clean up
|
||||
// step 1: clean up
|
||||
await cleanup();
|
||||
echo('Clean up done');
|
||||
|
||||
@@ -32,9 +43,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 +67,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);
|
||||
}
|
||||
|
||||
/// --------
|
||||
/// --------
|
||||
/// --------
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
#!/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
|
||||
const { handlers, events } = await import(
|
||||
path.resolve(mainDistDir, 'exposed.js')
|
||||
'file://' + path.resolve(mainDistDir, 'exposed.js')
|
||||
);
|
||||
|
||||
const handlersMeta = Object.entries(handlers).map(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { platform } from 'node:os';
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from './fixture';
|
||||
@@ -11,8 +13,74 @@ 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 }) => {
|
||||
await page.waitForSelector('v-line');
|
||||
const root = page.locator('html');
|
||||
{
|
||||
const themeMode = await root.evaluate(element =>
|
||||
@@ -20,30 +88,25 @@ test('app theme', async ({ page, electronApp }) => {
|
||||
);
|
||||
expect(themeMode).toBe('light');
|
||||
|
||||
// check if electron theme source is set to light
|
||||
const themeSource = await electronApp.evaluate(({ nativeTheme }) => {
|
||||
return nativeTheme.themeSource;
|
||||
const theme = await electronApp.evaluate(({ nativeTheme }) => {
|
||||
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
expect(themeSource).toBe('light');
|
||||
expect(theme).toBe('light');
|
||||
}
|
||||
|
||||
{
|
||||
await page.getByTestId('editor-option-menu').click();
|
||||
await page.getByTestId('change-theme-dark').click();
|
||||
await page.waitForTimeout(50);
|
||||
{
|
||||
const themeMode = await root.evaluate(element =>
|
||||
element.getAttribute('data-theme')
|
||||
);
|
||||
expect(themeMode).toBe('dark');
|
||||
}
|
||||
|
||||
const themeSource = await electronApp.evaluate(({ nativeTheme }) => {
|
||||
return nativeTheme.themeSource;
|
||||
const themeMode = await root.evaluate(element =>
|
||||
element.getAttribute('data-theme')
|
||||
);
|
||||
expect(themeMode).toBe('dark');
|
||||
const theme = await electronApp.evaluate(({ nativeTheme }) => {
|
||||
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
expect(themeSource).toBe('dark');
|
||||
expect(theme).toBe('dark');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,7 +136,7 @@ test('affine onboarding button', async ({ page }) => {
|
||||
'[data-testid=onboarding-modal-editing-video]'
|
||||
);
|
||||
expect(await editingVideo.isVisible()).toEqual(true);
|
||||
await page.getByTestId('onboarding-modal-ok-button').click();
|
||||
await page.getByTestId('onboarding-modal-close-button').click();
|
||||
|
||||
expect(await onboardingModal.isVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -42,7 +47,31 @@ export const test = base.extend<{
|
||||
const logFilePath = await page.evaluate(async () => {
|
||||
return window.apis?.debug.logFilePath();
|
||||
});
|
||||
// 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');
|
||||
@@ -52,16 +81,27 @@ 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',
|
||||
});
|
||||
const sessionDataPath = await electronApp.evaluate(async ({ app }) => {
|
||||
return app.getPath('sessionData');
|
||||
});
|
||||
await use(electronApp);
|
||||
await fs.rm(sessionDataPath, { recursive: true, force: true });
|
||||
// FIXME: the following does not work well on CI
|
||||
// const sessionDataPath = await electronApp.evaluate(async ({ app }) => {
|
||||
// return app.getPath('sessionData');
|
||||
// });
|
||||
// await fs.rm(sessionDataPath, { recursive: true, force: true });
|
||||
},
|
||||
appInfo: async ({ electronApp }, use) => {
|
||||
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
||||
|
||||
@@ -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, '..'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": false,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"noEmit": false
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["layers", "types", "package.json"],
|
||||
"include": ["**/*.ts", "**/*.tsx", "package.json"],
|
||||
"exclude": ["out", "dist", "node_modules"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/native"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.5.4-canary.30",
|
||||
"version": "0.5.4-beta.1",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -16,36 +16,35 @@
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.7.1",
|
||||
"@nestjs/apollo": "^11.0.5",
|
||||
"@nestjs/common": "^9.4.0",
|
||||
"@nestjs/core": "^9.4.0",
|
||||
"@nestjs/common": "^9.4.1",
|
||||
"@nestjs/core": "^9.4.1",
|
||||
"@nestjs/graphql": "^11.0.5",
|
||||
"@nestjs/platform-express": "^9.4.0",
|
||||
"@prisma/client": "^4.13.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"@nestjs/platform-express": "^9.4.1",
|
||||
"@node-rs/bcrypt": "^1.7.1",
|
||||
"@prisma/client": "^4.14.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"prisma": "^4.13.0",
|
||||
"prisma": "^4.14.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/testing": "^9.4.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@nestjs/testing": "^9.4.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^18.16.6",
|
||||
"@types/node": "^18.16.12",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"c8": "^7.13.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^0.31.0"
|
||||
"vitest": "^0.31.1"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "node",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -169,10 +169,6 @@ export interface AFFiNEConfig {
|
||||
* authentication config
|
||||
*/
|
||||
auth: {
|
||||
/**
|
||||
* Application sign key secret
|
||||
*/
|
||||
readonly salt: string;
|
||||
/**
|
||||
* Application access token expiration time
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
debug: true,
|
||||
},
|
||||
auth: {
|
||||
salt: '$2b$10$x4VDo2nmlo74yB5jflNhlu',
|
||||
accessTokenExpiresIn: '1h',
|
||||
refreshTokenExpiresIn: '7d',
|
||||
publicKey: examplePublicKey,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/web",
|
||||
"private": true,
|
||||
"version": "0.5.4-canary.30",
|
||||
"version": "0.5.4-beta.1",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@@ -19,32 +19,33 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
||||
"@blocksuite/icons": "^2.1.15",
|
||||
"@blocksuite/store": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/icons": "^2.1.16",
|
||||
"@blocksuite/lit": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.12.3",
|
||||
"@mui/material": "^5.13.1",
|
||||
"@react-hookz/web": "^23.0.0",
|
||||
"@sentry/nextjs": "^7.51.2",
|
||||
"@sentry/nextjs": "^7.52.1",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"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,16 +67,16 @@
|
||||
"@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",
|
||||
"redux": "^4.2.1",
|
||||
"swc-plugin-coverage-instrument": "^0.0.18",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^5.82.0"
|
||||
"webpack": "^5.83.1"
|
||||
},
|
||||
"stableVersion": "0.0.0"
|
||||
}
|
||||
|
||||
@@ -33,4 +33,7 @@ export const buildFlags = {
|
||||
enableDebugPage: Boolean(
|
||||
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
|
||||
),
|
||||
changelogUrl:
|
||||
process.env.CHANGELOG_URL ??
|
||||
'https://affine.pro/blog/whats-new-affine-0518',
|
||||
};
|
||||
|
||||
@@ -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, [
|
||||
|
||||
99
apps/web/src/atoms/history.ts
Normal file
99
apps/web/src/atoms/history.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,52 +1,19 @@
|
||||
import type {
|
||||
QueryParamError,
|
||||
Unreachable,
|
||||
WorkspaceNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { RequestError } from '@affine/workspace/affine/api';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import type { ErrorInfo, ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../../shared';
|
||||
|
||||
export type AffineErrorBoundaryProps = React.PropsWithChildren<{
|
||||
router: NextRouter;
|
||||
}>;
|
||||
|
||||
export class PageNotFoundError extends TypeError {
|
||||
readonly workspace: BlockSuiteWorkspace;
|
||||
readonly pageId: string;
|
||||
|
||||
constructor(workspace: BlockSuiteWorkspace, pageId: string) {
|
||||
super();
|
||||
this.workspace = workspace;
|
||||
this.pageId = pageId;
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceNotFoundError extends TypeError {
|
||||
readonly workspaceId: string;
|
||||
|
||||
constructor(workspaceId: string) {
|
||||
super();
|
||||
this.workspaceId = workspaceId;
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryParamError extends TypeError {
|
||||
readonly targetKey: string;
|
||||
readonly query: unknown;
|
||||
|
||||
constructor(targetKey: string, query: unknown) {
|
||||
super();
|
||||
this.targetKey = targetKey;
|
||||
this.query = query;
|
||||
}
|
||||
}
|
||||
|
||||
export class Unreachable extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
type AffineError =
|
||||
| QueryParamError
|
||||
| Unreachable
|
||||
|
||||
@@ -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);
|
||||
@@ -118,10 +118,7 @@ interface SetDBLocationContentProps {
|
||||
onConfirmLocation: (dir?: string) => void;
|
||||
}
|
||||
|
||||
const SetDBLocationContent = ({
|
||||
onConfirmLocation,
|
||||
}: SetDBLocationContentProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const useDefaultDBLocation = () => {
|
||||
const [defaultDBLocation, setDefaultDBLocation] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,20 +127,40 @@ const SetDBLocationContent = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
return defaultDBLocation;
|
||||
};
|
||||
|
||||
const SetDBLocationContent = ({
|
||||
onConfirmLocation,
|
||||
}: SetDBLocationContentProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const defaultDBLocation = useDefaultDBLocation();
|
||||
const [opening, setOpening] = useState(false);
|
||||
|
||||
const handleSelectDBFileLocation = async () => {
|
||||
if (opening) {
|
||||
return;
|
||||
}
|
||||
setOpening(true);
|
||||
const result = await window.apis?.dialog.selectDBFileLocation();
|
||||
setOpening(false);
|
||||
if (result?.filePath) {
|
||||
onConfirmLocation(result.filePath);
|
||||
} else if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<div className={style.contentTitle}>{t['Set database location']()}</div>
|
||||
<p>{t['Workspace database storage description']()}</p>
|
||||
<div className={style.buttonGroup}>
|
||||
<Button
|
||||
disabled={opening}
|
||||
data-testid="create-workspace-customize-button"
|
||||
type="light"
|
||||
onClick={async () => {
|
||||
const result = await window.apis?.dialog.selectDBFileLocation();
|
||||
if (result) {
|
||||
onConfirmLocation(result.filePath);
|
||||
}
|
||||
}}
|
||||
onClick={handleSelectDBFileLocation}
|
||||
>
|
||||
{t['Customize']()}
|
||||
</Button>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 be notified the first
|
||||
time it's available, please
|
||||
<a
|
||||
href="https://github.com/toeverything/AFFiNE/releases"
|
||||
target="_blank"
|
||||
style={{
|
||||
color: 'var(--affine-link-color)',
|
||||
}}
|
||||
>
|
||||
click here
|
||||
</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>
|
||||
|
||||
@@ -6,8 +6,10 @@ import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '52px 52px 0 52px',
|
||||
marginTop: '52px',
|
||||
padding: '0 52px 52px 52px',
|
||||
height: 'calc(100vh - 52px)',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
export const sidebar = style({
|
||||
@@ -15,7 +17,6 @@ export const sidebar = style({
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
overflow: 'auto',
|
||||
flex: 1,
|
||||
marginTop: '40px',
|
||||
});
|
||||
@@ -110,7 +111,8 @@ export const settingItemLabelHint = style({
|
||||
export const row = style({
|
||||
padding: '40px 0',
|
||||
display: 'flex',
|
||||
gap: '60px',
|
||||
columnGap: '60px',
|
||||
rowGap: '12px',
|
||||
selectors: {
|
||||
'&': {
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
@@ -119,22 +121,22 @@ export const row = style({
|
||||
paddingTop: 0,
|
||||
},
|
||||
},
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
export const col = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
flexShrink: 0,
|
||||
selectors: {
|
||||
[`${row} &:nth-child(1)`]: {
|
||||
flex: 3,
|
||||
flex: '3 0 200px',
|
||||
},
|
||||
[`${row} &:nth-child(2)`]: {
|
||||
flex: 5,
|
||||
flex: '5 0 240px',
|
||||
},
|
||||
[`${row} &:nth-child(3)`]: {
|
||||
flex: 2,
|
||||
flex: '2 0 200px',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
},
|
||||
@@ -156,7 +158,10 @@ export const indicator = style({
|
||||
|
||||
export const tabButtonWrapper = style({
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
position: 'sticky',
|
||||
top: '0',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const storageTypeWrapper = style({
|
||||
@@ -175,6 +180,10 @@ export const storageTypeWrapper = style({
|
||||
'&:not(:last-child)': {
|
||||
marginBottom: '12px',
|
||||
},
|
||||
'&[data-disabled="true"]': {
|
||||
cursor: 'default',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
||||
import { config } from '@affine/env';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
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,
|
||||
@@ -14,7 +18,6 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import { useMembers } from '../../../../../hooks/affine/use-members';
|
||||
import { toast } from '../../../../../utils';
|
||||
import { Unreachable } from '../../../affine-error-eoundary';
|
||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
|
||||
import type { PanelProps } from '../../index';
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -15,8 +15,13 @@ export const ExportPanel = () => {
|
||||
disabled={!environment.isDesktop || !id}
|
||||
data-testid="export-affine-backup"
|
||||
onClick={async () => {
|
||||
if (id && (await window.apis?.dialog.saveDBFileAs(id))) {
|
||||
toast(t['Export success']());
|
||||
if (id) {
|
||||
const result = await window.apis?.dialog.saveDBFileAs(id);
|
||||
if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
|
||||
import { Upload } from '../../../../pure/file-upload';
|
||||
@@ -23,6 +23,26 @@ import { CameraIcon } from './icons';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
import { StyledInput } from './style';
|
||||
|
||||
const useDBFilePathMeta = (workspaceId: string) => {
|
||||
const [meta, setMeta] = useState<{
|
||||
path: string;
|
||||
realPath: string;
|
||||
}>();
|
||||
useEffect(() => {
|
||||
if (window.apis && window.events) {
|
||||
window.apis.db.getDBFilePath(workspaceId).then(meta => {
|
||||
setMeta(meta);
|
||||
});
|
||||
return window.events.db.onDBFilePathChange(meta => {
|
||||
if (meta.workspaceId === workspaceId) {
|
||||
setMeta(meta);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [workspaceId]);
|
||||
return meta;
|
||||
};
|
||||
|
||||
export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
@@ -36,11 +56,36 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
const isOwner = useIsWorkspaceOwner(workspace);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const dbPathMeta = useDBFilePathMeta(workspace.id);
|
||||
const showOpenFolder =
|
||||
environment.isDesktop && dbPathMeta?.path !== dbPathMeta?.realPath;
|
||||
|
||||
const handleUpdateWorkspaceName = (name: string) => {
|
||||
setName(name);
|
||||
toast(t['Update workspace name success']());
|
||||
};
|
||||
|
||||
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
||||
|
||||
const handleMoveTo = async () => {
|
||||
if (moveToInProgress) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setMoveToInProgress(true);
|
||||
const result = await window.apis?.dialog.moveDBFile(workspace.id);
|
||||
if (!result?.error && !result?.canceled) {
|
||||
toast(t['Move folder success']());
|
||||
} else if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
}
|
||||
} catch (err) {
|
||||
toast(t['UNKNOWN_ERROR']());
|
||||
} finally {
|
||||
setMoveToInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
@@ -88,12 +133,11 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
|
||||
<div className={style.col}>
|
||||
<StyledInput
|
||||
width={284}
|
||||
height={38}
|
||||
value={input}
|
||||
data-testid="workspace-name-input"
|
||||
placeholder={t['Workspace Name']()}
|
||||
maxLength={50}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
onChange={newName => {
|
||||
setInput(newName);
|
||||
@@ -129,34 +173,33 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
</div>
|
||||
|
||||
<div className={style.col}>
|
||||
<div
|
||||
className={style.storageTypeWrapper}
|
||||
onClick={() => {
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.dialog.revealDBFile(workspace.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FolderIcon color="var(--affine-primary-color)" />
|
||||
<div className={style.storageTypeLabelWrapper}>
|
||||
<div className={style.storageTypeLabel}>
|
||||
{t['Open folder']()}
|
||||
</div>
|
||||
<div className={style.storageTypeLabelHint}>
|
||||
{t['Open folder hint']()}
|
||||
{showOpenFolder && (
|
||||
<div
|
||||
className={style.storageTypeWrapper}
|
||||
onClick={() => {
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.dialog.revealDBFile(workspace.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FolderIcon color="var(--affine-primary-color)" />
|
||||
<div className={style.storageTypeLabelWrapper}>
|
||||
<div className={style.storageTypeLabel}>
|
||||
{t['Open folder']()}
|
||||
</div>
|
||||
<div className={style.storageTypeLabelHint}>
|
||||
{t['Open folder hint']()}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
|
||||
</div>
|
||||
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
data-testid="move-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
className={style.storageTypeWrapper}
|
||||
onClick={async () => {
|
||||
if (await window.apis?.dialog.moveDBFile(workspace.id)) {
|
||||
toast(t['Move folder success']());
|
||||
}
|
||||
}}
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
<MoveToIcon color="var(--affine-primary-color)" />
|
||||
<div className={style.storageTypeLabelWrapper}>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { Input } from '@affine/component';
|
||||
|
||||
export const StyledInput = styled(Input)(() => {
|
||||
return {
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
borderRadius: '8px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
};
|
||||
});
|
||||
export const StyledInput = Input;
|
||||
|
||||
export const StyledWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
|
||||
@@ -6,8 +6,12 @@ import {
|
||||
Wrapper,
|
||||
} from '@affine/component';
|
||||
import { config } from '@affine/env';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
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';
|
||||
@@ -16,7 +20,6 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
|
||||
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
||||
import { toast } from '../../../../../utils';
|
||||
import { Unreachable } from '../../../affine-error-eoundary';
|
||||
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||
import type { WorkspaceSettingDetailProps } from '../../index';
|
||||
@@ -26,7 +29,7 @@ export type PublishPanelProps = WorkspaceSettingDetailProps & {
|
||||
};
|
||||
|
||||
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
|
||||
workspace: AffineWorkspace;
|
||||
workspace: AffineLegacyCloudWorkspace;
|
||||
};
|
||||
|
||||
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
@@ -27,6 +28,7 @@ export const EditorModeSwitch = ({
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const t = useAFFiNEI18N();
|
||||
assertExists(pageMeta);
|
||||
const { trash } = pageMeta;
|
||||
|
||||
@@ -41,8 +43,12 @@ export const EditorModeSwitch = ({
|
||||
active={currentMode === 'page'}
|
||||
hide={trash && currentMode !== 'page'}
|
||||
onClick={() => {
|
||||
setMode(mode => ({ ...mode, [pageMeta.id]: 'page' }));
|
||||
toast('Page mode');
|
||||
setMode(mode => {
|
||||
if (mode[pageMeta.id] !== 'page') {
|
||||
toast(t['com.affine.pageMode']());
|
||||
}
|
||||
return { ...mode, [pageMeta.id]: 'page' };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<EdgelessSwitchItem
|
||||
@@ -50,8 +56,12 @@ export const EditorModeSwitch = ({
|
||||
active={currentMode === 'edgeless'}
|
||||
hide={trash && currentMode !== 'edgeless'}
|
||||
onClick={() => {
|
||||
setMode(mode => ({ ...mode, [pageMeta.id]: 'edgeless' }));
|
||||
toast('Edgeless mode');
|
||||
setMode(mode => {
|
||||
if (mode[pageMeta.id] !== 'edgeless') {
|
||||
toast(t['com.affine.edgelessMode']());
|
||||
}
|
||||
return { ...mode, [pageMeta.id]: 'edgeless' };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</StyledEditorModeSwitch>
|
||||
|
||||
@@ -24,10 +24,7 @@ import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id'
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { toast } from '../../../../utils';
|
||||
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
|
||||
import {
|
||||
StyledHorizontalDivider,
|
||||
StyledHorizontalDividerContainer,
|
||||
} from '../styles';
|
||||
import * as styles from '../styles.css';
|
||||
import { LanguageMenu } from './language-menu';
|
||||
const CommonMenu = () => {
|
||||
const content = (
|
||||
@@ -43,7 +40,6 @@ const CommonMenu = () => {
|
||||
return (
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
width={276}
|
||||
content={content}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
@@ -120,9 +116,9 @@ const PageMenu = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<StyledHorizontalDividerContainer>
|
||||
<StyledHorizontalDivider />
|
||||
</StyledHorizontalDividerContainer>
|
||||
<div className={styles.horizontalDividerContainer}>
|
||||
<div className={styles.horizontalDivider} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div
|
||||
@@ -140,7 +136,6 @@ const PageMenu = () => {
|
||||
<>
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
width={276}
|
||||
content={EditMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
@@ -156,6 +151,7 @@ const PageMenu = () => {
|
||||
onConfirm={() => {
|
||||
removeToTrash(pageMeta.id);
|
||||
toast(t['Moved to Trash']());
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setOpenConfirm(false);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import { config } from '@affine/env';
|
||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
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';
|
||||
@@ -12,19 +16,18 @@ import { useToggleWorkspacePublish } from '../../../../hooks/affine/use-toggle-w
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
||||
import { WorkspaceSubPath } from '../../../../shared';
|
||||
import { Unreachable } from '../../../affine/affine-error-eoundary';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
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(
|
||||
|
||||
@@ -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')(() => {
|
||||
|
||||
@@ -1,9 +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,
|
||||
@@ -24,11 +28,7 @@ import { HeaderShareMenu } from './header-right-items/share-menu';
|
||||
import SyncUser from './header-right-items/sync-user';
|
||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||
import UserAvatar from './header-right-items/user-avatar';
|
||||
import {
|
||||
StyledHeader,
|
||||
StyledHeaderContainer,
|
||||
StyledHeaderRightSide,
|
||||
} from './styles';
|
||||
import * as styles from './styles.css';
|
||||
import { OSWarningMessage, shouldShowWarning } from './utils';
|
||||
|
||||
const SidebarSwitch = lazy(() =>
|
||||
@@ -53,6 +53,9 @@ export const enum HeaderRightItemName {
|
||||
ShareMenu = 'shareMenu',
|
||||
EditPage = 'editPage',
|
||||
UserAvatar = 'userAvatar',
|
||||
|
||||
// some windows only items
|
||||
WindowsAppControls = 'windowsAppControls',
|
||||
}
|
||||
|
||||
type HeaderItem = {
|
||||
@@ -67,6 +70,7 @@ type HeaderItem = {
|
||||
}
|
||||
) => boolean;
|
||||
};
|
||||
|
||||
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
[HeaderRightItemName.TrashButtonGroup]: {
|
||||
Component: TrashButtonGroup,
|
||||
@@ -104,6 +108,44 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
return !isPublic && !isPreview;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.WindowsAppControls]: {
|
||||
Component: () => {
|
||||
return (
|
||||
<div className={styles.windowAppControlsWrapper}>
|
||||
<button
|
||||
data-type="minimize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={() => {
|
||||
window.apis?.ui.handleMinimizeApp();
|
||||
}}
|
||||
>
|
||||
<MinusIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="maximize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={() => {
|
||||
window.apis?.ui.handleMaximizeApp();
|
||||
}}
|
||||
>
|
||||
<RoundedRectangleIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="close"
|
||||
className={styles.windowAppControl}
|
||||
onClick={() => {
|
||||
window.apis?.ui.handleCloseApp();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
availableWhen: () => {
|
||||
return environment.isDesktop && environment.isWindows;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type HeaderProps = BaseHeaderProps;
|
||||
@@ -122,15 +164,19 @@ 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 (
|
||||
<StyledHeaderContainer
|
||||
<div
|
||||
className={styles.headerContainer}
|
||||
ref={ref}
|
||||
hasWarning={showWarning}
|
||||
data-has-warning={showWarning}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
{...props}
|
||||
>
|
||||
{showGuideDownloadClientTip ? (
|
||||
@@ -145,11 +191,11 @@ export const Header = forwardRef<
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledHeader
|
||||
hasWarning={showWarning}
|
||||
<div
|
||||
className={styles.header}
|
||||
data-has-warning={showWarning}
|
||||
data-testid="editor-header-items"
|
||||
data-tauri-drag-region
|
||||
isEdgeless={mode === 'edgeless'}
|
||||
data-is-edgeless={mode === 'edgeless'}
|
||||
>
|
||||
<Suspense>
|
||||
<SidebarSwitch
|
||||
@@ -160,7 +206,7 @@ export const Header = forwardRef<
|
||||
</Suspense>
|
||||
|
||||
{props.children}
|
||||
<StyledHeaderRightSide>
|
||||
<div className={styles.headerRightSide}>
|
||||
{useMemo(() => {
|
||||
return Object.entries(HeaderRightItems).map(
|
||||
([name, { availableWhen, Component }]) => {
|
||||
@@ -184,10 +230,9 @@ export const Header = forwardRef<
|
||||
}
|
||||
);
|
||||
}, [props])}
|
||||
{/*<ShareMenu />*/}
|
||||
</StyledHeaderRightSide>
|
||||
</StyledHeader>
|
||||
</StyledHeaderContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,99 +1,43 @@
|
||||
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';
|
||||
import { Header } from './header';
|
||||
import {
|
||||
StyledQuickSearchTipButton,
|
||||
StyledQuickSearchTipContent,
|
||||
StyledSearchArrowWrapper,
|
||||
StyledSwitchWrapper,
|
||||
StyledTitle,
|
||||
StyledTitleContainer,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
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 = (
|
||||
<StyledQuickSearchTipContent>
|
||||
<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>
|
||||
<StyledQuickSearchTipButton
|
||||
data-testid="quick-search-got-it"
|
||||
onClick={() => setShowQuickSearchTips(false)}
|
||||
>
|
||||
Got it
|
||||
</StyledQuickSearchTipButton>
|
||||
</StyledQuickSearchTipContent>
|
||||
);
|
||||
|
||||
return (
|
||||
<Header ref={ref} {...props}>
|
||||
<Header ref={headerRef} {...props}>
|
||||
{children}
|
||||
{!isPublic && currentPage && (
|
||||
<StyledTitleContainer data-tauri-drag-region>
|
||||
<StyledTitleWrapper>
|
||||
<StyledSwitchWrapper>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.titleWrapper}>
|
||||
<div className={styles.switchWrapper}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={currentPage.id}
|
||||
@@ -101,29 +45,21 @@ export const WorkspaceHeader = forwardRef<
|
||||
marginRight: '12px',
|
||||
}}
|
||||
/>
|
||||
</StyledSwitchWrapper>
|
||||
<StyledTitle>{title || 'Untitled'}</StyledTitle>
|
||||
<QuickSearchTips
|
||||
data-testid="quick-search-tips"
|
||||
content={TipsContent}
|
||||
placement="bottom"
|
||||
popperRef={popperRef}
|
||||
open={showQuickSearchTips}
|
||||
offset={[0, -5]}
|
||||
>
|
||||
<StyledSearchArrowWrapper>
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</StyledSearchArrowWrapper>
|
||||
</QuickSearchTips>
|
||||
</StyledTitleWrapper>
|
||||
</StyledTitleContainer>
|
||||
</div>
|
||||
<div className={styles.title}>{title || 'Untitled'}</div>
|
||||
|
||||
<div className={styles.searchArrowWrapper}>
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
WorkspaceHeader.displayName = 'BlockSuiteEditorHeader';
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import type { ComplexStyleRule } from '@vanilla-extract/css';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerContainer = style({
|
||||
height: '52px',
|
||||
flexShrink: 0,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
selectors: {
|
||||
'&[data-has-warning="true"]': {
|
||||
height: '96px',
|
||||
},
|
||||
'&[data-sidebar-floating="false"]': {
|
||||
WebkitAppRegion: 'drag',
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const header = style({
|
||||
flexShrink: 0,
|
||||
height: '52px',
|
||||
width: '100%',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 99,
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&[data-is-edgeless="true"]': {
|
||||
borderBottom: `1px solid var(--affine-border-color)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const titleContainer = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
margin: 'auto',
|
||||
position: 'absolute',
|
||||
inset: 'auto auto auto 50%',
|
||||
transform: 'translate(-50%, 0px)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignContent: 'unset',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
maxWidth: '620px',
|
||||
transition: 'max-width .15s',
|
||||
userSelect: 'none',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'@media': {
|
||||
'(max-width: 768px)': {
|
||||
selectors: {
|
||||
'&[data-open="true"]': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const titleWrapper = style({
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const headerRightSide = style({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const browserWarning = style({
|
||||
backgroundColor: 'var(--affine-background-warning-color)',
|
||||
color: 'var(--affine-warning-color)',
|
||||
height: '36px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
display: 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
selectors: {
|
||||
'&[data-show="true"]': {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const closeButton = style({
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
right: '15px',
|
||||
top: 0,
|
||||
});
|
||||
|
||||
export const switchWrapper = style({
|
||||
position: 'absolute',
|
||||
right: '100%',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const searchArrowWrapper = style({
|
||||
position: 'absolute',
|
||||
left: 'calc(100% + 4px)',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const pageListTitleWrapper = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const pageListTitleIcon = style({
|
||||
fontSize: '20px',
|
||||
height: '1em',
|
||||
marginRight: '12px',
|
||||
});
|
||||
|
||||
export const quickSearchTipButton = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: '12px',
|
||||
color: '#FFFFFF',
|
||||
width: '48px',
|
||||
height: ' 26px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
background: 'var(--affine-primary-color)',
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const quickSearchTipContent = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const horizontalDivider = style({
|
||||
width: '100%',
|
||||
borderTop: `1px solid var(--affine-border-color)`,
|
||||
});
|
||||
|
||||
export const horizontalDividerContainer = style({
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
});
|
||||
|
||||
export const windowAppControlsWrapper = style({
|
||||
display: 'flex',
|
||||
gap: '2px',
|
||||
transform: 'translateX(8px)',
|
||||
});
|
||||
|
||||
export const windowAppControl = style({
|
||||
WebkitAppRegion: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
width: '32px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '2px',
|
||||
selectors: {
|
||||
'&[data-type="close"]:hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
'&:hover': {
|
||||
background: 'var(--affine-background-tertiary-color)',
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
@@ -1,186 +0,0 @@
|
||||
import {
|
||||
absoluteCenter,
|
||||
displayFlex,
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
|
||||
export const StyledHeaderContainer = styled('div')<{
|
||||
hasWarning: boolean;
|
||||
}>(({ hasWarning }) => {
|
||||
return {
|
||||
height: hasWarning ? '96px' : '52px',
|
||||
flexShrink: 0,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
WebkitAppRegion: 'drag',
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
'@media (max-width: 768px)': {
|
||||
'&[data-open="true"]': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledHeader = styled('div')<{
|
||||
hasWarning: boolean;
|
||||
isEdgeless: boolean;
|
||||
}>(({ isEdgeless }) => {
|
||||
return {
|
||||
flexShrink: 0,
|
||||
height: '52px',
|
||||
width: '100%',
|
||||
padding: '0 20px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 99,
|
||||
position: 'relative',
|
||||
borderBottom: isEdgeless ? '1px solid var(--affine-border-color)' : 'none',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTitleContainer = styled('div')(() => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
margin: 'auto',
|
||||
...absoluteCenter({ horizontal: true, position: { top: 0 } }),
|
||||
...displayFlex('center', 'center'),
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
}));
|
||||
|
||||
export const StyledTitle = styled('div')(({ theme }) => {
|
||||
return {
|
||||
maxWidth: '620px',
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
maxWidth: '480px',
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
maxWidth: '240px',
|
||||
},
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
maxWidth: '180px',
|
||||
},
|
||||
transition: 'max-width .15s',
|
||||
userSelect: 'none',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTitleWrapper = styled('div')({
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
...displayFlex('center', 'center'),
|
||||
});
|
||||
|
||||
export const StyledHeaderRightSide = styled('div')({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'>*:not(:last-child)': {
|
||||
marginRight: '12px',
|
||||
},
|
||||
});
|
||||
|
||||
export const StyledBrowserWarning = styled('div')<{ show: boolean }>(
|
||||
({ show }) => {
|
||||
return {
|
||||
backgroundColor: 'var(--affine-background-warning-color)',
|
||||
color: 'var(--affine-background-warning-color)',
|
||||
height: '36px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
display: show ? 'flex' : 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledCloseButton = styled('div')(() => {
|
||||
return {
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
cursor: 'pointer',
|
||||
...displayFlex('center', 'center'),
|
||||
position: 'absolute',
|
||||
right: '15px',
|
||||
top: '0',
|
||||
|
||||
svg: {
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSwitchWrapper = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '100%',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSearchArrowWrapper = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: 'calc(100% + 4px)',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPageListTittleWrapper = styled(StyledTitle)(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
...displayFlex('center', 'center'),
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledQuickSearchTipButton = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('center', 'center'),
|
||||
marginTop: '12px',
|
||||
color: '#FFFFFF',
|
||||
width: '48px',
|
||||
height: ' 26px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
background: 'var(--affine-primary-color)',
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
});
|
||||
export const StyledQuickSearchTipContent = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('center', 'flex-end'),
|
||||
flexDirection: 'column',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledHorizontalDivider = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
borderTop: `1px solid var(--affine-border-color)`,
|
||||
};
|
||||
});
|
||||
export const StyledHorizontalDividerContainer = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
};
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
@@ -11,7 +12,6 @@ import { startTransition, useCallback } from 'react';
|
||||
|
||||
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
import { PageNotFoundError } from './affine/affine-error-eoundary';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import { WorkspaceHeader } from './blocksuite/workspace-header';
|
||||
|
||||
@@ -84,6 +84,7 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
|
||||
page.workspace.setPageMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
localStorage.setItem('last_page_id', page.id);
|
||||
onLoad?.(page, editor);
|
||||
},
|
||||
[onLoad, setEditor]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MuiFade, Tooltip } from '@affine/component';
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { config, getEnvironment } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon } from '@blocksuite/icons';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -74,10 +74,7 @@ export const HelpIsland = ({
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-change-log-icon"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://github.com/toeverything/AFFiNE/releases',
|
||||
'_blank'
|
||||
);
|
||||
window.open(config.changelogUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<NewIcon />
|
||||
@@ -111,7 +108,10 @@ export const HelpIsland = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('guide') && (
|
||||
<Tooltip content={'Easy Guide'} placement="left-end">
|
||||
<Tooltip
|
||||
content={t['com.affine.helpIsland.gettingStarted']()}
|
||||
placement="left-end"
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="easy-guide"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { styled } from '@affine/component';
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { memo, Suspense } from 'react';
|
||||
|
||||
export const Loading = memo(function Loading() {
|
||||
@@ -18,31 +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',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const PageLoading = ({ text }: { text?: string }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<StyledLoadingContainer>
|
||||
<Loading />
|
||||
<h1>{text ? text : t['Loading']()}</h1>
|
||||
</StyledLoadingContainer>
|
||||
);
|
||||
/**
|
||||
* @deprecated use skeleton instead
|
||||
*/
|
||||
export const PageLoading = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PageLoading;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,66 +1,107 @@
|
||||
import { MuiCollapse } from '@affine/component';
|
||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } 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) => {
|
||||
|
||||
interface FavoriteMenuItemProps {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
metaMapping: Record<string, PageMeta>;
|
||||
parentIds: Set<string>;
|
||||
}
|
||||
|
||||
function FavoriteMenuItem({
|
||||
workspace,
|
||||
pageId,
|
||||
metaMapping,
|
||||
parentIds,
|
||||
}: FavoriteMenuItemProps) {
|
||||
const router = useRouter();
|
||||
const record = useAtomValue(workspacePreferredModeAtom);
|
||||
const active = router.query.pageId === pageId;
|
||||
const icon = record[pageId] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
const references = useBlockSuitePageReferences(workspace, pageId);
|
||||
const referencesToShow = useMemo(() => {
|
||||
return [...new Set(references.filter(ref => !parentIds.has(ref)))];
|
||||
}, [references, parentIds]);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0 && parentIds.size === 0;
|
||||
const showReferences = collapsible ? !collapsed : referencesToShow.length > 0;
|
||||
const nestedItem = parentIds.size > 0;
|
||||
return (
|
||||
<>
|
||||
<MenuLinkItem
|
||||
style={{
|
||||
marginLeft: nestedItem ? '12px' : undefined,
|
||||
width: nestedItem ? 'calc(100% - 12px)' : undefined,
|
||||
}}
|
||||
data-type="favorite-list-item"
|
||||
data-testid={`favorite-list-item-${pageId}`}
|
||||
active={active}
|
||||
href={`/workspace/${workspace.id}/${pageId}`}
|
||||
icon={icon}
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
>
|
||||
<span>{metaMapping[pageId]?.title || 'Untitled'}</span>
|
||||
</MenuLinkItem>
|
||||
{showReferences &&
|
||||
referencesToShow.map(ref => {
|
||||
return (
|
||||
<FavoriteMenuItem
|
||||
key={ref}
|
||||
workspace={workspace}
|
||||
pageId={ref}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([...parentIds, pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
|
||||
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||
|
||||
const favoriteList = useMemo(
|
||||
() => pageMeta.filter(p => p.favorite && !p.trash),
|
||||
[pageMeta]
|
||||
() => metas.filter(p => p.favorite && !p.trash),
|
||||
[metas]
|
||||
);
|
||||
|
||||
const metaMapping = useMemo(
|
||||
() =>
|
||||
metas.reduce((acc, meta) => {
|
||||
acc[meta.id] = meta;
|
||||
return acc;
|
||||
}, {} as Record<string, PageMeta>),
|
||||
[metas]
|
||||
);
|
||||
|
||||
return (
|
||||
<MuiCollapse
|
||||
in={showList}
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
overflowY: 'auto',
|
||||
marginLeft: '16px',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{favoriteList.map((pageMeta, index) => {
|
||||
const active = router.query.pageId === pageMeta.id;
|
||||
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>
|
||||
<FavoriteMenuItem
|
||||
key={`${pageMeta}-${index}`}
|
||||
metaMapping={metaMapping}
|
||||
pageId={pageMeta.id}
|
||||
// memo?
|
||||
parentIds={new Set()}
|
||||
workspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{favoriteList.length === 0 && <EmptyItem />}
|
||||
</MuiCollapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import type React from 'react';
|
||||
import { openQuickSearchModalAtom } from '../../../atoms';
|
||||
import type { HeaderProps } from '../../blocksuite/workspace-header/header';
|
||||
import { Header } from '../../blocksuite/workspace-header/header';
|
||||
import { StyledPageListTittleWrapper } from '../../blocksuite/workspace-header/styles';
|
||||
import * as styles from '../../blocksuite/workspace-header/styles.css';
|
||||
import { QuickSearchButton } from '../quick-search-button';
|
||||
|
||||
export type WorkspaceTitleProps = React.PropsWithChildren<
|
||||
@@ -22,15 +22,15 @@ export const WorkspaceTitle: React.FC<WorkspaceTitleProps> = ({
|
||||
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
||||
return (
|
||||
<Header {...props}>
|
||||
<StyledPageListTittleWrapper>
|
||||
{icon}
|
||||
<div className={styles.pageListTitleWrapper}>
|
||||
<div className={styles.pageListTitleIcon}>{icon}</div>
|
||||
{children}
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</StyledPageListTittleWrapper>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
139
apps/web/src/hooks/__tests__/use-recent-views.spec.tsx
Normal file
139
apps/web/src/hooks/__tests__/use-recent-views.spec.tsx
Normal 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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { lastWorkspaceIdAtom } from '../current/use-current-workspace';
|
||||
|
||||
export function useLastWorkspaceId() {
|
||||
return useAtomValue(lastWorkspaceIdAtom);
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user