mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 01:42:55 +08:00
Compare commits
85 Commits
v0.26.3-be
...
v0.5.4-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48a7814a6 | ||
|
|
8d34de3e9e | ||
|
|
f435377757 | ||
|
|
9a81896563 | ||
|
|
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",
|
"hooks",
|
||||||
"i18n",
|
"i18n",
|
||||||
"jotai",
|
"jotai",
|
||||||
"octobase-node",
|
"native",
|
||||||
"templates",
|
"templates",
|
||||||
"y-indexeddb",
|
"y-indexeddb",
|
||||||
"debug",
|
"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:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- 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:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- 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:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -37,23 +54,6 @@ jobs:
|
|||||||
path: ./packages/component/storybook-static
|
path: ./packages/component/storybook-static
|
||||||
if-no-files-found: error
|
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:
|
build:
|
||||||
name: Build @affine/web
|
name: Build @affine/web
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -261,7 +261,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: test-results-e2e-${{ matrix.shard }}
|
name: test-results-e2e-${{ matrix.shard }}
|
||||||
path: ./test-results
|
path: ./test-results
|
||||||
@@ -269,28 +269,63 @@ jobs:
|
|||||||
|
|
||||||
dekstop-test:
|
dekstop-test:
|
||||||
name: Desktop Test
|
name: Desktop Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.spec.os }}
|
||||||
environment: development
|
environment: development
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
|
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
|
||||||
matrix:
|
matrix:
|
||||||
spec:
|
spec:
|
||||||
- { os: macos-latest, platform: macos, arch: x64 }
|
- {
|
||||||
- { os: macos-latest, platform: macos, arch: arm64 }
|
os: macos-latest,
|
||||||
- { os: ubuntu-latest, platform: linux, arch: x64 }
|
platform: macos,
|
||||||
- { os: windows-latest, platform: windows, arch: x64 }
|
arch: x64,
|
||||||
needs: [build, build-electron]
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
- name: Download Ubuntu desktop artifact
|
- name: Build AFFiNE native
|
||||||
uses: actions/download-artifact@v3
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
name: affine-ubuntu
|
target: ${{ matrix.spec.target }}
|
||||||
path: ./apps/electron/dist
|
- 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
|
- name: Download static resource artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
@@ -299,18 +334,47 @@ jobs:
|
|||||||
path: ./apps/electron/resources/web-static
|
path: ./apps/electron/resources/web-static
|
||||||
|
|
||||||
- name: Rebuild Electron dependences
|
- name: Rebuild Electron dependences
|
||||||
run: yarn rebuild:for-electron
|
shell: bash
|
||||||
working-directory: apps/electron
|
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
|
- 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
|
working-directory: apps/electron
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: test-results-e2e-${{ matrix.shard }}
|
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
||||||
path: ./test-results
|
path: ./test-results
|
||||||
if-no-files-found: ignore
|
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:
|
env:
|
||||||
BUILD_TYPE: ${{ github.event.inputs.build-type }}
|
BUILD_TYPE: ${{ github.event.inputs.build-type }}
|
||||||
|
DEBUG: napi:*
|
||||||
|
APP_NAME: affine
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
before-make:
|
before-make:
|
||||||
@@ -46,8 +49,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
- name: generate-assets
|
- name: generate-assets
|
||||||
working-directory: apps/electron
|
run: yarn workspace @affine/electron generate-assets
|
||||||
run: yarn generate-assets
|
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
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_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
|
||||||
@@ -64,6 +66,8 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
API_SERVER_PROFILE: prod
|
API_SERVER_PROFILE: prod
|
||||||
ENABLE_TEST_PROPERTIES: false
|
ENABLE_TEST_PROPERTIES: false
|
||||||
|
ENABLE_IMAGE_PREVIEW_MODAL: false
|
||||||
|
RELEASE_VERSION: ${{ github.event.inputs.version }}
|
||||||
|
|
||||||
- name: Upload Artifact (web-static)
|
- name: Upload Artifact (web-static)
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -71,28 +75,36 @@ jobs:
|
|||||||
name: before-make-web-static
|
name: before-make-web-static
|
||||||
path: apps/electron/resources/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:
|
make-distribution:
|
||||||
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
|
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
|
||||||
strategy:
|
strategy:
|
||||||
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
|
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
|
||||||
matrix:
|
matrix:
|
||||||
spec:
|
spec:
|
||||||
- { os: macos-latest, platform: macos, arch: x64 }
|
- {
|
||||||
- { os: macos-latest, platform: macos, arch: arm64 }
|
os: macos-latest,
|
||||||
- { os: ubuntu-latest, platform: linux, arch: x64 }
|
platform: darwin,
|
||||||
- { os: windows-latest, platform: windows, arch: x64 }
|
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 }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
needs: before-make
|
needs: before-make
|
||||||
env:
|
env:
|
||||||
@@ -104,34 +116,42 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
- name: Build AFFiNE native
|
||||||
|
uses: ./.github/actions/build-rust
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.spec.target }}
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: before-make-web-static
|
name: before-make-web-static
|
||||||
path: apps/electron/resources/web-static
|
path: apps/electron/resources/web-static
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
- name: Rebuild Electron dependences
|
||||||
name: before-make-electron-dist
|
shell: bash
|
||||||
path: apps/electron/dist
|
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
|
- name: Signing By Apple Developer ID
|
||||||
if: ${{ matrix.spec.platform == 'macos' }}
|
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||||
uses: apple-actions/import-codesign-certs@v2
|
uses: apple-actions/import-codesign-certs@v2
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||||
|
|
||||||
- name: make
|
- name: make
|
||||||
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||||
working-directory: apps/electron
|
|
||||||
|
|
||||||
- name: Save artifacts (mac)
|
- name: Save artifacts (mac)
|
||||||
if: ${{ matrix.spec.platform == 'macos' }}
|
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p builds
|
mkdir -p builds
|
||||||
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
|
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
|
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)
|
- name: Save artifacts (windows)
|
||||||
if: ${{ matrix.spec.platform == 'windows' }}
|
if: ${{ matrix.spec.platform == 'win32' }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p builds
|
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/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
|
||||||
@@ -156,37 +176,36 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- name: Download Artifacts (macos-x64)
|
- name: Download Artifacts (macos-x64)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: affine-macos-x64-builds
|
name: affine-darwin-x64-builds
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifacts (macos-arm64)
|
- name: Download Artifacts (macos-arm64)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: affine-macos-arm64-builds
|
name: affine-darwin-arm64-builds
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifacts (windows-x64)
|
- name: Download Artifacts (windows-x64)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: affine-windows-x64-builds
|
name: affine-win32-x64-builds
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifacts (linux-x64)
|
- name: Download Artifacts (linux-x64)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: affine-linux-x64-builds
|
name: affine-linux-x64-builds
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifacts
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: release-yml-build-script
|
|
||||||
path: ./
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Generate Release yml
|
- name: Generate Release yml
|
||||||
run: |
|
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
|
- name: Create Release Draft
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -66,3 +66,7 @@ i18n-generated.ts
|
|||||||
# Cache
|
# Cache
|
||||||
.eslintcache
|
.eslintcache
|
||||||
next-env.d.ts
|
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]": {
|
"[toml]": {
|
||||||
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
||||||
},
|
},
|
||||||
"rust-analyzer.linkedProjects": ["packages/octobase-node/Cargo.toml"],
|
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
@@ -38,5 +37,6 @@
|
|||||||
"apps/electron/layers/**/*.spec.ts",
|
"apps/electron/layers/**/*.spec.ts",
|
||||||
"tests/unit/**/*.spec.ts",
|
"tests/unit/**/*.spec.ts",
|
||||||
"tests/unit/**/*.spec.tsx"
|
"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).
|
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">
|
<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>
|
</a>
|
||||||
|
|
||||||
## Self-Host
|
## Self-Host
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const { z } = require('zod');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
utils: { fromBuildIdentifier },
|
utils: { fromBuildIdentifier },
|
||||||
} = require('@electron-forge/core');
|
} = require('@electron-forge/core');
|
||||||
|
|
||||||
const path = require('node:path');
|
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 stableBuild = buildType === 'stable';
|
||||||
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
|
const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE';
|
||||||
const icoPath = !stableBuild
|
const icoPath = !stableBuild
|
||||||
@@ -28,6 +33,7 @@ module.exports = {
|
|||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
name: productName,
|
name: productName,
|
||||||
appBundleId: fromBuildIdentifier({
|
appBundleId: fromBuildIdentifier({
|
||||||
|
internal: 'pro.affine.internal',
|
||||||
canary: 'pro.affine.canary',
|
canary: 'pro.affine.canary',
|
||||||
beta: 'pro.affine.beta',
|
beta: 'pro.affine.beta',
|
||||||
stable: 'pro.affine.app',
|
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';
|
import type { MainEventListener } from './type';
|
||||||
|
|
||||||
|
interface DBFilePathMeta {
|
||||||
|
workspaceId: string;
|
||||||
|
path: string;
|
||||||
|
realPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const dbSubjects = {
|
export const dbSubjects = {
|
||||||
// emit workspace ids
|
// emit workspace ids
|
||||||
dbFileMissing: new Subject<string>(),
|
dbFileMissing: new Subject<string>(),
|
||||||
// emit workspace ids
|
// emit workspace ids
|
||||||
dbFileUpdate: new Subject<string>(),
|
dbFileUpdate: new Subject<string>(),
|
||||||
|
dbFilePathChange: new Subject<DBFilePathMeta>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dbEvents = {
|
export const dbEvents = {
|
||||||
onDbFileMissing: (fn: (workspaceId: string) => void) => {
|
onDBFileMissing: (fn: (workspaceId: string) => void) => {
|
||||||
const sub = dbSubjects.dbFileMissing.subscribe(fn);
|
const sub = dbSubjects.dbFileMissing.subscribe(fn);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onDbFileUpdate: (fn: (workspaceId: string) => void) => {
|
onDBFileUpdate: (fn: (workspaceId: string) => void) => {
|
||||||
const sub = dbSubjects.dbFileUpdate.subscribe(fn);
|
const sub = dbSubjects.dbFileUpdate.subscribe(fn);
|
||||||
return () => {
|
return () => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
onDBFilePathChange: (fn: (meta: DBFilePathMeta) => void) => {
|
||||||
|
const sub = dbSubjects.dbFilePathChange.subscribe(fn);
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
};
|
||||||
|
},
|
||||||
} satisfies Record<string, MainEventListener>;
|
} satisfies Record<string, MainEventListener>;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export * from './register';
|
export * from './register';
|
||||||
|
|
||||||
|
import { applicationMenuSubjects } from './application-menu';
|
||||||
import { dbSubjects } from './db';
|
import { dbSubjects } from './db';
|
||||||
|
|
||||||
export const subjects = {
|
export const subjects = {
|
||||||
db: dbSubjects,
|
db: dbSubjects,
|
||||||
|
applicationMenu: applicationMenuSubjects,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { applicationMenuEvents } from './application-menu';
|
||||||
import { dbEvents } from './db';
|
import { dbEvents } from './db';
|
||||||
import { updaterEvents } from './updater';
|
import { updaterEvents } from './updater';
|
||||||
|
|
||||||
export const allEvents = {
|
export const allEvents = {
|
||||||
db: dbEvents,
|
db: dbEvents,
|
||||||
updater: updaterEvents,
|
updater: updaterEvents,
|
||||||
|
applicationMenu: applicationMenuEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getActiveWindows() {
|
function getActiveWindows() {
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
import { Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
|
|
||||||
import type { MainEventListener } from './type';
|
import type { MainEventListener } from './type';
|
||||||
|
|
||||||
interface UpdateMeta {
|
interface UpdateMeta {
|
||||||
version: string;
|
version: string;
|
||||||
|
allowAutoUpdate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updaterSubjects = {
|
export const updaterSubjects = {
|
||||||
// means it is ready for restart and install the new version
|
// 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 = {
|
export const updaterEvents = {
|
||||||
onClientUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
|
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||||
const sub = updaterSubjects.clientUpdateReady.subscribe(fn);
|
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 () => {
|
return () => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import assert from 'node:assert';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import type { Subscription } from 'rxjs';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
@@ -61,6 +63,9 @@ const ipcMain = {
|
|||||||
handlers.push(callback);
|
handlers.push(callback);
|
||||||
registeredHandlers.set(key, handlers);
|
registeredHandlers.set(key, handlers);
|
||||||
},
|
},
|
||||||
|
setMaxListeners: (_n: number) => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const nativeTheme = {
|
const nativeTheme = {
|
||||||
@@ -96,6 +101,11 @@ const electronModule = {
|
|||||||
handlers.push(callback);
|
handlers.push(callback);
|
||||||
registeredHandlers.set(name, handlers);
|
registeredHandlers.set(name, handlers);
|
||||||
},
|
},
|
||||||
|
addEventListener: (...args: any[]) => {
|
||||||
|
// @ts-ignore
|
||||||
|
electronModule.app.on(...args);
|
||||||
|
},
|
||||||
|
removeEventListener: () => {},
|
||||||
},
|
},
|
||||||
BrowserWindow: {
|
BrowserWindow: {
|
||||||
getAllWindows: () => {
|
getAllWindows: () => {
|
||||||
@@ -113,6 +123,8 @@ vi.doMock('electron', () => {
|
|||||||
return electronModule;
|
return electronModule;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let connectableSubscription: Subscription;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { registerHandlers } = await import('../register');
|
const { registerHandlers } = await import('../register');
|
||||||
registerHandlers();
|
registerHandlers();
|
||||||
@@ -120,20 +132,24 @@ beforeEach(async () => {
|
|||||||
// should also register events
|
// should also register events
|
||||||
const { registerEvents } = await import('../../events');
|
const { registerEvents } = await import('../../events');
|
||||||
registerEvents();
|
registerEvents();
|
||||||
|
await fs.mkdirp(SESSION_DATA_PATH);
|
||||||
|
const { database$ } = await import('../db/ensure-db');
|
||||||
|
|
||||||
|
connectableSubscription = database$.connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const { cleanupSQLiteDBs } = await import('../db/ensure-db');
|
|
||||||
await cleanupSQLiteDBs();
|
|
||||||
await fs.remove(SESSION_DATA_PATH);
|
|
||||||
|
|
||||||
// reset registered handlers
|
// reset registered handlers
|
||||||
registeredHandlers.get('before-quit')?.forEach(fn => fn());
|
registeredHandlers.get('before-quit')?.forEach(fn => fn());
|
||||||
|
|
||||||
|
connectableSubscription.unsubscribe();
|
||||||
|
|
||||||
|
await fs.remove(SESSION_DATA_PATH);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ensureSQLiteDB', () => {
|
describe('ensureSQLiteDB', () => {
|
||||||
test('should create db file on connection if it does not exist', async () => {
|
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 { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
const workspaceDB = await ensureSQLiteDB(id);
|
const workspaceDB = await ensureSQLiteDB(id);
|
||||||
const file = workspaceDB.path;
|
const file = workspaceDB.path;
|
||||||
@@ -143,73 +159,76 @@ describe('ensureSQLiteDB', () => {
|
|||||||
|
|
||||||
test('when db file is removed', async () => {
|
test('when db file is removed', async () => {
|
||||||
// stub webContents.send
|
// stub webContents.send
|
||||||
const sendStub = vi.fn();
|
const sendSpy = vi.spyOn(browserWindow.webContents, 'send');
|
||||||
browserWindow.webContents.send = sendStub;
|
const id = v4();
|
||||||
const id = 'test-workspace-id';
|
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
let workspaceDB = await ensureSQLiteDB(id);
|
let workspaceDB = await ensureSQLiteDB(id);
|
||||||
const file = workspaceDB.path;
|
const file = workspaceDB.path;
|
||||||
const fileExists = await fs.pathExists(file);
|
const fileExists = await fs.pathExists(file);
|
||||||
expect(fileExists).toBe(true);
|
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);
|
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);
|
await delay(2000);
|
||||||
|
|
||||||
expect(sendStub).toBeCalledWith('db:onDbFileMissing', id);
|
expect(sendSpy).toBeCalledWith('db:onDBFileMissing', id);
|
||||||
|
|
||||||
// ensureSQLiteDB should recreate the db file
|
// ensureSQLiteDB should recreate the db file
|
||||||
workspaceDB = await ensureSQLiteDB(id);
|
workspaceDB = await ensureSQLiteDB(id);
|
||||||
const fileExists2 = await fs.pathExists(file);
|
const fileExists2 = await fs.pathExists(file);
|
||||||
expect(fileExists2).toBe(true);
|
expect(fileExists2).toBe(true);
|
||||||
|
sendSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('when db file is updated', async () => {
|
test('when db file is updated', async () => {
|
||||||
// stub webContents.send
|
const id = v4();
|
||||||
const sendStub = vi.fn();
|
|
||||||
browserWindow.webContents.send = sendStub;
|
|
||||||
const id = 'test-workspace-id';
|
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
|
const { dbSubjects } = await import('../../events/db');
|
||||||
const workspaceDB = await ensureSQLiteDB(id);
|
const workspaceDB = await ensureSQLiteDB(id);
|
||||||
const file = workspaceDB.path;
|
const file = workspaceDB.path;
|
||||||
const fileExists = await fs.pathExists(file);
|
const fileExists = await fs.pathExists(file);
|
||||||
expect(fileExists).toBe(true);
|
expect(fileExists).toBe(true);
|
||||||
|
const dbUpdateSpy = vi.spyOn(dbSubjects.dbFileUpdate, 'next');
|
||||||
// wait to make sure
|
await delay(100);
|
||||||
await delay(500);
|
|
||||||
|
|
||||||
// writes some data to the db file
|
// writes some data to the db file
|
||||||
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
|
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
|
||||||
// write again
|
// write again
|
||||||
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
|
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);
|
await delay(2000);
|
||||||
|
|
||||||
expect(sendStub).toBeCalledWith('db:onDbFileUpdate', id);
|
expect(dbUpdateSpy).toBeCalledWith(id);
|
||||||
|
dbUpdateSpy.mockRestore();
|
||||||
// should only call once for multiple writes
|
|
||||||
expect(sendStub).toBeCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('workspace handlers', () => {
|
describe('workspace handlers', () => {
|
||||||
test('list all workspace ids', async () => {
|
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');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
|
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
|
||||||
const list = await dispatch('workspace', 'list');
|
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 () => {
|
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');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
|
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');
|
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', () => {
|
describe('db handlers', () => {
|
||||||
test('apply doc and get doc updates', async () => {
|
test('apply doc and get doc updates', async () => {
|
||||||
const workspaceId = 'test-workspace-id';
|
const workspaceId = v4();
|
||||||
const bin = await dispatch('db', 'getDocAsUpdates', workspaceId);
|
const bin = await dispatch('db', 'getDocAsUpdates', workspaceId);
|
||||||
// ? is this a good test?
|
// ? is this a good test?
|
||||||
expect(bin.every((byte: number) => byte === 0)).toBe(true);
|
expect(bin.every((byte: number) => byte === 0)).toBe(true);
|
||||||
@@ -264,13 +283,13 @@ describe('db handlers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('get non existent blob', async () => {
|
test('get non existent blob', async () => {
|
||||||
const workspaceId = 'test-workspace-id';
|
const workspaceId = v4();
|
||||||
const bin = await dispatch('db', 'getBlob', workspaceId, 'non-existent-id');
|
const bin = await dispatch('db', 'getBlob', workspaceId, 'non-existent-id');
|
||||||
expect(bin).toBeNull();
|
expect(bin).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('list blobs (empty)', async () => {
|
test('list blobs (empty)', async () => {
|
||||||
const workspaceId = 'test-workspace-id';
|
const workspaceId = v4();
|
||||||
const list = await dispatch('db', 'getPersistedBlobs', workspaceId);
|
const list = await dispatch('db', 'getPersistedBlobs', workspaceId);
|
||||||
expect(list).toEqual([]);
|
expect(list).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -318,7 +337,7 @@ describe('dialog handlers', () => {
|
|||||||
const mockShowItemInFolder = vi.fn();
|
const mockShowItemInFolder = vi.fn();
|
||||||
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
||||||
|
|
||||||
const id = 'test-workspace-id';
|
const id = v4();
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
const db = await ensureSQLiteDB(id);
|
const db = await ensureSQLiteDB(id);
|
||||||
|
|
||||||
@@ -334,13 +353,15 @@ describe('dialog handlers', () => {
|
|||||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||||
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
||||||
|
|
||||||
const id = 'test-workspace-id';
|
const id = v4();
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
await ensureSQLiteDB(id);
|
await ensureSQLiteDB(id);
|
||||||
|
|
||||||
await dispatch('dialog', 'saveDBFileAs', id);
|
await dispatch('dialog', 'saveDBFileAs', id);
|
||||||
expect(mockShowSaveDialog).toBeCalled();
|
expect(mockShowSaveDialog).toBeCalled();
|
||||||
expect(mockShowItemInFolder).not.toBeCalled();
|
expect(mockShowItemInFolder).not.toBeCalled();
|
||||||
|
electronModule.dialog = {};
|
||||||
|
electronModule.shell = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('saveDBFileAs', async () => {
|
test('saveDBFileAs', async () => {
|
||||||
@@ -352,7 +373,7 @@ describe('dialog handlers', () => {
|
|||||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||||
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
||||||
|
|
||||||
const id = 'test-workspace-id';
|
const id = v4();
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
await ensureSQLiteDB(id);
|
await ensureSQLiteDB(id);
|
||||||
|
|
||||||
@@ -403,11 +424,13 @@ describe('dialog handlers', () => {
|
|||||||
const res = await dispatch('dialog', 'loadDBFile');
|
const res = await dispatch('dialog', 'loadDBFile');
|
||||||
expect(mockShowOpenDialog).toBeCalled();
|
expect(mockShowOpenDialog).toBeCalled();
|
||||||
expect(res.error).toBe('DB_FILE_INVALID');
|
expect(res.error).toBe('DB_FILE_INVALID');
|
||||||
|
|
||||||
|
electronModule.dialog = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loadDBFile', async () => {
|
test('loadDBFile', async () => {
|
||||||
// we use ensureSQLiteDB to create a valid db file
|
// 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 { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
const db = await ensureSQLiteDB(id);
|
const db = await ensureSQLiteDB(id);
|
||||||
|
|
||||||
@@ -417,6 +440,11 @@ describe('dialog handlers', () => {
|
|||||||
await fs.ensureDir(basePath);
|
await fs.ensureDir(basePath);
|
||||||
await fs.copyFile(db.path, originDBFilePath);
|
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
|
// remove db
|
||||||
await fs.remove(db.path);
|
await fs.remove(db.path);
|
||||||
|
|
||||||
@@ -440,19 +468,19 @@ describe('dialog handlers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('moveDBFile', async () => {
|
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(() => {
|
const mockShowSaveDialog = vi.fn(() => {
|
||||||
return { filePath: newPath };
|
return { filePath: newPath };
|
||||||
}) as any;
|
}) as any;
|
||||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||||
|
|
||||||
const id = 'test-workspace-id';
|
const id = v4();
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
await ensureSQLiteDB(id);
|
await ensureSQLiteDB(id);
|
||||||
|
|
||||||
const res = await dispatch('dialog', 'moveDBFile', id);
|
const res = await dispatch('dialog', 'moveDBFile', id);
|
||||||
expect(mockShowSaveDialog).toBeCalled();
|
expect(mockShowSaveDialog).toBeCalled();
|
||||||
expect(res.filePath).toBe(newPath);
|
expect(res.filePath).toBe(newPath);
|
||||||
|
electronModule.dialog = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('moveDBFile (skipped)', async () => {
|
test('moveDBFile (skipped)', async () => {
|
||||||
@@ -461,12 +489,13 @@ describe('dialog handlers', () => {
|
|||||||
}) as any;
|
}) as any;
|
||||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||||
|
|
||||||
const id = 'test-workspace-id';
|
const id = v4();
|
||||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||||
await ensureSQLiteDB(id);
|
await ensureSQLiteDB(id);
|
||||||
|
|
||||||
const res = await dispatch('dialog', 'moveDBFile', id);
|
const res = await dispatch('dialog', 'moveDBFile', id);
|
||||||
expect(mockShowSaveDialog).toBeCalled();
|
expect(mockShowSaveDialog).toBeCalled();
|
||||||
expect(res.filePath).toBe(undefined);
|
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 { appContext } from '../../context';
|
||||||
import { subjects } from '../../events';
|
import { subjects } from '../../events';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
import { debounce, ts } from '../../utils';
|
import { ts } from '../../utils';
|
||||||
import type { WorkspaceSQLiteDB } from './sqlite';
|
import type { WorkspaceSQLiteDB } from './sqlite';
|
||||||
import { openWorkspaceDatabase } from './sqlite';
|
import { openWorkspaceDatabase } from './sqlite';
|
||||||
|
|
||||||
const dbMapping = new Map<string, Promise<WorkspaceSQLiteDB>>();
|
const databaseInput$ = new Subject<string>();
|
||||||
const dbWatchers = new Map<string, () => void>();
|
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
|
// if we removed the file, we will stop watching it
|
||||||
function startWatchingDBFile(db: WorkspaceSQLiteDB) {
|
function startWatchingDBFile(db: WorkspaceSQLiteDB) {
|
||||||
if (dbWatchers.has(db.workspaceId)) {
|
const FSWatcher = createFSWatcher();
|
||||||
return dbWatchers.get(db.workspaceId);
|
return new Observable<NotifyEvent>(subscriber => {
|
||||||
}
|
logger.info('[FSWatcher] start watching db file', db.workspaceId);
|
||||||
logger.info('watch db file', db.path);
|
const subscription = FSWatcher.watch(db.path, {
|
||||||
const watcher = watch(db.path);
|
recursive: false,
|
||||||
|
}).subscribe(
|
||||||
const debounceOnChange = debounce(() => {
|
event => {
|
||||||
logger.info(
|
logger.info('[FSWatcher]', event);
|
||||||
'db file changed on disk',
|
subscriber.next(event);
|
||||||
db.workspaceId,
|
// remove file or move file, complete the observable and close db
|
||||||
ts() - db.lastUpdateTime,
|
if (isRemoveOrMoveEvent(event)) {
|
||||||
'ms'
|
subscriber.complete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
subscriber.error(err);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// reconnect db
|
return () => {
|
||||||
db.reconnectDB();
|
// destroy on unsubscribe
|
||||||
subjects.db.dbFileUpdate.next(db.workspaceId);
|
logger.info('[FSWatcher] cleanup db file watcher', 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(() => {
|
|
||||||
db.destroy();
|
db.destroy();
|
||||||
dbWatchers.delete(db.workspaceId);
|
subscription.unsubscribe();
|
||||||
dbMapping.delete(db.workspaceId);
|
};
|
||||||
});
|
}).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) {
|
export function ensureSQLiteDB(id: string) {
|
||||||
let workspaceDB = dbMapping.get(id);
|
const deferValue = lastValueFrom(
|
||||||
if (!workspaceDB) {
|
database$.pipe(
|
||||||
logger.info('[ensureSQLiteDB] open db connection', id);
|
filter(db => db.workspaceId === id && db.db.open),
|
||||||
workspaceDB = openWorkspaceDatabase(appContext, id);
|
take(1),
|
||||||
dbMapping.set(id, workspaceDB);
|
tap({
|
||||||
startWatchingDBFile(await workspaceDB);
|
error: err => {
|
||||||
}
|
logger.error('[ensureSQLiteDB] error', err);
|
||||||
return await workspaceDB;
|
},
|
||||||
}
|
})
|
||||||
|
)
|
||||||
export async function disconnectSQLiteDB(id: string) {
|
);
|
||||||
const dbp = dbMapping.get(id);
|
databaseInput$.next(id);
|
||||||
if (dbp) {
|
return deferValue;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
import { appContext } from '../../context';
|
import { appContext } from '../../context';
|
||||||
import type { NamespaceHandlers } from '../type';
|
import type { NamespaceHandlers } from '../type';
|
||||||
import { ensureSQLiteDB } from './ensure-db';
|
import { ensureSQLiteDB } from './ensure-db';
|
||||||
@@ -30,4 +32,11 @@ export const dbHandlers = {
|
|||||||
getDefaultStorageLocation: async () => {
|
getDefaultStorageLocation: async () => {
|
||||||
return appContext.appDataPath;
|
return appContext.appDataPath;
|
||||||
},
|
},
|
||||||
|
getDBFilePath: async (_, workspaceId: string) => {
|
||||||
|
const workspaceDB = await ensureSQLiteDB(workspaceId);
|
||||||
|
return {
|
||||||
|
path: workspaceDB.path,
|
||||||
|
realPath: await fs.realpath(workspaceDB.path),
|
||||||
|
};
|
||||||
|
},
|
||||||
} satisfies NamespaceHandlers;
|
} satisfies NamespaceHandlers;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import fs from 'fs-extra';
|
|||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import type { AppContext } from '../../context';
|
import type { AppContext } from '../../context';
|
||||||
|
import { dbSubjects } from '../../events/db';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
import { ts } from '../../utils';
|
import { ts } from '../../utils';
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export class WorkspaceSQLiteDB {
|
|||||||
ydoc = new Y.Doc();
|
ydoc = new Y.Doc();
|
||||||
firstConnect = false;
|
firstConnect = false;
|
||||||
lastUpdateTime = ts();
|
lastUpdateTime = ts();
|
||||||
|
destroyed = false;
|
||||||
|
|
||||||
constructor(public path: string, public workspaceId: string) {
|
constructor(public path: string, public workspaceId: string) {
|
||||||
this.db = this.reconnectDB();
|
this.db = this.reconnectDB();
|
||||||
@@ -57,11 +59,23 @@ export class WorkspaceSQLiteDB {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reconnectDB = () => {
|
reconnectDB = () => {
|
||||||
logger.log('open db', this.workspaceId);
|
logger.log('[WorkspaceSQLiteDB] open db', this.workspaceId);
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
this.db.close();
|
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?
|
// use cached version?
|
||||||
const db = (this.db = sqlite(this.path));
|
const db = (this.db = sqlite(this.path));
|
||||||
db.exec(schemas.join(';'));
|
db.exec(schemas.join(';'));
|
||||||
@@ -211,8 +225,9 @@ export async function openWorkspaceDatabase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isValidDBFile(path: string) {
|
export function isValidDBFile(path: string) {
|
||||||
|
let db: Database | null = null;
|
||||||
try {
|
try {
|
||||||
const db = sqlite(path);
|
db = sqlite(path);
|
||||||
// check if db has two tables, one for updates and onefor blobs
|
// check if db has two tables, one for updates and onefor blobs
|
||||||
const statement = db.prepare(
|
const statement = db.prepare(
|
||||||
`SELECT name FROM sqlite_schema WHERE type='table'`
|
`SELECT name FROM sqlite_schema WHERE type='table'`
|
||||||
@@ -226,6 +241,7 @@ export function isValidDBFile(path: string) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('isValidDBFile', error);
|
logger.error('isValidDBFile', error);
|
||||||
|
db?.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { nanoid } from 'nanoid';
|
|||||||
|
|
||||||
import { appContext } from '../../context';
|
import { appContext } from '../../context';
|
||||||
import { logger } from '../../logger';
|
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 { getWorkspaceDBPath, isValidDBFile } from '../db/sqlite';
|
||||||
import { listWorkspaces } from '../workspace/workspace';
|
import { listWorkspaces } from '../workspace/workspace';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ import { listWorkspaces } from '../workspace/workspace';
|
|||||||
|
|
||||||
export async function revealDBFile(workspaceId: string) {
|
export async function revealDBFile(workspaceId: string) {
|
||||||
const workspaceDB = await ensureSQLiteDB(workspaceId);
|
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
|
// provide a backdoor to set dialog path for testing in playwright
|
||||||
@@ -47,6 +48,7 @@ const ErrorMessages = [
|
|||||||
'DB_FILE_ALREADY_LOADED',
|
'DB_FILE_ALREADY_LOADED',
|
||||||
'DB_FILE_PATH_INVALID',
|
'DB_FILE_PATH_INVALID',
|
||||||
'DB_FILE_INVALID',
|
'DB_FILE_INVALID',
|
||||||
|
'FILE_ALREADY_EXISTS',
|
||||||
'UNKNOWN_ERROR',
|
'UNKNOWN_ERROR',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -201,7 +203,7 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
|||||||
|
|
||||||
await fs.ensureDir(path.join(appContext.appDataPath, 'workspaces'));
|
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}`);
|
logger.info(`loadDBFile, symlink: ${filePath} -> ${linkedFilePath}`);
|
||||||
|
|
||||||
return { workspaceId };
|
return { workspaceId };
|
||||||
@@ -231,17 +233,29 @@ export async function moveDBFile(
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
dbFileLocation?: string
|
dbFileLocation?: string
|
||||||
): Promise<MoveDBFileResult> {
|
): Promise<MoveDBFileResult> {
|
||||||
|
let db: WorkspaceSQLiteDB | null = null;
|
||||||
try {
|
try {
|
||||||
const db = await ensureSQLiteDB(workspaceId);
|
const { moveFile, FsWatcher } = await import('@affine/native');
|
||||||
|
db = await ensureSQLiteDB(workspaceId);
|
||||||
// get the real file path of db
|
// get the real file path of db
|
||||||
const realpath = await fs.realpath(db.path);
|
const realpath = await fs.realpath(db.path);
|
||||||
const isLink = 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 =
|
const newFilePath =
|
||||||
dbFileLocation ||
|
dbFileLocation ??
|
||||||
(
|
(
|
||||||
getFakedResult() ||
|
getFakedResult() ??
|
||||||
(await dialog.showSaveDialog({
|
(await dialog.showSaveDialog({
|
||||||
properties: ['showOverwriteConfirmation'],
|
properties: ['showOverwriteConfirmation'],
|
||||||
title: 'Move Workspace Storage',
|
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) {
|
if (isLink) {
|
||||||
// remove the old link to unblock new link
|
// remove the old link to unblock new link
|
||||||
await fs.unlink(db.path);
|
await fs.unlink(db.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.move(realpath, newFilePath, {
|
logger.info(`[moveDBFile] move ${realpath} -> ${newFilePath}`);
|
||||||
overwrite: true,
|
|
||||||
});
|
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 {
|
return {
|
||||||
filePath: newFilePath,
|
filePath: newFilePath,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('moveDBFile', err);
|
db?.destroy();
|
||||||
|
logger.error('[moveDBFile]', err);
|
||||||
return {
|
return {
|
||||||
error: 'UNKNOWN_ERROR',
|
error: 'UNKNOWN_ERROR',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const dialogHandlers = {
|
|||||||
saveDBFileAs: async (_, workspaceId: string) => {
|
saveDBFileAs: async (_, workspaceId: string) => {
|
||||||
return saveDBFileAs(workspaceId);
|
return saveDBFileAs(workspaceId);
|
||||||
},
|
},
|
||||||
moveDBFile: async (_, workspaceId: string, dbFileLocation?: string) => {
|
moveDBFile: (_, workspaceId: string, dbFileLocation?: string) => {
|
||||||
return moveDBFile(workspaceId, dbFileLocation);
|
return moveDBFile(workspaceId, dbFileLocation);
|
||||||
},
|
},
|
||||||
selectDBFileLocation: async () => {
|
selectDBFileLocation: async () => {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export const allHandlers = {
|
|||||||
} satisfies Record<string, NamespaceHandlers>;
|
} satisfies Record<string, NamespaceHandlers>;
|
||||||
|
|
||||||
export const registerHandlers = () => {
|
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 [namespace, namespaceHandlers] of Object.entries(allHandlers)) {
|
||||||
for (const [key, handler] of Object.entries(namespaceHandlers)) {
|
for (const [key, handler] of Object.entries(namespaceHandlers)) {
|
||||||
const chan = `${namespace}:${key}`;
|
const chan = `${namespace}:${key}`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, nativeTheme } from 'electron';
|
import { app, BrowserWindow, nativeTheme } from 'electron';
|
||||||
|
|
||||||
import { isMacOS } from '../../../../utils';
|
import { isMacOS } from '../../../../utils';
|
||||||
import type { NamespaceHandlers } from '../type';
|
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 () => {
|
getGoogleOauthCode: async () => {
|
||||||
return getGoogleOauthCode();
|
return getGoogleOauthCode();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
import type { NamespaceHandlers } from '../type';
|
import type { NamespaceHandlers } from '../type';
|
||||||
import { updateClient } from './updater';
|
import { checkForUpdatesAndNotify, quitAndInstall } from './updater';
|
||||||
|
|
||||||
export const updaterHandlers = {
|
export const updaterHandlers = {
|
||||||
updateClient: async () => {
|
currentVersion: async () => {
|
||||||
return updateClient();
|
return app.getVersion();
|
||||||
|
},
|
||||||
|
quitAndInstall: async () => {
|
||||||
|
return quitAndInstall();
|
||||||
|
},
|
||||||
|
checkForUpdatesAndNotify: async () => {
|
||||||
|
return checkForUpdatesAndNotify(true);
|
||||||
},
|
},
|
||||||
} satisfies NamespaceHandlers;
|
} satisfies NamespaceHandlers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,100 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
import type { AppUpdater } from 'electron-updater';
|
import type { AppUpdater } from 'electron-updater';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { isMacOS } from '../../../../utils';
|
import { isMacOS } from '../../../../utils';
|
||||||
import { updaterSubjects } from '../../events/updater';
|
import { updaterSubjects } from '../../events/updater';
|
||||||
import { logger } from '../../logger';
|
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 mode = process.env.NODE_ENV;
|
||||||
const isDev = mode === 'development';
|
const isDev = mode === 'development';
|
||||||
|
|
||||||
let _autoUpdater: AppUpdater | null = null;
|
let _autoUpdater: AppUpdater | null = null;
|
||||||
|
|
||||||
export const updateClient = async () => {
|
export const quitAndInstall = async () => {
|
||||||
_autoUpdater?.quitAndInstall();
|
_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 () => {
|
export const registerUpdater = async () => {
|
||||||
// require it will cause some side effects and will break generate-main-exposed-meta,
|
// require it will cause some side effects and will break generate-main-exposed-meta,
|
||||||
// so we wrap it in a function
|
// so we wrap it in a function
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { autoUpdater } = await import('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
|
||||||
_autoUpdater = autoUpdater;
|
_autoUpdater = autoUpdater;
|
||||||
|
|
||||||
autoUpdater.autoDownload = false;
|
if (!_autoUpdater) {
|
||||||
autoUpdater.allowPrerelease = buildType !== 'stable';
|
return;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 { app } from 'electron';
|
||||||
|
|
||||||
|
import { createApplicationMenu } from './application-menu';
|
||||||
import { registerEvents } from './events';
|
import { registerEvents } from './events';
|
||||||
import { registerHandlers } from './handlers';
|
import { registerHandlers } from './handlers';
|
||||||
import { registerUpdater } from './handlers/updater';
|
import { registerUpdater } from './handlers/updater';
|
||||||
@@ -57,6 +58,7 @@ app
|
|||||||
.then(registerHandlers)
|
.then(registerHandlers)
|
||||||
.then(registerEvents)
|
.then(registerEvents)
|
||||||
.then(restoreOrCreateWindow)
|
.then(restoreOrCreateWindow)
|
||||||
|
.then(createApplicationMenu)
|
||||||
.then(registerUpdater)
|
.then(registerUpdater)
|
||||||
.catch(e => console.error('Failed create window:', e));
|
.catch(e => console.error('Failed create window:', e));
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
|||||||
import electronWindowState from 'electron-window-state';
|
import electronWindowState from 'electron-window-state';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { isMacOS } from '../../utils';
|
import { isMacOS, isWindows } from '../../utils';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
const IS_DEV: boolean =
|
const IS_DEV: boolean =
|
||||||
@@ -18,13 +18,17 @@ async function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const browserWindow = new BrowserWindow({
|
const browserWindow = new BrowserWindow({
|
||||||
titleBarStyle: isMacOS() ? 'hiddenInset' : 'default',
|
titleBarStyle: isMacOS()
|
||||||
|
? 'hiddenInset'
|
||||||
|
: isWindows()
|
||||||
|
? 'hidden'
|
||||||
|
: 'default',
|
||||||
trafficLightPosition: { x: 24, y: 18 },
|
trafficLightPosition: { x: 24, y: 18 },
|
||||||
x: mainWindowState.x,
|
x: mainWindowState.x,
|
||||||
y: mainWindowState.y,
|
y: mainWindowState.y,
|
||||||
width: mainWindowState.width,
|
width: mainWindowState.width,
|
||||||
minWidth: 640,
|
minWidth: 640,
|
||||||
transparent: isMacOS(),
|
minHeight: 480,
|
||||||
visualEffectState: 'active',
|
visualEffectState: 'active',
|
||||||
vibrancy: 'under-window',
|
vibrancy: 'under-window',
|
||||||
height: mainWindowState.height,
|
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 */
|
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
apis?: typeof import('./src/affine-apis').apis;
|
apis: typeof import('./src/affine-apis').apis;
|
||||||
events?: typeof import('./src/affine-apis').events;
|
events: typeof import('./src/affine-apis').events;
|
||||||
appInfo?: typeof import('./src/affine-apis').appInfo;
|
appInfo: typeof import('./src/affine-apis').appInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ const events: MainIPCEventMap = (() => {
|
|||||||
const {
|
const {
|
||||||
events: eventsMeta,
|
events: eventsMeta,
|
||||||
}: MainExposedMeta = require('../main/exposed-meta');
|
}: 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 all = eventsMeta.map(([namespace, eventNames]) => {
|
||||||
const namespaceEvents = eventNames.map(name => {
|
const namespaceEvents = eventNames.map(name => {
|
||||||
const channel = `${namespace}:${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 = () => {
|
export const isMacOS = () => {
|
||||||
return process.platform === 'darwin';
|
return process.platform === 'darwin';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isWindows = () => {
|
||||||
|
return process.platform === 'win32';
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@affine/electron",
|
"name": "@affine/electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.4-canary.30",
|
"version": "0.5.4-beta.2",
|
||||||
"author": "affine",
|
"author": "affine",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/toeverything/AFFiNE",
|
"url": "https://github.com/toeverything/AFFiNE",
|
||||||
@@ -18,10 +18,6 @@
|
|||||||
"generate-main-exposed-meta": "zx scripts/generate-main-exposed-meta.mjs",
|
"generate-main-exposed-meta": "zx scripts/generate-main-exposed-meta.mjs",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"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-unit-test": "yarn rebuild better-sqlite3",
|
||||||
"rebuild:for-electron": "yarn electron-rebuild",
|
"rebuild:for-electron": "yarn electron-rebuild",
|
||||||
"test": "playwright test"
|
"test": "playwright test"
|
||||||
@@ -32,6 +28,7 @@
|
|||||||
"main": "./dist/layers/main/index.js",
|
"main": "./dist/layers/main/index.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@affine-test/kit": "workspace:*",
|
"@affine-test/kit": "workspace:*",
|
||||||
|
"@affine/native": "workspace:*",
|
||||||
"@electron-forge/cli": "^6.1.1",
|
"@electron-forge/cli": "^6.1.1",
|
||||||
"@electron-forge/core": "^6.1.1",
|
"@electron-forge/core": "^6.1.1",
|
||||||
"@electron-forge/core-utils": "^6.1.1",
|
"@electron-forge/core-utils": "^6.1.1",
|
||||||
@@ -44,16 +41,18 @@
|
|||||||
"@electron/remote": "2.0.9",
|
"@electron/remote": "2.0.9",
|
||||||
"@types/better-sqlite3": "^7.6.4",
|
"@types/better-sqlite3": "^7.6.4",
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"electron": "24.2.0",
|
"electron": "24.3.1",
|
||||||
"electron-log": "^5.0.0-beta.23",
|
"electron-log": "^5.0.0-beta.24",
|
||||||
"electron-squirrel-startup": "1.0.0",
|
"electron-squirrel-startup": "1.0.0",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"esbuild": "^0.17.18",
|
"esbuild": "^0.17.19",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"playwright": "^1.33.0",
|
"playwright": "^1.33.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"undici": "^5.22.0",
|
"undici": "^5.22.1",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"zx": "^7.2.2"
|
"zx": "^7.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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 =
|
const NODE_ENV =
|
||||||
process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
$.shell = true;
|
||||||
|
$.prefix = '';
|
||||||
|
}
|
||||||
|
|
||||||
async function buildLayers() {
|
async function buildLayers() {
|
||||||
const common = config();
|
const common = config();
|
||||||
await esbuild.build(common.preload);
|
await esbuild.build(common.preload);
|
||||||
|
|||||||
@@ -12,16 +12,6 @@ const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
|
|||||||
/** @type 'production' | 'development'' */
|
/** @type 'production' | 'development'' */
|
||||||
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || '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
|
// List of env that will be replaced by esbuild
|
||||||
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
|
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
|
||||||
|
|
||||||
@@ -50,9 +40,13 @@ export const config = () => {
|
|||||||
target: `node${NODE_MAJOR_VERSION}`,
|
target: `node${NODE_MAJOR_VERSION}`,
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
external: ['electron', 'yjs', 'better-sqlite3', 'electron-updater'],
|
external: ['electron', 'yjs', 'better-sqlite3', 'electron-updater'],
|
||||||
plugins: [nativeNodeModulesPlugin],
|
|
||||||
define: define,
|
define: define,
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
|
loader: {
|
||||||
|
'.node': 'copy',
|
||||||
|
},
|
||||||
|
assetNames: '[name]',
|
||||||
|
treeShaking: true,
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
entryPoints: [resolve(root, './layers/preload/src/index.ts')],
|
entryPoints: [resolve(root, './layers/preload/src/index.ts')],
|
||||||
@@ -61,7 +55,6 @@ export const config = () => {
|
|||||||
target: `node${NODE_MAJOR_VERSION}`,
|
target: `node${NODE_MAJOR_VERSION}`,
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
external: ['electron', '../main/exposed-meta'],
|
external: ['electron', '../main/exposed-meta'],
|
||||||
plugins: [nativeNodeModulesPlugin],
|
|
||||||
define: define,
|
define: define,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
#!/usr/bin/env zx
|
#!/usr/bin/env zx
|
||||||
import 'zx/globals';
|
import 'zx/globals';
|
||||||
|
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
const repoRootDir = path.join(__dirname, '..', '..', '..');
|
const repoRootDir = path.join(__dirname, '..', '..', '..');
|
||||||
const electronRootDir = path.join(__dirname, '..');
|
const electronRootDir = path.join(__dirname, '..');
|
||||||
const publicDistDir = path.join(electronRootDir, 'resources');
|
const publicDistDir = path.join(electronRootDir, 'resources');
|
||||||
const affineWebDir = path.join(repoRootDir, 'apps', 'web');
|
const affineWebDir = path.join(repoRootDir, 'apps', 'web');
|
||||||
const affineWebOutDir = path.join(affineWebDir, 'out');
|
const affineWebOutDir = path.join(affineWebDir, 'out');
|
||||||
const publicAffineOutDir = path.join(publicDistDir, `web-static`);
|
const publicAffineOutDir = path.join(publicDistDir, `web-static`);
|
||||||
|
const releaseVersionEnv = process.env.RELEASE_VERSION || '';
|
||||||
|
|
||||||
console.log('build with following dir', {
|
console.log('build with following dir', {
|
||||||
repoRootDir,
|
repoRootDir,
|
||||||
@@ -19,9 +23,16 @@ console.log('build with following dir', {
|
|||||||
publicAffineOutDir,
|
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
|
// copy web dist files to electron dist
|
||||||
|
|
||||||
// step 0: clean up
|
// step 1: clean up
|
||||||
await cleanup();
|
await cleanup();
|
||||||
echo('Clean up done');
|
echo('Clean up done');
|
||||||
|
|
||||||
@@ -32,9 +43,6 @@ if (process.platform === 'win32') {
|
|||||||
|
|
||||||
cd(repoRootDir);
|
cd(repoRootDir);
|
||||||
|
|
||||||
// step 1: build electron resources
|
|
||||||
await $`yarn workspace @affine/electron build-layers`;
|
|
||||||
|
|
||||||
// step 2: build web (nextjs) dist
|
// step 2: build web (nextjs) dist
|
||||||
if (!process.env.SKIP_WEB_BUILD) {
|
if (!process.env.SKIP_WEB_BUILD) {
|
||||||
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
||||||
@@ -59,6 +67,17 @@ if (!process.env.SKIP_WEB_BUILD) {
|
|||||||
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
|
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');
|
const mainDistDir = path.resolve(__dirname, '../dist/layers/main');
|
||||||
|
|
||||||
// be careful and avoid any side effects in
|
// be careful and avoid any side effects in
|
||||||
const { handlers, events } = await import(
|
const { handlers, events } = await import(
|
||||||
path.resolve(mainDistDir, 'exposed.js')
|
'file://' + path.resolve(mainDistDir, 'exposed.js')
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlersMeta = Object.entries(handlers).map(
|
const handlersMeta = Object.entries(handlers).map(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { platform } from 'node:os';
|
||||||
|
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import { test } from './fixture';
|
import { test } from './fixture';
|
||||||
@@ -11,8 +13,74 @@ test('new page', async ({ page, workspace }) => {
|
|||||||
expect(flavour).toBe('local');
|
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 }) => {
|
test('app theme', async ({ page, electronApp }) => {
|
||||||
await page.waitForSelector('v-line');
|
|
||||||
const root = page.locator('html');
|
const root = page.locator('html');
|
||||||
{
|
{
|
||||||
const themeMode = await root.evaluate(element =>
|
const themeMode = await root.evaluate(element =>
|
||||||
@@ -20,30 +88,25 @@ test('app theme', async ({ page, electronApp }) => {
|
|||||||
);
|
);
|
||||||
expect(themeMode).toBe('light');
|
expect(themeMode).toBe('light');
|
||||||
|
|
||||||
// check if electron theme source is set to light
|
const theme = await electronApp.evaluate(({ nativeTheme }) => {
|
||||||
const themeSource = await electronApp.evaluate(({ nativeTheme }) => {
|
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||||
return nativeTheme.themeSource;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(themeSource).toBe('light');
|
expect(theme).toBe('light');
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
await page.getByTestId('editor-option-menu').click();
|
await page.getByTestId('editor-option-menu').click();
|
||||||
await page.getByTestId('change-theme-dark').click();
|
await page.getByTestId('change-theme-dark').click();
|
||||||
await page.waitForTimeout(50);
|
await page.waitForTimeout(50);
|
||||||
{
|
const themeMode = await root.evaluate(element =>
|
||||||
const themeMode = await root.evaluate(element =>
|
element.getAttribute('data-theme')
|
||||||
element.getAttribute('data-theme')
|
);
|
||||||
);
|
expect(themeMode).toBe('dark');
|
||||||
expect(themeMode).toBe('dark');
|
const theme = await electronApp.evaluate(({ nativeTheme }) => {
|
||||||
}
|
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||||
|
|
||||||
const themeSource = await electronApp.evaluate(({ nativeTheme }) => {
|
|
||||||
return nativeTheme.themeSource;
|
|
||||||
});
|
});
|
||||||
|
expect(theme).toBe('dark');
|
||||||
expect(themeSource).toBe('dark');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +136,7 @@ test('affine onboarding button', async ({ page }) => {
|
|||||||
'[data-testid=onboarding-modal-editing-video]'
|
'[data-testid=onboarding-modal-editing-video]'
|
||||||
);
|
);
|
||||||
expect(await editingVideo.isVisible()).toEqual(true);
|
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);
|
expect(await onboardingModal.isVisible()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
|
|
||||||
/* eslint-disable no-empty-pattern */
|
/* eslint-disable no-empty-pattern */
|
||||||
import crypto from 'node:crypto';
|
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 fs from 'fs-extra';
|
||||||
import type { ElectronApplication, Page } from 'playwright';
|
import type { ElectronApplication, Page } from 'playwright';
|
||||||
import { _electron as electron } from 'playwright';
|
import { _electron as electron } from 'playwright';
|
||||||
@@ -42,7 +47,31 @@ export const test = base.extend<{
|
|||||||
const logFilePath = await page.evaluate(async () => {
|
const logFilePath = await page.evaluate(async () => {
|
||||||
return window.apis?.debug.logFilePath();
|
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);
|
await use(page);
|
||||||
|
if (enableCoverage) {
|
||||||
|
await page.evaluate(() =>
|
||||||
|
// @ts-expect-error
|
||||||
|
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
|
||||||
|
);
|
||||||
|
}
|
||||||
await page.close();
|
await page.close();
|
||||||
if (logFilePath) {
|
if (logFilePath) {
|
||||||
const logs = await fs.readFile(logFilePath, 'utf-8');
|
const logs = await fs.readFile(logFilePath, 'utf-8');
|
||||||
@@ -52,16 +81,27 @@ export const test = base.extend<{
|
|||||||
electronApp: async ({}, use) => {
|
electronApp: async ({}, use) => {
|
||||||
// a random id to avoid conflicts between tests
|
// a random id to avoid conflicts between tests
|
||||||
const id = generateUUID();
|
const id = generateUUID();
|
||||||
|
const ext = process.platform === 'win32' ? '.cmd' : '';
|
||||||
const electronApp = await electron.launch({
|
const electronApp = await electron.launch({
|
||||||
args: [resolve(__dirname, '..'), '--app-name', 'affine-test-' + id],
|
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',
|
colorScheme: 'light',
|
||||||
});
|
});
|
||||||
const sessionDataPath = await electronApp.evaluate(async ({ app }) => {
|
|
||||||
return app.getPath('sessionData');
|
|
||||||
});
|
|
||||||
await use(electronApp);
|
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) => {
|
appInfo: async ({ electronApp }, use) => {
|
||||||
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
execSync('yarn ts-node-esm scripts/', {
|
execSync('yarn ts-node-esm scripts/', {
|
||||||
cwd: path.join(__dirname, '..'),
|
cwd: join(__dirname, '..'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"skipLibCheck": true,
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"isolatedModules": false,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"noEmit": false
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": ["layers", "types", "package.json"],
|
"include": ["**/*.ts", "**/*.tsx", "package.json"],
|
||||||
"exclude": ["out", "dist", "node_modules"],
|
"exclude": ["out", "dist", "node_modules"],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/native"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# Please do not edit this file manually
|
||||||
# It should be added in your version-control system (i.e. Git)
|
# It should be added in your version-control system (i.e. Git)
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@affine/server",
|
"name": "@affine/server",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.4-canary.30",
|
"version": "0.5.4-beta.2",
|
||||||
"description": "Affine Node.js server",
|
"description": "Affine Node.js server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -16,36 +16,35 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.7.1",
|
"@apollo/server": "^4.7.1",
|
||||||
"@nestjs/apollo": "^11.0.5",
|
"@nestjs/apollo": "^11.0.5",
|
||||||
"@nestjs/common": "^9.4.0",
|
"@nestjs/common": "^9.4.1",
|
||||||
"@nestjs/core": "^9.4.0",
|
"@nestjs/core": "^9.4.1",
|
||||||
"@nestjs/graphql": "^11.0.5",
|
"@nestjs/graphql": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^9.4.0",
|
"@nestjs/platform-express": "^9.4.1",
|
||||||
"@prisma/client": "^4.13.0",
|
"@node-rs/bcrypt": "^1.7.1",
|
||||||
"bcrypt": "^5.1.0",
|
"@prisma/client": "^4.14.1",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"prisma": "^4.13.0",
|
"prisma": "^4.14.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/testing": "^9.4.0",
|
"@nestjs/testing": "^9.4.1",
|
||||||
"@types/bcrypt": "^5.0.0",
|
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jsonwebtoken": "^9.0.2",
|
"@types/jsonwebtoken": "^9.0.2",
|
||||||
"@types/lodash-es": "^4.17.7",
|
"@types/lodash-es": "^4.17.7",
|
||||||
"@types/node": "^18.16.6",
|
"@types/node": "^18.16.12",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"c8": "^7.13.0",
|
"c8": "^7.13.0",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vitest": "^0.31.0"
|
"vitest": "^0.31.1"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"exec": "node",
|
"exec": "node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
import { genSalt } from 'bcrypt';
|
import { genSalt } from '@node-rs/bcrypt';
|
||||||
|
|
||||||
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
|
||||||
namedCurve: 'prime256v1',
|
namedCurve: 'prime256v1',
|
||||||
|
|||||||
@@ -169,10 +169,6 @@ export interface AFFiNEConfig {
|
|||||||
* authentication config
|
* authentication config
|
||||||
*/
|
*/
|
||||||
auth: {
|
auth: {
|
||||||
/**
|
|
||||||
* Application sign key secret
|
|
||||||
*/
|
|
||||||
readonly salt: string;
|
|
||||||
/**
|
/**
|
||||||
* Application access token expiration time
|
* Application access token expiration time
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
|||||||
debug: true,
|
debug: true,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
salt: '$2b$10$x4VDo2nmlo74yB5jflNhlu',
|
|
||||||
accessTokenExpiresIn: '1h',
|
accessTokenExpiresIn: '1h',
|
||||||
refreshTokenExpiresIn: '7d',
|
refreshTokenExpiresIn: '7d',
|
||||||
publicKey: examplePublicKey,
|
publicKey: examplePublicKey,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { compare, hash } from '@node-rs/bcrypt';
|
||||||
import { User } from '@prisma/client';
|
import { User } from '@prisma/client';
|
||||||
import { compare, hash } from 'bcrypt';
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
import { Config } from '../../config';
|
import { Config } from '../../config';
|
||||||
@@ -69,7 +69,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async register(name: string, email: string, password: string): Promise<User> {
|
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({
|
const user = await this.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { afterEach, beforeEach, describe, test } from 'node:test';
|
|||||||
|
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { hash } from '@node-rs/bcrypt';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { hash } from 'bcrypt';
|
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
import { AppModule } from '../app';
|
import { AppModule } from '../app';
|
||||||
@@ -27,7 +27,7 @@ describe('AppModule', () => {
|
|||||||
id: '1',
|
id: '1',
|
||||||
name: 'Alex Yang',
|
name: 'Alex Yang',
|
||||||
email: 'alex.yang@example.org',
|
email: 'alex.yang@example.org',
|
||||||
password: await hash('123456', globalThis.AFFiNE.auth.salt),
|
password: await hash('123456'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@affine/web",
|
"name": "@affine/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.4-canary.30",
|
"version": "0.5.4-beta.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@@ -19,32 +19,33 @@
|
|||||||
"@affine/jotai": "workspace:*",
|
"@affine/jotai": "workspace:*",
|
||||||
"@affine/templates": "workspace:*",
|
"@affine/templates": "workspace:*",
|
||||||
"@affine/workspace": "workspace:*",
|
"@affine/workspace": "workspace:*",
|
||||||
"@blocksuite/blocks": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
"@blocksuite/blocks": "0.0.0-20230518051344-45970a96-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
"@blocksuite/editor": "0.0.0-20230518051344-45970a96-nightly",
|
||||||
"@blocksuite/global": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
"@blocksuite/global": "0.0.0-20230518051344-45970a96-nightly",
|
||||||
"@blocksuite/icons": "^2.1.15",
|
"@blocksuite/icons": "^2.1.16",
|
||||||
"@blocksuite/store": "0.0.0-20230509052644-b8b1b6a1-nightly",
|
"@blocksuite/lit": "0.0.0-20230518051344-45970a96-nightly",
|
||||||
|
"@blocksuite/store": "0.0.0-20230518051344-45970a96-nightly",
|
||||||
"@dnd-kit/core": "^6.0.8",
|
"@dnd-kit/core": "^6.0.8",
|
||||||
"@dnd-kit/sortable": "^7.0.2",
|
"@dnd-kit/sortable": "^7.0.2",
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
"@emotion/react": "^11.11.0",
|
"@emotion/react": "^11.11.0",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/material": "^5.12.3",
|
"@mui/material": "^5.13.1",
|
||||||
"@react-hookz/web": "^23.0.0",
|
"@react-hookz/web": "^23.0.0",
|
||||||
"@sentry/nextjs": "^7.51.2",
|
"@sentry/nextjs": "^7.52.1",
|
||||||
"@toeverything/hooks": "workspace:*",
|
"@toeverything/hooks": "workspace:*",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"css-spring": "^4.1.0",
|
"css-spring": "^4.1.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"jotai": "^2.1.0",
|
"jotai": "^2.1.0",
|
||||||
"jotai-devtools": "^0.5.2",
|
"jotai-devtools": "^0.5.3",
|
||||||
"lit": "^2.7.4",
|
"lit": "^2.7.4",
|
||||||
"lottie-web": "^5.11.0",
|
"lottie-web": "^5.11.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "18.3.0-canary-16d053d59-20230506",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "18.3.0-canary-16d053d59-20230506",
|
||||||
"react-is": "^18.2.0",
|
"react-is": "^18.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swr": "^2.1.5",
|
"swr": "^2.1.5",
|
||||||
@@ -66,16 +67,16 @@
|
|||||||
"@vanilla-extract/next-plugin": "^2.1.2",
|
"@vanilla-extract/next-plugin": "^2.1.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-next": "^13.4.1",
|
"eslint-config-next": "^13.4.2",
|
||||||
"eslint-plugin-unicorn": "^47.0.0",
|
"eslint-plugin-unicorn": "^47.0.0",
|
||||||
"next": "^13.4.1",
|
"next": "^13.4.2",
|
||||||
"next-debug-local": "^0.1.5",
|
"next-debug-local": "^0.1.5",
|
||||||
"next-router-mock": "^0.9.3",
|
"next-router-mock": "^0.9.3",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"redux": "^4.2.1",
|
"redux": "^4.2.1",
|
||||||
"swc-plugin-coverage-instrument": "^0.0.18",
|
"swc-plugin-coverage-instrument": "^0.0.18",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"webpack": "^5.82.0"
|
"webpack": "^5.83.1"
|
||||||
},
|
},
|
||||||
"stableVersion": "0.0.0"
|
"stableVersion": "0.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,7 @@ export const buildFlags = {
|
|||||||
enableDebugPage: Boolean(
|
enableDebugPage: Boolean(
|
||||||
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
|
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 { createStore } from 'jotai';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import { WorkspacePlugins } from '../../plugins';
|
import { WorkspaceAdapters } from '../../plugins';
|
||||||
import { rootCurrentWorkspaceAtom } from '../root';
|
import { rootCurrentWorkspaceAtom } from '../root';
|
||||||
|
|
||||||
describe('currentWorkspace atom', () => {
|
describe('currentWorkspace atom', () => {
|
||||||
@@ -45,7 +45,7 @@ describe('currentWorkspace atom', () => {
|
|||||||
const provider = createIndexedDBDownloadProvider(workspace);
|
const provider = createIndexedDBDownloadProvider(workspace);
|
||||||
provider.sync();
|
provider.sync();
|
||||||
await provider.whenReady;
|
await provider.whenReady;
|
||||||
const workspaceId = await WorkspacePlugins[
|
const workspaceId = await WorkspaceAdapters[
|
||||||
WorkspaceFlavour.LOCAL
|
WorkspaceFlavour.LOCAL
|
||||||
].CRUD.create(workspace);
|
].CRUD.create(workspace);
|
||||||
store.set(rootWorkspacesMetadataAtom, [
|
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 { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspaceAdapters } from '../plugins';
|
||||||
|
|
||||||
const logger = new DebugLogger('web:atoms');
|
const logger = new DebugLogger('web:atoms');
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
|
|||||||
// todo(himself65): move this to the workspace package
|
// todo(himself65): move this to the workspace package
|
||||||
rootWorkspacesMetadataAtom.onMount = setAtom => {
|
rootWorkspacesMetadataAtom.onMount = setAtom => {
|
||||||
function createFirst(): RootWorkspaceMetadata[] {
|
function createFirst(): RootWorkspaceMetadata[] {
|
||||||
const Plugins = Object.values(WorkspacePlugins).sort(
|
const Plugins = Object.values(WorkspaceAdapters).sort(
|
||||||
(a, b) => a.loadPriority - b.loadPriority
|
(a, b) => a.loadPriority - b.loadPriority
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -40,17 +40,24 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
|
|||||||
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
|
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAtom(metadata => {
|
const abortController = new AbortController();
|
||||||
if (metadata.length === 0) {
|
|
||||||
const newMetadata = createFirst();
|
// next tick to make sure the hydration is correct
|
||||||
logger.info('create first workspace', newMetadata);
|
const id = setTimeout(() => {
|
||||||
return newMetadata;
|
setAtom(metadata => {
|
||||||
}
|
if (abortController.signal.aborted) return metadata;
|
||||||
return metadata;
|
if (metadata.length === 0) {
|
||||||
});
|
const newMetadata = createFirst();
|
||||||
|
logger.info('create first workspace', newMetadata);
|
||||||
|
return newMetadata;
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
window.apis?.workspace.list().then(workspaceIDs => {
|
window.apis?.workspace.list().then(workspaceIDs => {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
const newMetadata = workspaceIDs.map(w => ({
|
const newMetadata = workspaceIDs.map(w => ({
|
||||||
id: w[0],
|
id: w[0],
|
||||||
flavour: WorkspaceFlavour.LOCAL,
|
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 { assertExists } from '@blocksuite/store';
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspaceAdapters } from '../plugins';
|
||||||
import type { AllWorkspace } from '../shared';
|
import type { AllWorkspace } from '../shared';
|
||||||
|
|
||||||
const logger = new DebugLogger('web:atoms:root');
|
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
|
* Fetch all workspaces from the Plugin CRUD
|
||||||
*/
|
*/
|
||||||
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
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
|
plugin => plugin.flavour
|
||||||
);
|
);
|
||||||
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
|
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
|
||||||
@@ -38,7 +38,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
|||||||
const workspaces = await Promise.all(
|
const workspaces = await Promise.all(
|
||||||
jotaiWorkspaces.map(workspace => {
|
jotaiWorkspaces.map(workspace => {
|
||||||
const plugin =
|
const plugin =
|
||||||
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
|
WorkspaceAdapters[workspace.flavour as keyof typeof WorkspaceAdapters];
|
||||||
assertExists(plugin);
|
assertExists(plugin);
|
||||||
const { CRUD } = plugin;
|
const { CRUD } = plugin;
|
||||||
return CRUD.get(workspace.id).then(workspace => {
|
return CRUD.get(workspace.id).then(workspace => {
|
||||||
@@ -93,7 +93,7 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
|
|||||||
if (!targetWorkspace) {
|
if (!targetWorkspace) {
|
||||||
throw new Error(`cannot find the workspace with id ${targetId}.`);
|
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
|
targetWorkspace.id
|
||||||
);
|
);
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const createAffineDownloadProvider = (
|
|||||||
new Uint8Array(hashMap.get(id) as ArrayBuffer)
|
new Uint8Array(hashMap.get(id) as ArrayBuffer)
|
||||||
);
|
);
|
||||||
connected = true;
|
connected = true;
|
||||||
|
callbacks.forEach(cb => cb());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
affineApis
|
affineApis
|
||||||
@@ -41,6 +42,8 @@ export const createAffineDownloadProvider = (
|
|||||||
blockSuiteWorkspace.doc,
|
blockSuiteWorkspace.doc,
|
||||||
new Uint8Array(binary)
|
new Uint8Array(binary)
|
||||||
);
|
);
|
||||||
|
connected = true;
|
||||||
|
callbacks.forEach(cb => cb());
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
providerLogger.error('downloadWorkspace', 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 { RequestError } from '@affine/workspace/affine/api';
|
||||||
import type { NextRouter } from 'next/router';
|
import type { NextRouter } from 'next/router';
|
||||||
import type { ErrorInfo, ReactNode } from 'react';
|
import type { ErrorInfo, ReactNode } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../../shared';
|
|
||||||
|
|
||||||
export type AffineErrorBoundaryProps = React.PropsWithChildren<{
|
export type AffineErrorBoundaryProps = React.PropsWithChildren<{
|
||||||
router: NextRouter;
|
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 =
|
type AffineError =
|
||||||
| QueryParamError
|
| QueryParamError
|
||||||
| Unreachable
|
| Unreachable
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const NameWorkspaceContent = ({
|
|||||||
data-testid="create-workspace-input"
|
data-testid="create-workspace-input"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={t['Set a Workspace name']()}
|
placeholder={t['Set a Workspace name']()}
|
||||||
maxLength={15} // TODO: the max workspace name length?
|
maxLength={64}
|
||||||
minLength={0}
|
minLength={0}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
setWorkspaceName(value);
|
setWorkspaceName(value);
|
||||||
@@ -118,10 +118,7 @@ interface SetDBLocationContentProps {
|
|||||||
onConfirmLocation: (dir?: string) => void;
|
onConfirmLocation: (dir?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetDBLocationContent = ({
|
const useDefaultDBLocation = () => {
|
||||||
onConfirmLocation,
|
|
||||||
}: SetDBLocationContentProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const [defaultDBLocation, setDefaultDBLocation] = useState('');
|
const [defaultDBLocation, setDefaultDBLocation] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
<div className={style.contentTitle}>{t['Set database location']()}</div>
|
<div className={style.contentTitle}>{t['Set database location']()}</div>
|
||||||
<p>{t['Workspace database storage description']()}</p>
|
<p>{t['Workspace database storage description']()}</p>
|
||||||
<div className={style.buttonGroup}>
|
<div className={style.buttonGroup}>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={opening}
|
||||||
data-testid="create-workspace-customize-button"
|
data-testid="create-workspace-customize-button"
|
||||||
type="light"
|
type="light"
|
||||||
onClick={async () => {
|
onClick={handleSelectDBFileLocation}
|
||||||
const result = await window.apis?.dialog.selectDBFileLocation();
|
|
||||||
if (result) {
|
|
||||||
onConfirmLocation(result.filePath);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t['Customize']()}
|
{t['Customize']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const StyledSidebarSwitch = styled(IconButton, {
|
|||||||
})<{ visible: boolean }>(({ visible }) => {
|
})<{ visible: boolean }>(({ visible }) => {
|
||||||
return {
|
return {
|
||||||
opacity: visible ? 1 : 0,
|
opacity: visible ? 1 : 0,
|
||||||
WebkitAppRegion: 'no-drag',
|
WebkitAppRegion: visible ? 'no-drag' : 'drag',
|
||||||
transition: 'all 0.2s ease-in-out',
|
transition: 'all 0.2s ease-in-out',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Empty, IconButton, Modal, ModalWrapper } from '@affine/component';
|
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 { CloseIcon } from '@blocksuite/icons';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ interface TmpDisableAffineCloudModalProps {
|
|||||||
export const TmpDisableAffineCloudModal: React.FC<
|
export const TmpDisableAffineCloudModal: React.FC<
|
||||||
TmpDisableAffineCloudModalProps
|
TmpDisableAffineCloudModalProps
|
||||||
> = ({ open, onClose }) => {
|
> = ({ open, onClose }) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
data-testid="disable-affine-cloud-modal"
|
data-testid="disable-affine-cloud-modal"
|
||||||
@@ -37,21 +40,25 @@ export const TmpDisableAffineCloudModal: React.FC<
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
<Content>
|
<Content>
|
||||||
<ContentTitle>AFFiNE Cloud is upgrading now.</ContentTitle>
|
<ContentTitle>
|
||||||
|
{t['com.affine.cloudTempDisable.title']()}
|
||||||
|
</ContentTitle>
|
||||||
<StyleTips>
|
<StyleTips>
|
||||||
We are upgrading the AFFiNE Cloud service and it is temporarily
|
<Trans i18nKey="com.affine.cloudTempDisable.description">
|
||||||
unavailable on the client side. If you wish to be notified the first
|
We are upgrading the AFFiNE Cloud service and it is temporarily
|
||||||
time it's available, please
|
unavailable on the client side. If you wish to stay updated on the
|
||||||
<a
|
progress and be notified on availability, you can fill out the
|
||||||
href="https://github.com/toeverything/AFFiNE/releases"
|
<a
|
||||||
target="_blank"
|
href="https://6dxre9ihosp.typeform.com/to/B8IHwuyy"
|
||||||
style={{
|
target="_blank"
|
||||||
color: 'var(--affine-link-color)',
|
style={{
|
||||||
}}
|
color: 'var(--affine-link-color)',
|
||||||
>
|
}}
|
||||||
click here
|
>
|
||||||
</a>
|
AFFiNE Cloud Signup
|
||||||
.
|
</a>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
</StyleTips>
|
</StyleTips>
|
||||||
<StyleImage>
|
<StyleImage>
|
||||||
<Empty
|
<Empty
|
||||||
@@ -69,7 +76,7 @@ export const TmpDisableAffineCloudModal: React.FC<
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Got it
|
{t['Got it']()}
|
||||||
</StyleButton>
|
</StyleButton>
|
||||||
</StyleButtonContainer>
|
</StyleButtonContainer>
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
|
|||||||
export const container = style({
|
export const container = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: '52px 52px 0 52px',
|
marginTop: '52px',
|
||||||
|
padding: '0 52px 52px 52px',
|
||||||
height: 'calc(100vh - 52px)',
|
height: 'calc(100vh - 52px)',
|
||||||
|
overflow: 'auto',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sidebar = style({
|
export const sidebar = style({
|
||||||
@@ -15,7 +17,6 @@ export const sidebar = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
overflow: 'auto',
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginTop: '40px',
|
marginTop: '40px',
|
||||||
});
|
});
|
||||||
@@ -110,7 +111,8 @@ export const settingItemLabelHint = style({
|
|||||||
export const row = style({
|
export const row = style({
|
||||||
padding: '40px 0',
|
padding: '40px 0',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '60px',
|
columnGap: '60px',
|
||||||
|
rowGap: '12px',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&': {
|
'&': {
|
||||||
borderBottom: '1px solid var(--affine-border-color)',
|
borderBottom: '1px solid var(--affine-border-color)',
|
||||||
@@ -119,22 +121,22 @@ export const row = style({
|
|||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
flexWrap: 'wrap',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const col = style({
|
export const col = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
flexShrink: 0,
|
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${row} &:nth-child(1)`]: {
|
[`${row} &:nth-child(1)`]: {
|
||||||
flex: 3,
|
flex: '3 0 200px',
|
||||||
},
|
},
|
||||||
[`${row} &:nth-child(2)`]: {
|
[`${row} &:nth-child(2)`]: {
|
||||||
flex: 5,
|
flex: '5 0 240px',
|
||||||
},
|
},
|
||||||
[`${row} &:nth-child(3)`]: {
|
[`${row} &:nth-child(3)`]: {
|
||||||
flex: 2,
|
flex: '2 0 200px',
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -156,7 +158,10 @@ export const indicator = style({
|
|||||||
|
|
||||||
export const tabButtonWrapper = style({
|
export const tabButtonWrapper = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
position: 'relative',
|
position: 'sticky',
|
||||||
|
top: '0',
|
||||||
|
background: 'var(--affine-background-primary-color)',
|
||||||
|
zIndex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const storageTypeWrapper = style({
|
export const storageTypeWrapper = style({
|
||||||
@@ -175,6 +180,10 @@ export const storageTypeWrapper = style({
|
|||||||
'&:not(:last-child)': {
|
'&:not(:last-child)': {
|
||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
},
|
},
|
||||||
|
'&[data-disabled="true"]': {
|
||||||
|
cursor: 'default',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { PermissionType } from '@affine/workspace/affine/api';
|
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
@@ -14,7 +18,6 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import { useMembers } from '../../../../../hooks/affine/use-members';
|
import { useMembers } from '../../../../../hooks/affine/use-members';
|
||||||
import { toast } from '../../../../../utils';
|
import { toast } from '../../../../../utils';
|
||||||
import { Unreachable } from '../../../affine-error-eoundary';
|
|
||||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||||
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
|
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
|
||||||
import type { PanelProps } from '../../index';
|
import type { PanelProps } from '../../index';
|
||||||
@@ -37,7 +40,7 @@ import {
|
|||||||
|
|
||||||
const AffineRemoteCollaborationPanel: React.FC<
|
const AffineRemoteCollaborationPanel: React.FC<
|
||||||
Omit<PanelProps, 'workspace'> & {
|
Omit<PanelProps, 'workspace'> & {
|
||||||
workspace: AffineWorkspace;
|
workspace: AffineLegacyCloudWorkspace;
|
||||||
}
|
}
|
||||||
> = ({ workspace }) => {
|
> = ({ workspace }) => {
|
||||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||||
@@ -214,7 +217,7 @@ const LocalCollaborationPanel: React.FC<
|
|||||||
export const CollaborationPanel: React.FC<PanelProps> = props => {
|
export const CollaborationPanel: React.FC<PanelProps> = props => {
|
||||||
switch (props.workspace.flavour) {
|
switch (props.workspace.flavour) {
|
||||||
case WorkspaceFlavour.AFFINE: {
|
case WorkspaceFlavour.AFFINE: {
|
||||||
const workspace = props.workspace as AffineWorkspace;
|
const workspace = props.workspace as AffineLegacyCloudWorkspace;
|
||||||
return (
|
return (
|
||||||
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
|
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ export const ExportPanel = () => {
|
|||||||
disabled={!environment.isDesktop || !id}
|
disabled={!environment.isDesktop || !id}
|
||||||
data-testid="export-affine-backup"
|
data-testid="export-affine-backup"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (id && (await window.apis?.dialog.saveDBFileAs(id))) {
|
if (id) {
|
||||||
toast(t['Export success']());
|
const result = await window.apis?.dialog.saveDBFileAs(id);
|
||||||
|
if (result?.error) {
|
||||||
|
toast(t[result.error]());
|
||||||
|
} else if (!result?.canceled) {
|
||||||
|
toast(t['Export success']());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const StyledButtonContent = styled('div')(() => {
|
|||||||
|
|
||||||
export const StyledWorkspaceName = styled('span')(() => {
|
export const StyledWorkspaceName = styled('span')(() => {
|
||||||
return {
|
return {
|
||||||
color: '#E8178A',
|
fontWeight: '600',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s
|
|||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
|
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
|
||||||
import { Upload } from '../../../../pure/file-upload';
|
import { Upload } from '../../../../pure/file-upload';
|
||||||
@@ -23,6 +23,26 @@ import { CameraIcon } from './icons';
|
|||||||
import { WorkspaceLeave } from './leave';
|
import { WorkspaceLeave } from './leave';
|
||||||
import { StyledInput } from './style';
|
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> = ({
|
export const GeneralPanel: React.FC<PanelProps> = ({
|
||||||
workspace,
|
workspace,
|
||||||
onDeleteWorkspace,
|
onDeleteWorkspace,
|
||||||
@@ -36,11 +56,36 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
|||||||
const isOwner = useIsWorkspaceOwner(workspace);
|
const isOwner = useIsWorkspaceOwner(workspace);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
const dbPathMeta = useDBFilePathMeta(workspace.id);
|
||||||
|
const showOpenFolder =
|
||||||
|
environment.isDesktop && dbPathMeta?.path !== dbPathMeta?.realPath;
|
||||||
|
|
||||||
const handleUpdateWorkspaceName = (name: string) => {
|
const handleUpdateWorkspaceName = (name: string) => {
|
||||||
setName(name);
|
setName(name);
|
||||||
toast(t['Update workspace name success']());
|
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(
|
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||||
workspace.blockSuiteWorkspace
|
workspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
@@ -88,12 +133,11 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
|||||||
|
|
||||||
<div className={style.col}>
|
<div className={style.col}>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
width={284}
|
|
||||||
height={38}
|
height={38}
|
||||||
value={input}
|
value={input}
|
||||||
data-testid="workspace-name-input"
|
data-testid="workspace-name-input"
|
||||||
placeholder={t['Workspace Name']()}
|
placeholder={t['Workspace Name']()}
|
||||||
maxLength={50}
|
maxLength={64}
|
||||||
minLength={0}
|
minLength={0}
|
||||||
onChange={newName => {
|
onChange={newName => {
|
||||||
setInput(newName);
|
setInput(newName);
|
||||||
@@ -129,34 +173,33 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={style.col}>
|
<div className={style.col}>
|
||||||
<div
|
{showOpenFolder && (
|
||||||
className={style.storageTypeWrapper}
|
<div
|
||||||
onClick={() => {
|
className={style.storageTypeWrapper}
|
||||||
if (environment.isDesktop) {
|
onClick={() => {
|
||||||
window.apis?.dialog.revealDBFile(workspace.id);
|
if (environment.isDesktop) {
|
||||||
}
|
window.apis?.dialog.revealDBFile(workspace.id);
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<FolderIcon color="var(--affine-primary-color)" />
|
>
|
||||||
<div className={style.storageTypeLabelWrapper}>
|
<FolderIcon color="var(--affine-primary-color)" />
|
||||||
<div className={style.storageTypeLabel}>
|
<div className={style.storageTypeLabelWrapper}>
|
||||||
{t['Open folder']()}
|
<div className={style.storageTypeLabel}>
|
||||||
</div>
|
{t['Open folder']()}
|
||||||
<div className={style.storageTypeLabelHint}>
|
</div>
|
||||||
{t['Open folder hint']()}
|
<div className={style.storageTypeLabelHint}>
|
||||||
|
{t['Open folder hint']()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-testid="move-folder"
|
data-testid="move-folder"
|
||||||
|
data-disabled={moveToInProgress}
|
||||||
className={style.storageTypeWrapper}
|
className={style.storageTypeWrapper}
|
||||||
onClick={async () => {
|
onClick={handleMoveTo}
|
||||||
if (await window.apis?.dialog.moveDBFile(workspace.id)) {
|
|
||||||
toast(t['Move folder success']());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<MoveToIcon color="var(--affine-primary-color)" />
|
<MoveToIcon color="var(--affine-primary-color)" />
|
||||||
<div className={style.storageTypeLabelWrapper}>
|
<div className={style.storageTypeLabelWrapper}>
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { displayFlex, styled } from '@affine/component';
|
import { displayFlex, styled } from '@affine/component';
|
||||||
import { Input } from '@affine/component';
|
import { Input } from '@affine/component';
|
||||||
|
|
||||||
export const StyledInput = styled(Input)(() => {
|
export const StyledInput = Input;
|
||||||
return {
|
|
||||||
border: '1px solid var(--affine-border-color)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
fontSize: 'var(--affine-font-sm)',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const StyledWorkspaceInfo = styled('div')(() => {
|
export const StyledWorkspaceInfo = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import {
|
|||||||
Wrapper,
|
Wrapper,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import type React from 'react';
|
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 { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
|
||||||
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
||||||
import { toast } from '../../../../../utils';
|
import { toast } from '../../../../../utils';
|
||||||
import { Unreachable } from '../../../affine-error-eoundary';
|
|
||||||
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
||||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||||
import type { WorkspaceSettingDetailProps } from '../../index';
|
import type { WorkspaceSettingDetailProps } from '../../index';
|
||||||
@@ -26,7 +29,7 @@ export type PublishPanelProps = WorkspaceSettingDetailProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
|
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
|
||||||
workspace: AffineWorkspace;
|
workspace: AffineLegacyCloudWorkspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
|
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { pageListEmptyStyle } from './index.css';
|
|||||||
|
|
||||||
export type BlockSuitePageListProps = {
|
export type BlockSuitePageListProps = {
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
listType: 'all' | 'trash' | 'favorite' | 'shared' | 'public';
|
listType: 'all' | 'trash' | 'shared' | 'public';
|
||||||
isPublic?: true;
|
isPublic?: true;
|
||||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||||
};
|
};
|
||||||
@@ -31,7 +31,6 @@ const filter = {
|
|||||||
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
|
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
|
||||||
return !parentMeta?.trash && pageMeta.trash;
|
return !parentMeta?.trash && pageMeta.trash;
|
||||||
},
|
},
|
||||||
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
|
|
||||||
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,9 +51,6 @@ const PageListEmpty = (props: {
|
|||||||
if (listType === 'all') {
|
if (listType === 'all') {
|
||||||
return t['emptyAllPages']();
|
return t['emptyAllPages']();
|
||||||
}
|
}
|
||||||
if (listType === 'favorite') {
|
|
||||||
return t['emptyFavorite']();
|
|
||||||
}
|
|
||||||
if (listType === 'trash') {
|
if (listType === 'trash') {
|
||||||
return t['emptyTrash']();
|
return t['emptyTrash']();
|
||||||
}
|
}
|
||||||
@@ -102,7 +98,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
pageId: pageMeta.id,
|
pageId: pageMeta.id,
|
||||||
title: pageMeta.title,
|
title: pageMeta.title,
|
||||||
createDate: formatDate(pageMeta.createDate),
|
createDate: formatDate(pageMeta.createDate),
|
||||||
updatedDate: formatDate(pageMeta.updatedDate),
|
updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
|
||||||
onClickPage: () => onOpenPage(pageMeta.id),
|
onClickPage: () => onOpenPage(pageMeta.id),
|
||||||
onClickRestore: () => {
|
onClickRestore: () => {
|
||||||
restoreFromTrash(pageMeta.id);
|
restoreFromTrash(pageMeta.id);
|
||||||
@@ -129,7 +125,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
favorite: !!pageMeta.favorite,
|
favorite: !!pageMeta.favorite,
|
||||||
isPublicPage: !!pageMeta.isPublic,
|
isPublicPage: !!pageMeta.isPublic,
|
||||||
createDate: formatDate(pageMeta.createDate),
|
createDate: formatDate(pageMeta.createDate),
|
||||||
updatedDate: formatDate(pageMeta.updatedDate),
|
updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
|
||||||
onClickPage: () => onOpenPage(pageMeta.id),
|
onClickPage: () => onOpenPage(pageMeta.id),
|
||||||
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
|
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
|
||||||
onClickRestore: () => {
|
onClickRestore: () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
@@ -27,6 +28,7 @@ export const EditorModeSwitch = ({
|
|||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
meta => meta.id === pageId
|
meta => meta.id === pageId
|
||||||
);
|
);
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
assertExists(pageMeta);
|
assertExists(pageMeta);
|
||||||
const { trash } = pageMeta;
|
const { trash } = pageMeta;
|
||||||
|
|
||||||
@@ -41,8 +43,12 @@ export const EditorModeSwitch = ({
|
|||||||
active={currentMode === 'page'}
|
active={currentMode === 'page'}
|
||||||
hide={trash && currentMode !== 'page'}
|
hide={trash && currentMode !== 'page'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode(mode => ({ ...mode, [pageMeta.id]: 'page' }));
|
setMode(mode => {
|
||||||
toast('Page mode');
|
if (mode[pageMeta.id] !== 'page') {
|
||||||
|
toast(t['com.affine.pageMode']());
|
||||||
|
}
|
||||||
|
return { ...mode, [pageMeta.id]: 'page' };
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EdgelessSwitchItem
|
<EdgelessSwitchItem
|
||||||
@@ -50,8 +56,12 @@ export const EditorModeSwitch = ({
|
|||||||
active={currentMode === 'edgeless'}
|
active={currentMode === 'edgeless'}
|
||||||
hide={trash && currentMode !== 'edgeless'}
|
hide={trash && currentMode !== 'edgeless'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode(mode => ({ ...mode, [pageMeta.id]: 'edgeless' }));
|
setMode(mode => {
|
||||||
toast('Edgeless mode');
|
if (mode[pageMeta.id] !== 'edgeless') {
|
||||||
|
toast(t['com.affine.edgelessMode']());
|
||||||
|
}
|
||||||
|
return { ...mode, [pageMeta.id]: 'edgeless' };
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledEditorModeSwitch>
|
</StyledEditorModeSwitch>
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id'
|
|||||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||||
import { toast } from '../../../../utils';
|
import { toast } from '../../../../utils';
|
||||||
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
|
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
|
||||||
import {
|
import * as styles from '../styles.css';
|
||||||
StyledHorizontalDivider,
|
|
||||||
StyledHorizontalDividerContainer,
|
|
||||||
} from '../styles';
|
|
||||||
import { LanguageMenu } from './language-menu';
|
import { LanguageMenu } from './language-menu';
|
||||||
const CommonMenu = () => {
|
const CommonMenu = () => {
|
||||||
const content = (
|
const content = (
|
||||||
@@ -43,7 +40,6 @@ const CommonMenu = () => {
|
|||||||
return (
|
return (
|
||||||
<FlexWrapper alignItems="center" justifyContent="center">
|
<FlexWrapper alignItems="center" justifyContent="center">
|
||||||
<Menu
|
<Menu
|
||||||
width={276}
|
|
||||||
content={content}
|
content={content}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
disablePortal={true}
|
disablePortal={true}
|
||||||
@@ -120,9 +116,9 @@ const PageMenu = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StyledHorizontalDividerContainer>
|
<div className={styles.horizontalDividerContainer}>
|
||||||
<StyledHorizontalDivider />
|
<div className={styles.horizontalDivider} />
|
||||||
</StyledHorizontalDividerContainer>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -140,7 +136,6 @@ const PageMenu = () => {
|
|||||||
<>
|
<>
|
||||||
<FlexWrapper alignItems="center" justifyContent="center">
|
<FlexWrapper alignItems="center" justifyContent="center">
|
||||||
<Menu
|
<Menu
|
||||||
width={276}
|
|
||||||
content={EditMenu}
|
content={EditMenu}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
disablePortal={true}
|
disablePortal={true}
|
||||||
@@ -156,6 +151,7 @@ const PageMenu = () => {
|
|||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
removeToTrash(pageMeta.id);
|
removeToTrash(pageMeta.id);
|
||||||
toast(t['Moved to Trash']());
|
toast(t['Moved to Trash']());
|
||||||
|
setOpenConfirm(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setOpenConfirm(false);
|
setOpenConfirm(false);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ShareMenu } from '@affine/component/share-menu';
|
import { ShareMenu } from '@affine/component/share-menu';
|
||||||
import { config } from '@affine/env';
|
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { assertEquals } 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 { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||||
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
||||||
import { WorkspaceSubPath } from '../../../../shared';
|
import { WorkspaceSubPath } from '../../../../shared';
|
||||||
import { Unreachable } from '../../../affine/affine-error-eoundary';
|
|
||||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||||
import type { BaseHeaderProps } from '../header';
|
import type { BaseHeaderProps } from '../header';
|
||||||
|
|
||||||
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||||
// todo: these hooks should be moved to the top level
|
// todo: these hooks should be moved to the top level
|
||||||
const togglePublish = useToggleWorkspacePublish(
|
const togglePublish = useToggleWorkspacePublish(
|
||||||
props.workspace as AffineWorkspace
|
props.workspace as AffineLegacyCloudWorkspace
|
||||||
);
|
);
|
||||||
const helper = useRouterHelper(useRouter());
|
const helper = useRouterHelper(useRouter());
|
||||||
return (
|
return (
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
workspace={props.workspace as AffineWorkspace}
|
workspace={props.workspace as AffineLegacyCloudWorkspace}
|
||||||
currentPage={props.currentPage as Page}
|
currentPage={props.currentPage as Page}
|
||||||
onEnableAffineCloud={useCallback(async () => {
|
onEnableAffineCloud={useCallback(async () => {
|
||||||
throw new Unreachable(
|
throw new Unreachable(
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ export const StyledThemeButton = styled('button')<{
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}>(({ active }) => {
|
}>(({ active }) => {
|
||||||
return {
|
return {
|
||||||
|
padding: '0 8px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
|
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
export const StyledVerticalDivider = styled('div')(() => {
|
export const StyledVerticalDivider = styled('div')(() => {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
|
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@@ -24,11 +28,7 @@ import { HeaderShareMenu } from './header-right-items/share-menu';
|
|||||||
import SyncUser from './header-right-items/sync-user';
|
import SyncUser from './header-right-items/sync-user';
|
||||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||||
import UserAvatar from './header-right-items/user-avatar';
|
import UserAvatar from './header-right-items/user-avatar';
|
||||||
import {
|
import * as styles from './styles.css';
|
||||||
StyledHeader,
|
|
||||||
StyledHeaderContainer,
|
|
||||||
StyledHeaderRightSide,
|
|
||||||
} from './styles';
|
|
||||||
import { OSWarningMessage, shouldShowWarning } from './utils';
|
import { OSWarningMessage, shouldShowWarning } from './utils';
|
||||||
|
|
||||||
const SidebarSwitch = lazy(() =>
|
const SidebarSwitch = lazy(() =>
|
||||||
@@ -53,6 +53,9 @@ export const enum HeaderRightItemName {
|
|||||||
ShareMenu = 'shareMenu',
|
ShareMenu = 'shareMenu',
|
||||||
EditPage = 'editPage',
|
EditPage = 'editPage',
|
||||||
UserAvatar = 'userAvatar',
|
UserAvatar = 'userAvatar',
|
||||||
|
|
||||||
|
// some windows only items
|
||||||
|
WindowsAppControls = 'windowsAppControls',
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeaderItem = {
|
type HeaderItem = {
|
||||||
@@ -67,6 +70,7 @@ type HeaderItem = {
|
|||||||
}
|
}
|
||||||
) => boolean;
|
) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||||
[HeaderRightItemName.TrashButtonGroup]: {
|
[HeaderRightItemName.TrashButtonGroup]: {
|
||||||
Component: TrashButtonGroup,
|
Component: TrashButtonGroup,
|
||||||
@@ -104,6 +108,44 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
|||||||
return !isPublic && !isPreview;
|
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;
|
export type HeaderProps = BaseHeaderProps;
|
||||||
@@ -122,15 +164,19 @@ export const Header = forwardRef<
|
|||||||
setShowWarning(shouldShowWarning());
|
setShowWarning(shouldShowWarning());
|
||||||
setShowGuideDownloadClientTip(shouldShowGuideDownloadClientTip);
|
setShowGuideDownloadClientTip(shouldShowGuideDownloadClientTip);
|
||||||
}, [shouldShowGuideDownloadClientTip]);
|
}, [shouldShowGuideDownloadClientTip]);
|
||||||
const [open] = useAtom(appSidebarOpenAtom);
|
const open = useAtomValue(appSidebarOpenAtom);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||||
|
|
||||||
const mode = useCurrentMode();
|
const mode = useCurrentMode();
|
||||||
return (
|
return (
|
||||||
<StyledHeaderContainer
|
<div
|
||||||
|
className={styles.headerContainer}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasWarning={showWarning}
|
data-has-warning={showWarning}
|
||||||
data-open={open}
|
data-open={open}
|
||||||
|
data-sidebar-floating={appSidebarFloating}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{showGuideDownloadClientTip ? (
|
{showGuideDownloadClientTip ? (
|
||||||
@@ -145,11 +191,11 @@ export const Header = forwardRef<
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StyledHeader
|
<div
|
||||||
hasWarning={showWarning}
|
className={styles.header}
|
||||||
|
data-has-warning={showWarning}
|
||||||
data-testid="editor-header-items"
|
data-testid="editor-header-items"
|
||||||
data-tauri-drag-region
|
data-is-edgeless={mode === 'edgeless'}
|
||||||
isEdgeless={mode === 'edgeless'}
|
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<SidebarSwitch
|
<SidebarSwitch
|
||||||
@@ -160,7 +206,7 @@ export const Header = forwardRef<
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
<StyledHeaderRightSide>
|
<div className={styles.headerRightSide}>
|
||||||
{useMemo(() => {
|
{useMemo(() => {
|
||||||
return Object.entries(HeaderRightItems).map(
|
return Object.entries(HeaderRightItems).map(
|
||||||
([name, { availableWhen, Component }]) => {
|
([name, { availableWhen, Component }]) => {
|
||||||
@@ -184,10 +230,9 @@ export const Header = forwardRef<
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [props])}
|
}, [props])}
|
||||||
{/*<ShareMenu />*/}
|
</div>
|
||||||
</StyledHeaderRightSide>
|
</div>
|
||||||
</StyledHeader>
|
</div>
|
||||||
</StyledHeaderContainer>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 { assertExists } from '@blocksuite/store';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
import type {
|
||||||
import { forwardRef, useCallback, useRef } from 'react';
|
FC,
|
||||||
|
HTMLAttributes,
|
||||||
|
PropsWithChildren,
|
||||||
|
ReactElement,
|
||||||
|
} from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
|
import { openQuickSearchModalAtom } from '../../../atoms';
|
||||||
import { guideQuickSearchTipsAtom } from '../../../atoms/guide';
|
|
||||||
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
|
|
||||||
import { QuickSearchButton } from '../../pure/quick-search-button';
|
import { QuickSearchButton } from '../../pure/quick-search-button';
|
||||||
import { EditorModeSwitch } from './editor-mode-switch';
|
import { EditorModeSwitch } from './editor-mode-switch';
|
||||||
import type { BaseHeaderProps } from './header';
|
import type { BaseHeaderProps } from './header';
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
import {
|
import * as styles from './styles.css';
|
||||||
StyledQuickSearchTipButton,
|
|
||||||
StyledQuickSearchTipContent,
|
|
||||||
StyledSearchArrowWrapper,
|
|
||||||
StyledSwitchWrapper,
|
|
||||||
StyledTitle,
|
|
||||||
StyledTitleContainer,
|
|
||||||
StyledTitleWrapper,
|
|
||||||
} from './styles';
|
|
||||||
|
|
||||||
export type WorkspaceHeaderProps = BaseHeaderProps;
|
export type WorkspaceHeaderProps = BaseHeaderProps;
|
||||||
|
|
||||||
export const WorkspaceHeader = forwardRef<
|
export const WorkspaceHeader: FC<
|
||||||
HTMLDivElement,
|
|
||||||
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
|
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
|
||||||
>((props, ref) => {
|
> = (props): ReactElement => {
|
||||||
const { workspace, currentPage, children, isPublic } = props;
|
const { workspace, currentPage, children, isPublic } = props;
|
||||||
// fixme(himself65): remove this atom and move it to props
|
// fixme(himself65): remove this atom and move it to props
|
||||||
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
||||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||||
meta => meta.id === currentPage?.id
|
meta => meta.id === currentPage?.id
|
||||||
);
|
);
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
assertExists(pageMeta);
|
assertExists(pageMeta);
|
||||||
const title = pageMeta.title;
|
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 (
|
return (
|
||||||
<Header ref={ref} {...props}>
|
<Header ref={headerRef} {...props}>
|
||||||
{children}
|
{children}
|
||||||
{!isPublic && currentPage && (
|
{!isPublic && currentPage && (
|
||||||
<StyledTitleContainer data-tauri-drag-region>
|
<div className={styles.titleContainer}>
|
||||||
<StyledTitleWrapper>
|
<div className={styles.titleWrapper}>
|
||||||
<StyledSwitchWrapper>
|
<div className={styles.switchWrapper}>
|
||||||
<EditorModeSwitch
|
<EditorModeSwitch
|
||||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||||
pageId={currentPage.id}
|
pageId={currentPage.id}
|
||||||
@@ -101,29 +45,21 @@ export const WorkspaceHeader = forwardRef<
|
|||||||
marginRight: '12px',
|
marginRight: '12px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledSwitchWrapper>
|
</div>
|
||||||
<StyledTitle>{title || 'Untitled'}</StyledTitle>
|
<div className={styles.title}>{title || 'Untitled'}</div>
|
||||||
<QuickSearchTips
|
|
||||||
data-testid="quick-search-tips"
|
<div className={styles.searchArrowWrapper}>
|
||||||
content={TipsContent}
|
<QuickSearchButton
|
||||||
placement="bottom"
|
onClick={() => {
|
||||||
popperRef={popperRef}
|
setOpenQuickSearch(true);
|
||||||
open={showQuickSearchTips}
|
}}
|
||||||
offset={[0, -5]}
|
/>
|
||||||
>
|
</div>
|
||||||
<StyledSearchArrowWrapper>
|
</div>
|
||||||
<QuickSearchButton
|
</div>
|
||||||
onClick={() => {
|
|
||||||
setOpenQuickSearch(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledSearchArrowWrapper>
|
|
||||||
</QuickSearchTips>
|
|
||||||
</StyledTitleWrapper>
|
|
||||||
</StyledTitleContainer>
|
|
||||||
)}
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
WorkspaceHeader.displayName = 'BlockSuiteEditorHeader';
|
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 { EditorContainer } from '@blocksuite/editor';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
@@ -11,7 +12,6 @@ import { startTransition, useCallback } from 'react';
|
|||||||
|
|
||||||
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
|
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
|
||||||
import type { AffineOfficialWorkspace } from '../shared';
|
import type { AffineOfficialWorkspace } from '../shared';
|
||||||
import { PageNotFoundError } from './affine/affine-error-eoundary';
|
|
||||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||||
import { WorkspaceHeader } from './blocksuite/workspace-header';
|
import { WorkspaceHeader } from './blocksuite/workspace-header';
|
||||||
|
|
||||||
@@ -84,6 +84,7 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
|
|||||||
page.workspace.setPageMeta(page.id, {
|
page.workspace.setPageMeta(page.id, {
|
||||||
updatedDate: Date.now(),
|
updatedDate: Date.now(),
|
||||||
});
|
});
|
||||||
|
localStorage.setItem('last_page_id', page.id);
|
||||||
onLoad?.(page, editor);
|
onLoad?.(page, editor);
|
||||||
},
|
},
|
||||||
[onLoad, setEditor]
|
[onLoad, setEditor]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MuiFade, Tooltip } from '@affine/component';
|
import { MuiFade, Tooltip } from '@affine/component';
|
||||||
import { getEnvironment } from '@affine/env';
|
import { config, getEnvironment } from '@affine/env';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { CloseIcon, NewIcon } from '@blocksuite/icons';
|
import { CloseIcon, NewIcon } from '@blocksuite/icons';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
@@ -74,10 +74,7 @@ export const HelpIsland = ({
|
|||||||
<StyledIconWrapper
|
<StyledIconWrapper
|
||||||
data-testid="right-bottom-change-log-icon"
|
data-testid="right-bottom-change-log-icon"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(
|
window.open(config.changelogUrl, '_blank');
|
||||||
'https://github.com/toeverything/AFFiNE/releases',
|
|
||||||
'_blank'
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NewIcon />
|
<NewIcon />
|
||||||
@@ -111,7 +108,10 @@ export const HelpIsland = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{showList.includes('guide') && (
|
{showList.includes('guide') && (
|
||||||
<Tooltip content={'Easy Guide'} placement="left-end">
|
<Tooltip
|
||||||
|
content={t['com.affine.helpIsland.gettingStarted']()}
|
||||||
|
placement="left-end"
|
||||||
|
>
|
||||||
<StyledIconWrapper
|
<StyledIconWrapper
|
||||||
data-testid="easy-guide"
|
data-testid="easy-guide"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { styled } from '@affine/component';
|
|
||||||
import { AffineLoading } from '@affine/component/affine-loading';
|
import { AffineLoading } from '@affine/component/affine-loading';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import { memo, Suspense } from 'react';
|
import { memo, Suspense } from 'react';
|
||||||
|
|
||||||
export const Loading = memo(function Loading() {
|
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')(() => {
|
* @deprecated use skeleton instead
|
||||||
return {
|
*/
|
||||||
height: '100vh',
|
export const PageLoading = () => {
|
||||||
display: 'flex',
|
return null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PageLoading;
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
FavoriteIcon,
|
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
@@ -24,11 +23,6 @@ export const useSwitchToConfig = (
|
|||||||
href: pathGenerator.all(workspaceId),
|
href: pathGenerator.all(workspaceId),
|
||||||
icon: FolderIcon,
|
icon: FolderIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t['Favorites'](),
|
|
||||||
href: pathGenerator.favorite(workspaceId),
|
|
||||||
icon: FavoriteIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t['Workspace Settings'](),
|
title: t['Workspace Settings'](),
|
||||||
href: pathGenerator.setting(workspaceId),
|
href: pathGenerator.setting(workspaceId),
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
|
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
@@ -109,7 +112,7 @@ export const WorkspaceListModal = ({
|
|||||||
items={
|
items={
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||||
) as (AffineWorkspace | LocalWorkspace)[]
|
) as (AffineLegacyCloudWorkspace | LocalWorkspace)[]
|
||||||
}
|
}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
currentWorkspaceId={currentWorkspaceId}
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
@@ -145,7 +148,7 @@ export const WorkspaceListModal = ({
|
|||||||
{environment.isDesktop && (
|
{environment.isDesktop && (
|
||||||
<Menu
|
<Menu
|
||||||
placement="auto"
|
placement="auto"
|
||||||
trigger={['click', 'hover']}
|
trigger={['click']}
|
||||||
zIndex={1000}
|
zIndex={1000}
|
||||||
content={
|
content={
|
||||||
<StyledCreateWorkspaceCardPillContainer>
|
<StyledCreateWorkspaceCardPillContainer>
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
|
|||||||
color: 'var(--affine-primary-color)',
|
color: 'var(--affine-primary-color)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'@media (max-width: 720px)': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const StyledSelectorContainer = styled('div')(() => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0 6px',
|
padding: '0 6px',
|
||||||
marginBottom: '16px',
|
margin: '0 -6px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: 'var(--affine-text-primary-color)',
|
color: 'var(--affine-text-primary-color)',
|
||||||
':hover': {
|
':hover': {
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
|
import { MenuItem } from '@affine/component/app-sidebar';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
|
||||||
import { StyledCollapseItem } from '../shared-styles';
|
|
||||||
|
|
||||||
export const EmptyItem = () => {
|
export const EmptyItem = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
return (
|
return (
|
||||||
<StyledCollapseItem disable={true} textWrap={true}>
|
<MenuItem disabled={true}>{t['Favorite pages for easy access']()}</MenuItem>
|
||||||
{t['Favorite pages for easy access']()}
|
|
||||||
</StyledCollapseItem>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1,107 @@
|
|||||||
import { MuiCollapse } from '@affine/component';
|
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
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 { useAtomValue } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { workspacePreferredModeAtom } from '../../../../atoms';
|
import { workspacePreferredModeAtom } from '../../../../atoms';
|
||||||
import type { FavoriteListProps } from '../index';
|
import type { FavoriteListProps } from '../index';
|
||||||
import { StyledCollapseItem } from '../shared-styles';
|
|
||||||
import EmptyItem from './empty-item';
|
import EmptyItem from './empty-item';
|
||||||
export const FavoriteList = ({
|
import * as styles from './styles.css';
|
||||||
pageMeta,
|
|
||||||
openPage,
|
interface FavoriteMenuItemProps {
|
||||||
showList,
|
workspace: Workspace;
|
||||||
}: FavoriteListProps) => {
|
pageId: string;
|
||||||
|
metaMapping: Record<string, PageMeta>;
|
||||||
|
parentIds: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FavoriteMenuItem({
|
||||||
|
workspace,
|
||||||
|
pageId,
|
||||||
|
metaMapping,
|
||||||
|
parentIds,
|
||||||
|
}: FavoriteMenuItemProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const record = useAtomValue(workspacePreferredModeAtom);
|
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;
|
||||||
|
const showReferences = collapsible ? !collapsed : referencesToShow.length > 0;
|
||||||
|
const nestedItem = parentIds.size > 0;
|
||||||
|
const untitled = !metaMapping[pageId]?.title;
|
||||||
|
return (
|
||||||
|
<div className={styles.favItemWrapper} data-nested={nestedItem}>
|
||||||
|
<MenuLinkItem
|
||||||
|
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 className={styles.label} data-untitled={untitled}>
|
||||||
|
{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])}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
|
||||||
|
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||||
|
|
||||||
const favoriteList = useMemo(
|
const favoriteList = useMemo(
|
||||||
() => pageMeta.filter(p => p.favorite && !p.trash),
|
() => metas.filter(p => p.favorite && !p.trash),
|
||||||
[pageMeta]
|
[metas]
|
||||||
|
);
|
||||||
|
|
||||||
|
const metaMapping = useMemo(
|
||||||
|
() =>
|
||||||
|
metas.reduce((acc, meta) => {
|
||||||
|
acc[meta.id] = meta;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, PageMeta>),
|
||||||
|
[metas]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiCollapse
|
<>
|
||||||
in={showList}
|
|
||||||
style={{
|
|
||||||
maxHeight: 300,
|
|
||||||
overflowY: 'auto',
|
|
||||||
marginLeft: '16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{favoriteList.map((pageMeta, index) => {
|
{favoriteList.map((pageMeta, index) => {
|
||||||
const active = router.query.pageId === pageMeta.id;
|
|
||||||
return (
|
return (
|
||||||
<div key={`${pageMeta}-${index}`}>
|
<FavoriteMenuItem
|
||||||
<StyledCollapseItem
|
key={`${pageMeta}-${index}`}
|
||||||
data-testid={`favorite-list-item-${pageMeta.id}`}
|
metaMapping={metaMapping}
|
||||||
active={active}
|
pageId={pageMeta.id}
|
||||||
ref={ref => {
|
// memo?
|
||||||
if (ref && active) {
|
parentIds={new Set()}
|
||||||
ref.scrollIntoView({ behavior: 'smooth' });
|
workspace={currentWorkspace.blockSuiteWorkspace}
|
||||||
}
|
/>
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openPage(pageMeta.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{record[pageMeta.id] === 'edgeless' ? (
|
|
||||||
<EdgelessIcon />
|
|
||||||
) : (
|
|
||||||
<PageIcon />
|
|
||||||
)}
|
|
||||||
<span>{pageMeta.title || 'Untitled'}</span>
|
|
||||||
</StyledCollapseItem>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{favoriteList.length === 0 && <EmptyItem />}
|
{favoriteList.length === 0 && <EmptyItem />}
|
||||||
</MuiCollapse>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
export * from './favorite-list';
|
||||||
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;
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const label = style({
|
||||||
|
selectors: {
|
||||||
|
'&[data-untitled="true"]': {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const favItemWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px',
|
||||||
|
selectors: {
|
||||||
|
'&[data-nested="true"]': {
|
||||||
|
marginLeft: '12px',
|
||||||
|
width: 'calc(100% - 12px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
|
|
||||||
export type FavoriteListProps = {
|
export type FavoriteListProps = {
|
||||||
currentPageId: string | null;
|
currentWorkspace: AllWorkspace;
|
||||||
openPage: (pageId: string) => void;
|
|
||||||
showList: boolean;
|
|
||||||
pageMeta: PageMeta[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkSpaceSliderBarProps = {
|
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 { openQuickSearchModalAtom } from '../../../atoms';
|
||||||
import type { HeaderProps } from '../../blocksuite/workspace-header/header';
|
import type { HeaderProps } from '../../blocksuite/workspace-header/header';
|
||||||
import { Header } 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';
|
import { QuickSearchButton } from '../quick-search-button';
|
||||||
|
|
||||||
export type WorkspaceTitleProps = React.PropsWithChildren<
|
export type WorkspaceTitleProps = React.PropsWithChildren<
|
||||||
@@ -22,15 +22,15 @@ export const WorkspaceTitle: React.FC<WorkspaceTitleProps> = ({
|
|||||||
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
||||||
return (
|
return (
|
||||||
<Header {...props}>
|
<Header {...props}>
|
||||||
<StyledPageListTittleWrapper>
|
<div className={styles.pageListTitleWrapper}>
|
||||||
{icon}
|
<div className={styles.pageListTitleIcon}>{icon}</div>
|
||||||
{children}
|
{children}
|
||||||
<QuickSearchButton
|
<QuickSearchButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenQuickSearch(true);
|
setOpenQuickSearch(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledPageListTittleWrapper>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
AddPageButton,
|
||||||
AppSidebar,
|
AppSidebar,
|
||||||
appSidebarOpenAtom,
|
appSidebarOpenAtom,
|
||||||
ResizeIndicator,
|
AppUpdaterButton,
|
||||||
|
CategoryDivider,
|
||||||
|
MenuLinkItem,
|
||||||
|
QuickSearchInput,
|
||||||
|
SidebarContainer,
|
||||||
|
SidebarScrollableContainer,
|
||||||
} from '@affine/component/app-sidebar';
|
} from '@affine/component/app-sidebar';
|
||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -9,27 +15,18 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
|||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
PlusIcon,
|
|
||||||
SearchIcon,
|
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import type { ReactElement, UIEvent } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import type React 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 type { AllWorkspace } from '../../shared';
|
||||||
import ChangeLog from '../pure/workspace-slider-bar/changeLog';
|
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||||
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 { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
||||||
|
|
||||||
export type RootAppSidebarProps = {
|
export type RootAppSidebarProps = {
|
||||||
@@ -37,19 +34,37 @@ export type RootAppSidebarProps = {
|
|||||||
onOpenQuickSearchModal: () => void;
|
onOpenQuickSearchModal: () => void;
|
||||||
onOpenWorkspaceListModal: () => void;
|
onOpenWorkspaceListModal: () => void;
|
||||||
currentWorkspace: AllWorkspace | null;
|
currentWorkspace: AllWorkspace | null;
|
||||||
currentPageId: string | null;
|
|
||||||
openPage: (pageId: string) => void;
|
openPage: (pageId: string) => void;
|
||||||
createPage: () => Page;
|
createPage: () => Page;
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
paths: {
|
paths: {
|
||||||
all: (workspaceId: string) => string;
|
all: (workspaceId: string) => string;
|
||||||
favorite: (workspaceId: string) => string;
|
|
||||||
trash: (workspaceId: string) => string;
|
trash: (workspaceId: string) => string;
|
||||||
setting: (workspaceId: string) => string;
|
setting: (workspaceId: string) => string;
|
||||||
shared: (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 is for the whole affine app sidebar.
|
||||||
* This component wraps the app sidebar in `@affine/component` with logic and data.
|
* This component wraps the app sidebar in `@affine/component` with logic and data.
|
||||||
@@ -58,7 +73,6 @@ export type RootAppSidebarProps = {
|
|||||||
*/
|
*/
|
||||||
export const RootAppSidebar = ({
|
export const RootAppSidebar = ({
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
currentPageId,
|
|
||||||
openPage,
|
openPage,
|
||||||
createPage,
|
createPage,
|
||||||
currentPath,
|
currentPath,
|
||||||
@@ -69,171 +83,118 @@ export const RootAppSidebar = ({
|
|||||||
const currentWorkspaceId = currentWorkspace?.id || null;
|
const currentWorkspaceId = currentWorkspace?.id || null;
|
||||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
|
|
||||||
const onClickNewPage = useCallback(async () => {
|
const onClickNewPage = useCallback(async () => {
|
||||||
const page = await createPage();
|
const page = await createPage();
|
||||||
openPage(page.id);
|
openPage(page.id);
|
||||||
}, [createPage, openPage]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (environment.isDesktop && typeof sidebarOpen === 'boolean') {
|
if (environment.isDesktop && typeof sidebarOpen === 'boolean') {
|
||||||
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen);
|
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen);
|
||||||
}
|
}
|
||||||
}, [sidebarOpen]);
|
}, [sidebarOpen]);
|
||||||
const [ref, setRef] = useState<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const handleQuickSearchButtonKeyDown = useCallback(
|
useEffect(() => {
|
||||||
(e: React.KeyboardEvent) => {
|
const keydown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if ((e.key === '/' && e.metaKey) || (e.key === '/' && e.ctrlKey)) {
|
||||||
e.preventDefault();
|
setSidebarOpen(!sidebarOpen);
|
||||||
onOpenQuickSearchModal();
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppSidebar
|
<AppSidebar router={router}>
|
||||||
ref={setRef}
|
<SidebarContainer>
|
||||||
footer={
|
|
||||||
<StyledNewPageButton
|
|
||||||
data-testid="new-page-button"
|
|
||||||
onClick={onClickNewPage}
|
|
||||||
>
|
|
||||||
<PlusIcon /> {t['New Page']()}
|
|
||||||
</StyledNewPageButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
|
|
||||||
<WorkspaceSelector
|
<WorkspaceSelector
|
||||||
currentWorkspace={currentWorkspace}
|
currentWorkspace={currentWorkspace}
|
||||||
onClick={onOpenWorkspaceListModal}
|
onClick={onOpenWorkspaceListModal}
|
||||||
/>
|
/>
|
||||||
<ChangeLog />
|
<QuickSearchInput
|
||||||
<StyledListItem
|
|
||||||
data-testid="slider-bar-quick-search-button"
|
data-testid="slider-bar-quick-search-button"
|
||||||
onClick={useCallback(() => {
|
onClick={onOpenQuickSearchModal}
|
||||||
onOpenQuickSearchModal();
|
/>
|
||||||
}, [onOpenQuickSearchModal])}
|
<RouteMenuLinkItem
|
||||||
onKeyDown={handleQuickSearchButtonKeyDown}
|
icon={<FolderIcon />}
|
||||||
|
currentPath={currentPath}
|
||||||
|
path={currentWorkspaceId && paths.all(currentWorkspaceId)}
|
||||||
>
|
>
|
||||||
<div
|
<span data-testid="all-pages">{t['All pages']()}</span>
|
||||||
role="button"
|
</RouteMenuLinkItem>
|
||||||
tabIndex={0}
|
<RouteMenuLinkItem
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
{t['Quick search']()}
|
|
||||||
</div>
|
|
||||||
</StyledListItem>
|
|
||||||
<StyledListItem
|
|
||||||
active={
|
|
||||||
currentPath ===
|
|
||||||
(currentWorkspaceId && paths.setting(currentWorkspaceId))
|
|
||||||
}
|
|
||||||
data-testid="slider-bar-workspace-setting-button"
|
data-testid="slider-bar-workspace-setting-button"
|
||||||
style={{
|
icon={<SettingsIcon />}
|
||||||
marginBottom: '16px',
|
currentPath={currentPath}
|
||||||
}}
|
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
|
||||||
>
|
>
|
||||||
<StyledLink
|
<span data-testid="settings">{t['Settings']()}</span>
|
||||||
href={{
|
</RouteMenuLinkItem>
|
||||||
pathname:
|
</SidebarContainer>
|
||||||
currentWorkspaceId && paths.setting(currentWorkspaceId),
|
|
||||||
}}
|
<SidebarScrollableContainer>
|
||||||
>
|
<CategoryDivider label={t['Favorites']()} />
|
||||||
<SettingsIcon />
|
{blockSuiteWorkspace && (
|
||||||
<div>{t['Workspace Settings']()}</div>
|
<FavoriteList currentWorkspace={currentWorkspace} />
|
||||||
</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>
|
|
||||||
{config.enableLegacyCloud &&
|
{config.enableLegacyCloud &&
|
||||||
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
|
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
|
||||||
currentWorkspace.public ? (
|
currentWorkspace.public ? (
|
||||||
<StyledListItem>
|
<RouteMenuLinkItem
|
||||||
<StyledLink
|
icon={<ShareIcon />}
|
||||||
href={{
|
currentPath={currentPath}
|
||||||
pathname:
|
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
|
||||||
currentWorkspaceId && paths.setting(currentWorkspaceId),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShareIcon />
|
|
||||||
<span data-testid="Published-to-web">Published to web</span>
|
|
||||||
</StyledLink>
|
|
||||||
</StyledListItem>
|
|
||||||
) : (
|
|
||||||
<StyledListItem
|
|
||||||
active={
|
|
||||||
currentPath ===
|
|
||||||
(currentWorkspaceId && paths.shared(currentWorkspaceId))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<StyledLink
|
<span data-testid="Published-to-web">Published to web</span>
|
||||||
href={{
|
</RouteMenuLinkItem>
|
||||||
pathname:
|
) : (
|
||||||
currentWorkspaceId && paths.shared(currentWorkspaceId),
|
<RouteMenuLinkItem
|
||||||
}}
|
icon={<ShareIcon />}
|
||||||
>
|
currentPath={currentPath}
|
||||||
<ShareIcon />
|
path={currentWorkspaceId && paths.shared(currentWorkspaceId)}
|
||||||
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
|
>
|
||||||
</StyledLink>
|
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
|
||||||
</StyledListItem>
|
</RouteMenuLinkItem>
|
||||||
))}
|
))}
|
||||||
<StyledListItem
|
|
||||||
active={
|
<CategoryDivider label={t['others']()} />
|
||||||
currentPath ===
|
<RouteMenuLinkItem
|
||||||
(currentWorkspaceId && paths.trash(currentWorkspaceId))
|
icon={<DeleteTemporarilyIcon />}
|
||||||
}
|
currentPath={currentPath}
|
||||||
|
path={currentWorkspaceId && paths.trash(currentWorkspaceId)}
|
||||||
>
|
>
|
||||||
<StyledLink
|
<span data-testid="trash-page">{t['Trash']()}</span>
|
||||||
href={{
|
</RouteMenuLinkItem>
|
||||||
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
|
</SidebarScrollableContainer>
|
||||||
}}
|
<SidebarContainer>
|
||||||
>
|
{environment.isDesktop && <AppUpdaterButton />}
|
||||||
<DeleteTemporarilyIcon /> {t['Trash']()}
|
<div />
|
||||||
</StyledLink>
|
<AddPageButton onClick={onClickNewPage} />
|
||||||
</StyledListItem>
|
</SidebarContainer>
|
||||||
</StyledSliderBarInnerWrapper>
|
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
<ResizeIndicator targetElement={ref} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import 'fake-indexeddb/auto';
|
|||||||
|
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
|
|
||||||
import {
|
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||||
rootCurrentWorkspaceIdAtom,
|
|
||||||
rootWorkspacesMetadataAtom,
|
|
||||||
} from '@affine/workspace/atom';
|
|
||||||
import type { LocalWorkspace } from '@affine/workspace/type';
|
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
@@ -21,23 +17,17 @@ import {
|
|||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { createStore, Provider } from 'jotai';
|
import { createStore, Provider } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import routerMock from 'next-router-mock';
|
import routerMock from 'next-router-mock';
|
||||||
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import { currentWorkspaceIdAtom, workspacesAtom } from '../../atoms';
|
import { workspacesAtom } from '../../atoms';
|
||||||
import { LocalPlugin } from '../../plugins/local';
|
|
||||||
import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared';
|
import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared';
|
||||||
import {
|
import {
|
||||||
currentWorkspaceAtom,
|
currentWorkspaceAtom,
|
||||||
useCurrentWorkspace,
|
useCurrentWorkspace,
|
||||||
} from '../current/use-current-workspace';
|
} from '../current/use-current-workspace';
|
||||||
import {
|
|
||||||
useRecentlyViewed,
|
|
||||||
useSyncRecentViewsWithRouter,
|
|
||||||
} from '../use-recent-views';
|
|
||||||
import { useAppHelper, useWorkspaces } from '../use-workspaces';
|
import { useAppHelper, useWorkspaces } from '../use-workspaces';
|
||||||
|
|
||||||
vi.mock(
|
vi.mock(
|
||||||
@@ -202,6 +192,8 @@ describe('useWorkspaces', () => {
|
|||||||
const { result } = renderHook(() => useAppHelper(), {
|
const { result } = renderHook(() => useAppHelper(), {
|
||||||
wrapper: ProviderWrapper,
|
wrapper: ProviderWrapper,
|
||||||
});
|
});
|
||||||
|
// next tick
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
{
|
{
|
||||||
const workspaces = await store.get(workspacesAtom);
|
const workspaces = await store.get(workspacesAtom);
|
||||||
expect(workspaces.length).toEqual(1);
|
expect(workspaces.length).toEqual(1);
|
||||||
@@ -221,58 +213,3 @@ describe('useWorkspaces', () => {
|
|||||||
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
|
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([
|
createDynamicRouteParser([
|
||||||
'/workspace/[workspaceId]/[pageId]',
|
'/workspace/[workspaceId]/[pageId]',
|
||||||
'/workspace/[workspaceId]/all',
|
'/workspace/[workspaceId]/all',
|
||||||
'/workspace/[workspaceId]/favorite',
|
|
||||||
'/workspace/[workspaceId]/trash',
|
'/workspace/[workspaceId]/trash',
|
||||||
'/workspace/[workspaceId]/setting',
|
'/workspace/[workspaceId]/setting',
|
||||||
'/workspace/[workspaceId]/shared',
|
'/workspace/[workspaceId]/shared',
|
||||||
@@ -54,19 +53,6 @@ describe('useRouterHelper', () => {
|
|||||||
// routerHook.result.current.back()
|
// routerHook.result.current.back()
|
||||||
// routerHook.rerender()
|
// routerHook.rerender()
|
||||||
// expect(routerHook.result.current.pathname).toBe('/')
|
// 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 () => {
|
test('should jump to the expected page', async () => {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { WorkspacePlugins } from '../../plugins';
|
import { WorkspaceAdapters } from '../../plugins';
|
||||||
|
|
||||||
export function useAffineLogIn() {
|
export function useAffineLogIn() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return useCallback(async () => {
|
return useCallback(async () => {
|
||||||
await WorkspacePlugins[WorkspaceFlavour.AFFINE].Events[
|
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
|
||||||
'workspace:access'
|
'workspace:access'
|
||||||
]?.();
|
]?.();
|
||||||
// todo: remove reload page requirement
|
// todo: remove reload page requirement
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user