Merge branch 'canary' into stable

This commit is contained in:
李华桥
2023-12-10 21:04:15 +08:00
399 changed files with 10365 additions and 4747 deletions
+10 -41
View File
@@ -29,6 +29,13 @@ runs:
env:
CARGO_INCREMENTAL: '1'
- name: Set CC
if: ${{ contains(inputs.target, 'linux') && inputs.package != '@affine/native' }}
shell: bash
run: |
echo "CC=clang" >> "$GITHUB_ENV"
echo "TARGET_CC=clang" >> "$GITHUB_ENV"
- name: Cache cargo
uses: actions/cache@v3
with:
@@ -36,51 +43,13 @@ runs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
.cargo-cache
~/.napi-rs
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 ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }} --use-napi-cross
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
- 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 -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 -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: |
export CC=x86_64-unknown-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc
rm -rf /usr/local/rustup/downloads/*
rustup target add x86_64-unknown-linux-gnu
export RUSTFLAGS="-C debuginfo=1"
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
if [ -d "node_modules/.cache" ]; then
chmod -R 777 node_modules/.cache
fi
if [ -d "target" ]; then
chmod -R 777 target;
fi
- 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 -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 -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: |
export RUSTFLAGS="-C debuginfo=1"
rm -rf /usr/local/rustup/downloads/*
rustup target add aarch64-unknown-linux-gnu
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
if [ -d "node_modules/.cache" ]; then
chmod -R 777 node_modules/.cache
fi
if [ -d "target" ]; then
chmod -R 777 target;
fi
DEBUG: 'napi:*'
+2 -2
View File
@@ -26,7 +26,7 @@ runs:
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
- uses: azure/setup-helm@v3
- id: auth
uses: google-github-actions/auth@v1
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/${{ inputs.gcp-project-number }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy'
service_account: '${{ inputs.service-account }}'
@@ -34,7 +34,7 @@ runs:
project_id: '${{ inputs.gcp-project-id }}'
- name: 'Setup gcloud cli'
uses: 'google-github-actions/setup-gcloud@v1'
uses: 'google-github-actions/setup-gcloud@v2'
with:
install_components: 'gke-gcloud-auth-plugin'
+3 -1
View File
@@ -1,6 +1,7 @@
import { execSync } from 'node:child_process';
const {
APP_VERSION,
BUILD_TYPE,
DEPLOY_HOST,
CANARY_DEPLOY_HOST,
@@ -79,6 +80,7 @@ const createHelmCommand = ({ isDryRun }) => {
`--set global.ingress.enabled=true`,
`--set-json global.ingress.annotations=\"{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${staticIpName}\\" }\"`,
`--set-string global.ingress.host="${host}"`,
`--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres,
`--set web.replicaCount=${webReplicaCount}`,
`--set-string web.image.tag="${imageTag}"`,
@@ -105,7 +107,7 @@ const createHelmCommand = ({ isDryRun }) => {
`--set sync.replicaCount=${syncReplicaCount}`,
`--set-string sync.image.tag="${imageTag}"`,
...serviceAnnotations,
`--version "0.0.0-${buildType}.${GIT_SHORT_HASH}" --timeout 10m`,
`--timeout 10m`,
flag,
].join(' ');
return deployCommand;
+22
View File
@@ -0,0 +1,22 @@
name: 'Download core artifacts'
description: 'Download core artifacts and extract to dist'
inputs:
path:
description: 'Path to extract'
required: true
runs:
using: 'composite'
steps:
- name: Download tar.gz
uses: actions/download-artifact@v3
with:
name: core
path: .
- name: Extract core artifacts
shell: bash
run: |
mkdir -p ${{ inputs.path }}
tar -xvf dist.tar.gz --directory ${{ inputs.path }}
rm dist.tar.gz
+53 -7
View File
@@ -36,6 +36,9 @@ inputs:
description: 'Set enableScripts in .yarnrc.yml'
required: false
default: 'true'
full-cache:
description: 'Full installation cache'
required: false
runs:
using: 'composite'
@@ -46,7 +49,6 @@ runs:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
cache: 'yarn'
- name: Set nmMode
if: ${{ inputs.hard-link-nm == 'false' }}
@@ -63,6 +65,48 @@ runs:
shell: bash
run: yarn config set enableScripts false
- name: Set yarn global cache path
shell: bash
id: yarn-cache
run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT
- name: Cache non-full yarn cache on Linux
uses: actions/cache@v3
if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }}
with:
path: |
node_modules
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-${{ github.job }}-${{ runner.os }}
# The network performance on macOS is very poor
# and the decompression performance on Windows is very terrible
# so we reduce the number of cached files on non-Linux systems by remove node_modules from cache path.
- name: Cache non-full yarn cache on non-Linux
uses: actions/cache@v3
if: ${{ inputs.full-cache != 'true' && runner.os != 'Linux' }}
with:
path: |
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-${{ github.job }}-${{ runner.os }}
- name: Cache full yarn cache on Linux
uses: actions/cache@v3
if: ${{ inputs.full-cache == 'true' && runner.os == 'Linux' }}
with:
path: |
node_modules
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-full-${{ runner.os }}
- name: Cache full yarn cache on non-Linux
uses: actions/cache@v3
if: ${{ inputs.full-cache == 'true' && runner.os != 'Linux' }}
with:
path: |
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-full-${{ runner.os }}
- name: yarn install
if: ${{ inputs.package-install == 'true' }}
continue-on-error: true
@@ -102,8 +146,8 @@ runs:
id: playwright-cache
if: ${{ inputs.playwright-install == 'true' }}
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}'
path: ${{ github.workspace }}/node_modules/.cache/ms-playwright
key: '${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}'
# As a fallback, if the Playwright version has changed, try use the
# most recently cached version. There's a good chance that at least one
# of the browser binary versions haven't been updated, so Playwright can
@@ -113,7 +157,7 @@ runs:
# date cache, but still let Playwright decide if it needs to download
# new binaries or not.
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-playwright-
${{ runner.os }}-playwright-
# If the Playwright browser binaries weren't able to be restored, we tell
# playwright to install everything for us.
@@ -121,6 +165,8 @@ runs:
shell: bash
if: inputs.playwright-install == 'true'
run: yarn playwright install --with-deps chromium
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
- name: Get installed Electron version
id: electron-version
@@ -134,16 +180,16 @@ runs:
if: ${{ inputs.electron-install == 'true' }}
with:
path: 'node_modules/.cache/electron'
key: '${{ runner.os }}-{{ runner.arch }}-electron-${{ steps.electron-version.outputs.version }}'
key: '${{ runner.os }}-electron-${{ steps.electron-version.outputs.version }}'
restore-keys: |
${{ runner.os }}-{{ runner.arch }}-electron-
${{ runner.os }}-electron-
- name: Install Electron binary
shell: bash
if: inputs.electron-install == 'true'
run: node ./node_modules/electron/install.js
env:
ELECTRON_OVERRIDE_DIST_PATH: ./node_modules/.cache/electron
electron_config_cache: ./node_modules/.cache/electron
- name: Build Infra
shell: bash
-31
View File
@@ -1,31 +0,0 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
groups:
all-npm-dependencies:
patterns:
- '*'
schedule:
interval: 'weekly'
versioning-strategy: increase
commit-message:
prefix: 'chore'
- package-ecosystem: 'cargo'
directory: '/'
schedule:
interval: 'weekly'
versioning-strategy: auto
commit-message:
prefix: 'chore'
groups:
all-cargo-dependencies:
patterns:
- '*'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
commit-message:
prefix: 'ci'
+3 -3
View File
@@ -1,6 +1,6 @@
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 12.5.8
digest: sha256:c91c0dc1370e879538dc9d6e435e731a726ef99d6a3b081372318483792b48a7
generated: "2023-06-27T18:34:12.683806+08:00"
version: 13.2.23
digest: sha256:5b64538509bd067bb0f67bf082847a2c5d66dc37d0b9d7948a40405d9c446400
generated: "2023-12-05T03:04:57.997927753Z"
+1 -1
View File
@@ -8,5 +8,5 @@ appVersion: '0.6.1'
dependencies:
- name: postgresql
version: 12.5.8
version: 13.2.23
repository: https://charts.bitnami.com/bitnami
+1 -1
View File
@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: '0.7.0-canary.18'
appVersion: "0.10.3-canary.2"
@@ -3,4 +3,4 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: '0.7.0-canary.18'
appVersion: "0.10.3-canary.2"
+1 -1
View File
@@ -3,4 +3,4 @@ name: sync
description: A Helm chart for Kubernetes
type: application
version: 0.0.0
appVersion: "0.7.0-canary.18"
appVersion: "0.10.3-canary.2"
+85 -32
View File
@@ -1,62 +1,115 @@
docs:
- 'docs/**/*'
- '**/README.md'
- 'packages/frontend/templates/**/*'
- changed-files:
- any-glob-to-any-file:
- 'docs/**/*'
- '**/README.md'
- 'packages/frontend/templates/**/*'
test:
- 'tests/**/*'
- '**/tests/**/*'
- '**/__tests__/**/*'
- changed-files:
- any-glob-to-any-file:
- 'tests/**/*'
- '**/tests/**/*'
- '**/__tests__/**/*'
mod:dev:
- 'scripts/**/*'
- 'tools/cli/**/*'
- 'packages/common/debug/**/*'
- changed-files:
- any-glob-to-any-file:
- 'scripts/**/*'
- 'tools/cli/**/*'
- 'packages/common/debug/**/*'
mod:plugin:
- 'packages/plugins/**/*'
- changed-files:
- any-glob-to-any-file:
- 'packages/plugins/**/*'
plugin:copilot:
- 'packages/plugins/copilot/**/*'
- changed-files:
- any-glob-to-any-file:
- 'packages/plugins/copilot/**/*'
mod:infra:
- 'packages/common/infra/**/*'
- changed-files:
- any-glob-to-any-file:
- 'packages/common/infra/**/*'
mod:sdk:
- 'packages/common/sdk/**/*'
- changed-files:
- any-glob-to-any-file:
- 'packages/common/sdk/**/*'
mod:plugin-cli:
- 'tools/plugin-cli/**/*'
- changed-files:
- any-glob-to-any-file:
- 'tools/plugin-cli/**/*'
mod:workspace: 'packages/frontend/workspace/**/*'
mod:workspace:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/workspace/**/*'
mod:i18n: 'packages/frontend/i18n/**/*'
mod:i18n:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/i18n/**/*'
mod:env: 'packages/common/env/**/*'
mod:env:
- changed-files:
- any-glob-to-any-file:
- 'packages/common/env/**/*'
mod:hooks: 'packages/frontend/hooks/**/*'
mod:hooks:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/hooks/**/*'
mod:component: 'packages/frontend/component/**/*'
mod:component:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/component/**/*'
mod:storage: 'packages/backend/storage/**/*'
mod:storage:
- changed-files:
- any-glob-to-any-file:
- 'packages/backend/storage/**/*'
mod:native: 'packages/frontend/native/**/*'
mod:native:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/native/**/*'
mod:store:
- '**/atoms/**/*'
- changed-files:
- any-glob-to-any-file:
- '**/atoms/**/*'
rust:
- '**/*.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain'
- '**/rust-toolchain.toml'
- '**/rustfmt.toml'
- changed-files:
- any-glob-to-any-file:
- '**/*.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain'
- '**/rust-toolchain.toml'
- '**/rustfmt.toml'
package:y-indexeddb: 'packages/common/y-indexeddb/**/*'
package:y-indexeddb:
- changed-files:
- any-glob-to-any-file:
- 'packages/common/y-indexeddb/**/*'
app:core: 'packages/frontend/core/**/*'
app:core:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/core/**/*'
app:electron: 'packages/frontend/electron/**/*'
app:electron:
- changed-files:
- any-glob-to-any-file:
- 'packages/frontend/electron/**/*'
app:server: 'packages/backend/server/**/*'
app:server:
- changed-files:
- any-glob-to-any-file:
- 'packages/backend/server/**/*'
+53
View File
@@ -0,0 +1,53 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"group:allNonMajor",
":preserveSemverRanges",
":disablePeerDependencies"
],
"labels": ["dependencies"],
"packageRules": [
{
"matchPackageNames": ["napi", "napi-build", "napi-derive"],
"groupName": "napi-rs"
},
{
"matchPackagePatterns": ["^eslint", "^@typescript-eslint"],
"groupName": "linter"
},
{
"matchPackagePatterns": ["^@nestjs"],
"groupName": "nestjs"
},
{
"matchPackagePatterns": ["^@opentelemetry"],
"groupName": "opentelemetry"
},
{
"matchPackageNames": [
"@prisma/client",
"@prisma/instrumentation",
"prisma"
],
"groupName": "prisma"
},
{
"matchPackagePatterns": ["^@electron-forge"],
"groupName": "electron-forge"
},
{
"matchPackagePatterns": ["^@blocksuite"],
"excludePackageNames": ["@blocksuite/icons"],
"followTag": "nightly"
}
],
"commitMessagePrefix": "chore: ",
"commitMessageAction": "bump up",
"commitMessageTopic": "{{depName}} version",
"ignoreDeps": [],
"lockFileMaintenance": {
"enabled": true,
"extends": ["schedule:weekly"]
}
}
+2 -1
View File
@@ -9,4 +9,5 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/checkout@v4
- uses: actions/labeler@v5
-190
View File
@@ -1,190 +0,0 @@
name: Build(Desktop) & Test
on:
push:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-desktop.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-desktop.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
DISTRIBUTION: desktop
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:
name: core
path: ./packages/frontend/core/dist
if-no-files-found: error
build-native:
name: Build Native
runs-on: ubuntu-latest
needs: build-core
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Run tests
run: yarn test
working-directory: ./packages/frontend/native
desktop-test:
name: Desktop Test
runs-on: ${{ matrix.spec.os }}
strategy:
fail-fast: false
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: true,
}
- {
os: macos-latest,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: false,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
test: true,
}
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
needs: build-core
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop
playwright-install: true
hard-link-nm: false
enableScripts: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn vitest
working-directory: packages/frontend/electron
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: packages/frontend/electron/resources/web-static
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine-test/affine-desktop e2e
env:
COVERAGE: true
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
HOIST_NODE_MODULES: 1
run: yarn workspace @affine/electron package --platform=darwin --arch=arm64
- name: Output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
yarn workspace @affine/electron ts-node ./scripts/macos-arm64-output-check.ts
- name: Collect code coverage report
if: ${{ matrix.spec.test }}
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
if: ${{ matrix.spec.test }}
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
-311
View File
@@ -1,311 +0,0 @@
name: Build(Server) & Test
on:
push:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-server.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-server.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
DISTRIBUTION: browser
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
build-storage:
name: Build Storage
runs-on: ubuntu-latest
env:
RUSTFLAGS: '-C debuginfo=1'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/storage
electron-install: false
build-infra: false
build-plugins: false
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/storage'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/backend/storage/storage.node
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec ts-node ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./packages/backend/server
- name: Run server tests
run: yarn workspace @affine/server test:coverage
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-e2e-test:
name: Server E2E Test
runs-on: ubuntu-latest
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec ts-node ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./packages/backend/server
- name: Run playwright tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-cloud e2e --forbid-only
env:
COVERAGE: true
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: server-e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore
server-desktop-e2e-test:
name: Server Desktop E2E Test
runs-on: ubuntu-latest
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec ts-node ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./packages/backend/server
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build:dev
- name: Run playwright tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" yarn workspace @affine-test/affine-desktop-cloud e2e
env:
COVERAGE: true
DEV_SERVER_URL: http://localhost:8080
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
ENABLE_LOCAL_EMAIL: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: server-e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore
+599
View File
@@ -0,0 +1,599 @@
name: Build & Test
on:
push:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
pull_request:
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
AFFINE_ENV: dev
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript', 'typescript']
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run oxlint
# oxlint is fast, so wrong code will fail quickly
run: yarn dlx $(node -e "console.log(require('./package.json').scripts['lint:ox'])")
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Run i18n codegen
run: yarn i18n-codegen gen
- name: Run ESLint
run: yarn lint:eslint --max-warnings=0
- name: Run Prettier
# Set nmMode in `actions/setup-node` will modify the .yarnrc.yml
run: |
git checkout .yarnrc.yml
yarn lint:prettier
- name: Run Type Check
run: yarn typecheck
check-yarn-binary:
name: Check yarn binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run check
run: |
yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
git diff --exit-code
e2e-plugin-test:
name: E2E Plugin Test
runs-on: ubuntu-latest
env:
DISTRIBUTION: browser
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
full-cache: true
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-plugin
env:
COVERAGE: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2e-plugin-test
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-plugin
path: ./test-results
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
env:
DISTRIBUTION: browser
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
full-cache: true
- name: Run playwright tests
run: yarn workspace @affine-test/affine-local e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
e2e-migration-test:
name: E2E Migration Test
runs-on: ubuntu-latest
env:
DISTRIBUTION: browser
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
full-cache: true
- name: Run playwright tests
run: yarn workspace @affine-test/affine-migration e2e --forbid-only
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-migration
path: ./tests/affine-migration/test-results
if-no-files-found: ignore
unit-test:
name: Unit Test
runs-on: ubuntu-latest
needs:
- build-native
env:
DISTRIBUTION: browser
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Download affine.linux-x64-gnu.node
uses: actions/download-artifact@v3
with:
name: affine.linux-x64-gnu.node
path: ./packages/frontend/native
- name: Unit Test
run: yarn nx test:coverage @affine/monorepo
- name: Upload unit test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/store/lcov.info
flags: unittest
name: affine
fail_ci_if_error: false
build-native:
name: Build AFFiNE native (${{ matrix.spec.target }})
runs-on: ${{ matrix.spec.os }}
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
strategy:
fail-fast: false
matrix:
spec:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: windows-latest, target: x86_64-pc-windows-msvc }
- { os: macos-latest, target: x86_64-apple-darwin }
- { os: macos-latest, target: aarch64-apple-darwin }
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/native
electron-install: false
build-infra: false
build-plugins: false
- name: Setup filename
id: filename
shell: bash
run: |
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload ${{ steps.filename.outputs.filename }}
uses: actions/upload-artifact@v3
with:
name: ${{ steps.filename.outputs.filename }}
path: ./packages/frontend/native/${{ steps.filename.outputs.filename }}
if-no-files-found: error
build-storage:
name: Build Storage
runs-on: ubuntu-latest
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/storage
electron-install: false
build-infra: false
build-plugins: false
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/storage'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/backend/storage/storage.node
if-no-files-found: error
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
build-plugins: false
full-cache: true
- name: Build Core
# always skip cache because its fast, and cache configuration is always changing
run: yarn nx build @affine/core --skip-nx-cache
- name: zip core
run: tar -czf dist.tar.gz --directory=packages/frontend/core/dist .
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:
name: core
path: dist.tar.gz
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
needs: build-storage
env:
DISTRIBUTION: browser
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec node --loader ts-node/esm/transpile-only ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./packages/backend/server
- name: Run server tests
run: yarn workspace @affine/server test:coverage
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-e2e-test:
name: ${{ matrix.tests.name }}
runs-on: ubuntu-latest
env:
DISTRIBUTION: browser
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
strategy:
fail-fast: false
matrix:
tests:
- name: 'Server E2E Test 1/3'
script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=1/3
- name: 'Server E2E Test 2/3'
script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=2/3
- name: 'Server E2E Test 3/3'
script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=3/3
- name: 'Server Desktop E2E Test'
script: |
yarn workspace @affine/electron build:dev
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop-cloud e2e
needs:
- build-storage
- build-native
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
hard-link-nm: false
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec node --loader ts-node/esm/transpile-only ./scripts/init-db.ts
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./packages/backend/server
- name: Download affine.linux-x64-gnu.node
uses: actions/download-artifact@v3
with:
name: affine.linux-x64-gnu.node
path: ./packages/frontend/native
- name: ${{ matrix.tests.name }}
run: |
${{ matrix.tests.script }}
env:
DEV_SERVER_URL: http://localhost:8080
ENABLE_LOCAL_EMAIL: true
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore
desktop-test:
name: Desktop Test (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
runs-on: ${{ matrix.spec.os }}
strategy:
fail-fast: false
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: true,
}
- {
os: macos-latest,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: false,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
target: x86_64-unknown-linux-gnu,
test: true,
}
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
needs:
- build-core
- build-native
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop
playwright-install: true
hard-link-nm: false
enableScripts: false
- name: Setup filename
id: filename
shell: bash
run: |
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
- name: Download ${{ steps.filename.outputs.filename }}
uses: actions/download-artifact@v3
with:
name: ${{ steps.filename.outputs.filename }}
path: ./packages/frontend/native
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn vitest
working-directory: packages/frontend/electron
- name: Download core artifact
uses: ./.github/actions/download-core
with:
path: packages/frontend/electron/resources/web-static
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine-test/affine-desktop e2e
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
HOIST_NODE_MODULES: 1
run: yarn workspace @affine/electron package --platform=darwin --arch=arm64
- name: Output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
yarn workspace @affine/electron exec node --loader ts-node/esm/transpile-only ./scripts/macos-arm64-output-check.ts
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
-199
View File
@@ -1,199 +0,0 @@
name: Build & Test
on:
push:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- canary
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
AFFINE_ENV: dev
COVERAGE: true
DISTRIBUTION: browser
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run oxlint
# oxlint is fast, so wrong code will fail quickly
run: yarn dlx $(node -e "console.log(require('./package.json').scripts['lint:ox'])")
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Run i18n codegen
run: yarn i18n-codegen gen
- name: Run ESLint
run: yarn lint:eslint --max-warnings=0
- name: Run Prettier
# Set nmMode in `actions/setup-node` will modify the .yarnrc.yml
run: |
git checkout .yarnrc.yml
yarn lint:prettier
- name: Run Type Check
run: yarn typecheck
check-yarn-binary:
name: Check yarn binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run check
run: |
yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
git diff --exit-code
e2e-plugin-test:
name: E2E Plugin Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-plugin
env:
COVERAGE: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2e-plugin-test
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-plugin
path: ./test-results
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Run playwright tests
run: yarn e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
working-directory: tests/affine-local
env:
COVERAGE: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
e2e-migration-test:
name: E2E Migration Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Run playwright tests
run: yarn workspace @affine-test/affine-migration e2e --forbid-only
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-migration
path: ./tests/affine-migration/test-results
if-no-files-found: ignore
unit-test:
name: Unit Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Unit Test
run: yarn nx test:coverage @affine/monorepo
- name: Upload unit test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/store/lcov.info
flags: unittest
name: affine
fail_ci_if_error: false
-36
View File
@@ -1,36 +0,0 @@
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup caches for closed branches
on:
pull_request:
types:
- closed
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-18
View File
@@ -1,18 +0,0 @@
name: Cancel
on:
pull_request_target:
types:
- edited
- synchronize
jobs:
cancel:
name: 'Cancel Previous Runs'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: styfle/cancel-workflow-action@0.12.0
with:
# See https://api.github.com/repos/toeverything/AFFiNE/actions/workflows
workflow_id: 44038251, 61883931, 65188160, 66789140
access_token: ${{ github.token }}
-70
View File
@@ -1,70 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
push:
branches: [canary]
pull_request:
merge_group:
# The branches below must be a subset of the branches above
branches: [canary]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+9 -2
View File
@@ -69,7 +69,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
@@ -90,7 +90,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'aarch64-unknown-linux-gnu'
@@ -207,6 +207,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: setup deploy version
id: version
run: |
export APP_VERSION=`node -e "console.log(require('./package.json').version)"`
echo $APP_VERSION
echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_OUTPUT"
- name: Deploy to ${{ github.event.inputs.flavor }}
uses: ./.github/actions/deploy
with:
@@ -217,6 +223,7 @@ jobs:
cluster-name: ${{ secrets.GCP_CLUSTER_NAME }}
cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }}
env:
APP_VERSION: ${{ steps.version.outputs.APP_VERSION }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
CANARY_DEPLOY_HOST: ${{ secrets.CANARY_DEPLOY_HOST }}
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
+6
View File
@@ -231,6 +231,11 @@ jobs:
node ./packages/frontend/electron/scripts/generate-yml.js
env:
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Generate SHA512 checksums
run: |
sha512sum *-linux-* > SHA512SUMS.txt
sha512sum *-macos-* >> SHA512SUMS.txt
sha512sum *-windows-* >> SHA512SUMS.txt
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
@@ -241,6 +246,7 @@ jobs:
tag_name: ${{ needs.set-build-version.outputs.version }}
prerelease: true
files: |
./SHA512SUMS.txt
./VERSION
./*.zip
./*.dmg
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged && yarn lint:ox && cargo fmt --all && git add .
yarn lint-staged && yarn lint:ox
+5 -6
View File
@@ -107,12 +107,11 @@ If you have questions, you are welcome to contact us. One of the best places to
## Ecosystem
| Name | | |
| ----------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| [@toeverything/component](https://github.com/toeverything/design/tree/main/packages/components) | Toeverything Shared Component Resources | |
| [@affine/component](packages/frontend/component) | AFFiNE Component Resources | [![](https://img.shields.io/codecov/c/github/toeverything/affine?style=flat-square)](https://affine-storybook.vercel.app/) |
| [@toeverything/y-indexeddb](packages/common/y-indexeddb) | IndexedDB database adapter for Yjs | [![](https://img.shields.io/npm/dm/@toeverything/y-indexeddb?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
| [@toeverything/theme](packages/common/theme) | AFFiNE theme | [![](https://img.shields.io/npm/dm/@toeverything/theme?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/theme) |
| Name | | |
| -------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| [@affine/component](packages/frontend/component) | AFFiNE Component Resources | [![](https://img.shields.io/codecov/c/github/toeverything/affine?style=flat-square)](https://affine-storybook.vercel.app/) |
| [@toeverything/y-indexeddb](packages/common/y-indexeddb) | IndexedDB database adapter for Yjs | [![](https://img.shields.io/npm/dm/@toeverything/y-indexeddb?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
| [@toeverything/theme](packages/common/theme) | AFFiNE theme | [![](https://img.shields.io/npm/dm/@toeverything/theme?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/theme) |
## Plugins
+5 -2
View File
@@ -50,6 +50,9 @@
],
"*.toml": [
"taplo format"
],
"*.rs": [
"cargo fmt --"
]
},
"devDependencies": {
@@ -104,7 +107,7 @@
"string-width": "^7.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.3.2",
"vite": "^5.0.1",
"vite": "^5.0.6",
"vite-plugin-istanbul": "^5.0.0",
"vite-plugin-static-copy": "^0.17.1",
"vite-tsconfig-paths": "^4.2.1",
@@ -114,7 +117,7 @@
},
"packageManager": "yarn@4.0.1",
"resolutions": {
"vite": "^4.4.11",
"vite": "^5.0.6",
"array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@latest",
"array-includes": "npm:@nolyfill/array-includes@latest",
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",
+3 -3
View File
@@ -41,11 +41,11 @@
"@opentelemetry/core": "^1.18.1",
"@opentelemetry/exporter-prometheus": "^0.45.1",
"@opentelemetry/exporter-zipkin": "^1.18.1",
"@opentelemetry/host-metrics": "^0.33.2",
"@opentelemetry/host-metrics": "^0.34.0",
"@opentelemetry/instrumentation": "^0.45.1",
"@opentelemetry/instrumentation-graphql": "^0.36.0",
"@opentelemetry/instrumentation-http": "^0.45.1",
"@opentelemetry/instrumentation-ioredis": "^0.35.3",
"@opentelemetry/instrumentation-ioredis": "^0.36.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.33.3",
"@opentelemetry/instrumentation-socket.io": "^0.34.3",
"@opentelemetry/resources": "^1.18.1",
@@ -102,7 +102,7 @@
"@types/sinon": "^17.0.2",
"@types/supertest": "^2.0.16",
"@types/ws": "^8.5.10",
"ava": "^5.3.1",
"ava": "^6.0.0",
"c8": "^8.0.1",
"nodemon": "^3.0.1",
"sinon": "^17.0.1",
+2
View File
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { CacheModule } from './cache';
import { ConfigModule } from './config';
import { EventModule } from './event';
import { BusinessModules } from './modules';
import { AuthModule } from './modules/auth';
import { PrismaModule } from './prisma';
@@ -14,6 +15,7 @@ const BasicModules = [
PrismaModule,
ConfigModule.forRoot(),
CacheModule,
EventModule,
StorageModule.forRoot(),
SessionModule,
RateLimiterModule,
@@ -0,0 +1,23 @@
import type { Snapshot, Workspace } from '@prisma/client';
import { Flatten, Payload } from './types';
interface EventDefinitions {
workspace: {
deleted: Payload<Workspace['id']>;
};
snapshot: {
updated: Payload<
Pick<Snapshot, 'id' | 'workspaceId'> & {
previous: Pick<Snapshot, 'blob' | 'state' | 'updatedAt'>;
}
>;
deleted: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
};
}
export type EventKV = Flatten<EventDefinitions>;
export type Event = keyof EventKV;
export type EventPayload<E extends Event> = EventKV[E];
@@ -0,0 +1,45 @@
import { Global, Injectable, Module } from '@nestjs/common';
import {
EventEmitter2,
EventEmitterModule,
OnEvent as RawOnEvent,
} from '@nestjs/event-emitter';
import type { Event, EventPayload } from './events';
@Injectable()
export class EventEmitter {
constructor(private readonly emitter: EventEmitter2) {}
emit<E extends Event>(event: E, payload: EventPayload<E>) {
return this.emitter.emit(event, payload);
}
emitAsync<E extends Event>(event: E, payload: EventPayload<E>) {
return this.emitter.emitAsync(event, payload);
}
on<E extends Event>(event: E, handler: (payload: EventPayload<E>) => void) {
return this.emitter.on(event, handler);
}
once<E extends Event>(event: E, handler: (payload: EventPayload<E>) => void) {
return this.emitter.once(event, handler);
}
}
export const OnEvent = (
event: Event,
opts?: Parameters<typeof RawOnEvent>[1]
) => {
return RawOnEvent(event, opts);
};
@Global()
@Module({
imports: [EventEmitterModule.forRoot()],
providers: [EventEmitter],
exports: [EventEmitter],
})
export class EventModule {}
export { EventPayload };
@@ -0,0 +1,33 @@
export type Payload<T> = {
__payload: true;
data: T;
};
export type Join<A extends string, B extends string> = A extends ''
? B
: `${A}.${B}`;
export type PathType<T, Path extends string> = string extends Path
? unknown
: Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? PathType<T[K], R>
: unknown
: unknown;
export type Leaves<T, P extends string = ''> = T extends Payload<any>
? P
: T extends Record<string, any>
? {
[K in keyof T]: K extends string ? Leaves<T[K], Join<P, K>> : never;
}[keyof T]
: never;
export type Flatten<T> = Leaves<T> extends infer R
? {
// @ts-expect-error yo, ts can't make it
[K in R]: PathType<T, K> extends Payload<infer U> ? U : never;
}
: never;
@@ -1,15 +1,15 @@
import { isDeepStrictEqual } from 'node:util';
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Cron, CronExpression } from '@nestjs/schedule';
import type { Snapshot } from '@prisma/client';
import { Config } from '../../config';
import { type EventPayload, OnEvent } from '../../event';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma';
import { SubscriptionStatus } from '../payment/service';
import { Permission } from '../workspaces/types';
import { isEmptyBuffer } from './manager';
@Injectable()
export class DocHistoryManager {
@@ -19,16 +19,38 @@ export class DocHistoryManager {
private readonly db: PrismaService
) {}
@OnEvent('doc:manager:snapshot:beforeUpdate')
async onDocUpdated(snapshot: Snapshot, forceCreate = false) {
const last = await this.last(snapshot.workspaceId, snapshot.id);
@OnEvent('workspace.deleted')
onWorkspaceDeleted(workspaceId: EventPayload<'workspace.deleted'>) {
return this.db.snapshotHistory.deleteMany({
where: {
workspaceId,
},
});
}
@OnEvent('snapshot.deleted')
onSnapshotDeleted({ workspaceId, id }: EventPayload<'snapshot.deleted'>) {
return this.db.snapshotHistory.deleteMany({
where: {
workspaceId,
id,
},
});
}
@OnEvent('snapshot.updated')
async onDocUpdated(
{ workspaceId, id, previous }: EventPayload<'snapshot.updated'>,
forceCreate = false
) {
const last = await this.last(workspaceId, id);
let shouldCreateHistory = false;
if (!last) {
// never created
shouldCreateHistory = true;
} else if (last.timestamp === snapshot.updatedAt) {
} else if (last.timestamp === previous.updatedAt) {
// no change
shouldCreateHistory = false;
} else if (
@@ -36,16 +58,23 @@ export class DocHistoryManager {
forceCreate ||
// last history created before interval in configs
last.timestamp.getTime() <
snapshot.updatedAt.getTime() - this.config.doc.history.interval
previous.updatedAt.getTime() - this.config.doc.history.interval
) {
shouldCreateHistory = true;
}
if (shouldCreateHistory) {
// skip the history recording when no actual update on snapshot happended
if (last && isDeepStrictEqual(last.state, snapshot.state)) {
if (last && isDeepStrictEqual(last.state, previous.state)) {
this.logger.debug(
`State matches, skip creating history record for ${snapshot.id} in workspace ${snapshot.workspaceId}`
`State matches, skip creating history record for ${id} in workspace ${workspaceId}`
);
return;
}
if (isEmptyBuffer(previous.blob)) {
this.logger.debug(
`Doc is empty, skip creating history record for ${id} in workspace ${workspaceId}`
);
return;
}
@@ -56,12 +85,12 @@ export class DocHistoryManager {
timestamp: true,
},
data: {
workspaceId: snapshot.workspaceId,
id: snapshot.id,
timestamp: snapshot.updatedAt,
blob: snapshot.blob,
state: snapshot.state,
expiredAt: await this.getExpiredDateFromNow(snapshot.workspaceId),
workspaceId,
id,
timestamp: previous.updatedAt,
blob: previous.blob,
state: previous.state,
expiredAt: await this.getExpiredDateFromNow(workspaceId),
},
})
.catch(() => {
@@ -73,9 +102,7 @@ export class DocHistoryManager {
description: 'How many times the snapshot history created',
})
.add(1);
this.logger.log(
`History created for ${snapshot.id} in workspace ${snapshot.workspaceId}.`
);
this.logger.log(`History created for ${id} in workspace ${workspaceId}.`);
}
}
@@ -180,7 +207,7 @@ export class DocHistoryManager {
}
// save old snapshot as one history record
await this.onDocUpdated(oldSnapshot, true);
await this.onDocUpdated({ workspaceId, id, previous: oldSnapshot }, true);
// WARN:
// we should never do the snapshot updating in recovering,
// which is not the solution in CRDT.
@@ -5,12 +5,12 @@ import {
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Snapshot, Update } from '@prisma/client';
import { chunk } from 'lodash-es';
import { defer, retry } from 'rxjs';
import {
applyUpdate,
decodeStateVector,
Doc,
encodeStateAsUpdate,
encodeStateVector,
@@ -19,6 +19,7 @@ import {
import { Cache } from '../../cache';
import { Config } from '../../config';
import { EventEmitter, type EventPayload, OnEvent } from '../../event';
import { metrics } from '../../metrics/metrics';
import { PrismaService } from '../../prisma';
import { mergeUpdatesInApplyWay as jwstMergeUpdates } from '../../storage';
@@ -40,7 +41,37 @@ function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean {
return compare(yBinary, yBinary2, true);
}
function isEmptyBuffer(buf: Buffer): boolean {
/**
* Detect whether rhs state is newer than lhs state.
*
* How could we tell a state is newer:
*
* i. if the state vector size is larger, it's newer
* ii. if the state vector size is same, compare each client's state
*/
function isStateNewer(lhs: Buffer, rhs: Buffer): boolean {
const lhsVector = decodeStateVector(lhs);
const rhsVector = decodeStateVector(rhs);
if (lhsVector.size < rhsVector.size) {
return true;
}
for (const [client, state] of lhsVector) {
const rstate = rhsVector.get(client);
if (!rstate) {
return false;
}
if (state < rstate) {
return true;
}
}
return false;
}
export function isEmptyBuffer(buf: Buffer): boolean {
return (
buf.length === 0 ||
// 0x0000
@@ -71,7 +102,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
private readonly db: PrismaService,
private readonly config: Config,
private readonly cache: Cache,
private readonly event: EventEmitter2
private readonly event: EventEmitter
) {}
onModuleInit() {
@@ -193,6 +224,33 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
}
}
@OnEvent('workspace.deleted')
async onWorkspaceDeleted(workspaceId: string) {
await this.db.snapshot.deleteMany({
where: {
workspaceId,
},
});
await this.db.update.deleteMany({
where: {
workspaceId,
},
});
}
@OnEvent('snapshot.deleted')
async onSnapshotDeleted({
id,
workspaceId,
}: EventPayload<'snapshot.deleted'>) {
await this.db.update.deleteMany({
where: {
id,
workspaceId,
},
});
}
/**
* add update to manager for later processing.
*/
@@ -374,23 +432,17 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
}
const { id, workspaceId } = candidate;
// acquire lock
const ok = await this.lockUpdatesForAutoSquash(workspaceId, id);
if (!ok) {
return;
}
try {
await this._get(workspaceId, id);
} catch (e) {
this.logger.error(
`Failed to apply updates for workspace: ${workspaceId}, guid: ${id}`
);
this.logger.error(e);
} finally {
await this.unlockUpdatesForAutoSquash(workspaceId, id);
}
await this.lockUpdatesForAutoSquash(workspaceId, id, async () => {
try {
await this._get(workspaceId, id);
} catch (e) {
this.logger.error(
`Failed to apply updates for workspace: ${workspaceId}, guid: ${id}`
);
this.logger.error(e);
}
});
}
private async getAutoSquashCandidate() {
@@ -414,34 +466,67 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
doc: Doc,
initialSeq?: number
) {
const blob = Buffer.from(encodeStateAsUpdate(doc));
const state = Buffer.from(encodeStateVector(doc));
return this.lockSnapshotForUpsert(workspaceId, guid, async () => {
const blob = Buffer.from(encodeStateAsUpdate(doc));
if (isEmptyBuffer(blob)) {
return;
}
if (isEmptyBuffer(blob)) {
return false;
}
await this.db.snapshot.upsert({
select: {
seq: true,
},
where: {
id_workspaceId: {
id: guid,
workspaceId,
},
},
create: {
id: guid,
workspaceId,
blob,
state,
seq: initialSeq,
},
update: {
blob,
state,
},
const state = Buffer.from(encodeStateVector(doc));
return await this.db.$transaction(async db => {
const snapshot = await db.snapshot.findUnique({
where: {
id_workspaceId: {
id: guid,
workspaceId,
},
},
});
// update
if (snapshot) {
// only update if state is newer
if (isStateNewer(snapshot.state ?? Buffer.from([0]), state)) {
await db.snapshot.update({
select: {
seq: true,
},
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
data: {
blob,
state,
},
});
return true;
} else {
return false;
}
} else {
// create
await db.snapshot.create({
select: {
seq: true,
},
data: {
id: guid,
workspaceId,
blob,
state,
seq: initialSeq,
},
});
return true;
}
});
});
}
@@ -480,25 +565,39 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
...updates.map(u => u.blob)
);
if (snapshot) {
this.event.emit('doc:manager:snapshot:beforeUpdate', snapshot);
}
await this.upsert(workspaceId, id, doc, last.seq);
this.logger.debug(
`Squashed ${updates.length} updates for ${id} in workspace ${workspaceId}`
);
await this.db.update.deleteMany({
where: {
if (snapshot) {
this.event.emit('snapshot.updated', {
id,
workspaceId,
seq: {
in: updates.map(u => u.seq),
previous: {
blob: snapshot.blob,
state: snapshot.state,
updatedAt: snapshot.updatedAt,
},
},
});
});
}
const done = await this.upsert(workspaceId, id, doc, last.seq);
if (done) {
this.logger.debug(
`Squashed ${updates.length} updates for ${id} in workspace ${workspaceId}`
);
await this.db.update.deleteMany({
where: {
id,
workspaceId,
seq: {
in: updates.map(u => u.seq),
},
},
});
await this.updateCachedUpdatesCount(workspaceId, id, -updates.length);
}
await this.updateCachedUpdatesCount(workspaceId, id, -updates.length);
return doc;
}
@@ -581,22 +680,44 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
return null;
}
private async lockUpdatesForAutoSquash(workspaceId: string, guid: string) {
return this.cache.setnx(
private async doWithLock<T>(lock: string, job: () => Promise<T>) {
const acquired = await this.cache.setnx(lock, 1, {
ttl: 60 * 1000,
});
if (!acquired) {
return;
}
try {
return await job();
} finally {
await this.cache.delete(lock).catch(e => {
// safe, the lock will be expired when ttl ends
this.logger.error(`Failed to release lock ${lock}`, e);
});
}
}
private async lockUpdatesForAutoSquash<T>(
workspaceId: string,
guid: string,
job: () => Promise<T>
) {
return this.doWithLock(
`doc:manager:updates-lock:${workspaceId}::${guid}`,
1,
{
ttl: 60 * 1000,
}
job
);
}
private async unlockUpdatesForAutoSquash(workspaceId: string, guid: string) {
return this.cache
.delete(`doc:manager:updates-lock:${workspaceId}::${guid}`)
.catch(e => {
// safe, the lock will be expired when ttl ends
this.logger.error('Failed to release updates lock', e);
});
async lockSnapshotForUpsert<T>(
workspaceId: string,
guid: string,
job: () => Promise<T>
) {
return this.doWithLock(
`doc:manager:snapshot-lock:${workspaceId}::${guid}`,
job
);
}
}
+1 -6
View File
@@ -1,5 +1,4 @@
import { DynamicModule, Type } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { GqlModule } from '../graphql.module';
@@ -11,11 +10,7 @@ import { SyncModule } from './sync';
import { UsersModule } from './users';
import { WorkspaceModule } from './workspaces';
const BusinessModules: (Type | DynamicModule)[] = [
EventEmitterModule.forRoot({
global: true,
}),
];
const BusinessModules: (Type | DynamicModule)[] = [];
switch (SERVER_FLAVOR) {
case 'sync':
@@ -25,7 +25,6 @@ import {
EventError,
InternalError,
NotInWorkspaceError,
WorkspaceNotFoundError,
} from './error';
export const GatewayErrorWrapper = (): MethodDecorator => {
@@ -319,9 +318,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
if (!doc) {
return {
error: docId.isWorkspace
? new WorkspaceNotFoundError(workspaceId)
: new DocNotFoundError(workspaceId, docId.guid),
error: new DocNotFoundError(workspaceId, docId.guid),
};
}
@@ -33,6 +33,7 @@ import type {
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { applyUpdate, Doc } from 'yjs';
import { EventEmitter } from '../../event';
import { PrismaService } from '../../prisma';
import { StorageProvide } from '../../storage';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
@@ -146,6 +147,7 @@ export class WorkspaceResolver {
private readonly prisma: PrismaService,
private readonly permissions: PermissionService,
private readonly users: UsersService,
private readonly event: EventEmitter,
@Inject(StorageProvide) private readonly storage: Storage
) {}
@@ -308,22 +310,11 @@ export class WorkspaceResolver {
})
async createWorkspace(
@CurrentUser() user: UserType,
@Args({ name: 'init', type: () => GraphQLUpload })
update: FileUpload
// we no longer support init workspace with a preload file
// use sync system to uploading them once created
@Args({ name: 'init', type: () => GraphQLUpload, nullable: true })
init: FileUpload | null
) {
// convert stream to buffer
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = update.createReadStream();
const chunks: Uint8Array[] = [];
stream.on('data', chunk => {
chunks.push(chunk);
});
stream.on('error', reject);
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
});
const workspace = await this.prisma.workspace.create({
data: {
public: false,
@@ -341,14 +332,31 @@ export class WorkspaceResolver {
},
});
if (buffer.length) {
await this.prisma.snapshot.create({
data: {
id: workspace.id,
workspaceId: workspace.id,
blob: buffer,
},
if (init) {
// convert stream to buffer
const buffer = await new Promise<Buffer>(resolve => {
const stream = init.createReadStream();
const chunks: Uint8Array[] = [];
stream.on('data', chunk => {
chunks.push(chunk);
});
stream.on('error', () => {
resolve(Buffer.from([]));
});
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
});
if (buffer.length) {
await this.prisma.snapshot.create({
data: {
id: workspace.id,
workspaceId: workspace.id,
blob: buffer,
},
});
}
}
return workspace;
@@ -382,18 +390,7 @@ export class WorkspaceResolver {
},
});
await this.prisma.$transaction([
this.prisma.update.deleteMany({
where: {
workspaceId: id,
},
}),
this.prisma.snapshot.deleteMany({
where: {
workspaceId: id,
},
}),
]);
this.event.emit('workspace.deleted', id);
return true;
}
+1 -1
View File
@@ -292,7 +292,7 @@ type Mutation {
sendVerifyChangeEmail(token: String!, email: String!, callbackUrl: String!): Boolean!
"""Create a new workspace"""
createWorkspace(init: Upload!): WorkspaceType!
createWorkspace(init: Upload): WorkspaceType!
"""Update workspace"""
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
+78 -3
View File
@@ -1,15 +1,20 @@
import { mock } from 'node:test';
import type { INestApplication } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
import test from 'ava';
import { register } from 'prom-client';
import * as Sinon from 'sinon';
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import {
applyUpdate,
decodeStateVector,
Doc as YDoc,
encodeStateAsUpdate,
} from 'yjs';
import { CacheModule } from '../src/cache';
import { Config, ConfigModule } from '../src/config';
import { EventModule } from '../src/event';
import { DocManager, DocModule } from '../src/modules/doc';
import { PrismaModule, PrismaService } from '../src/prisma';
import { flushDB } from './utils';
@@ -19,7 +24,7 @@ const createModule = () => {
imports: [
PrismaModule,
CacheModule,
EventEmitterModule.forRoot(),
EventModule,
ConfigModule.forRoot(),
DocModule.forRoot(),
],
@@ -283,3 +288,73 @@ test('should throw if meet max retry times', async t => {
);
t.is(stub.callCount, 5);
});
test('should not update snapshot if state is outdated', async t => {
const db = m.get(PrismaService);
const manager = m.get(DocManager);
await db.snapshot.create({
data: {
id: '2',
workspaceId: '2',
blob: Buffer.from([0, 0]),
seq: 1,
},
});
const doc = new YDoc();
const text = doc.getText('content');
const updates: Buffer[] = [];
doc.on('update', update => {
updates.push(Buffer.from(update));
});
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
await Promise.all(updates.map(update => manager.push('2', '2', update)));
const updateWith3Records = await manager.getUpdates('2', '2');
text.insert(11, '!');
await manager.push('2', '2', updates[3]);
const updateWith4Records = await manager.getUpdates('2', '2');
// Simulation:
// Node A get 3 updates and squash them at time 1, will finish at time 10
// Node B get 4 updates and squash them at time 3, will finish at time 8
// Node B finish the squash first, and update the snapshot
// Node A finish the squash later, and update the snapshot to an outdated state
// Time: ---------------------->
// A: ^get ^upsert
// B: ^get ^upsert
//
// We should avoid such situation
// @ts-expect-error private
await manager.squash(updateWith4Records, null);
// @ts-expect-error private
await manager.squash(updateWith3Records, null);
const result = await db.snapshot.findUnique({
where: {
id_workspaceId: {
id: '2',
workspaceId: '2',
},
},
});
if (!result) {
t.fail('snapshot not found');
return;
}
const state = decodeStateVector(result.state!);
t.is(state.get(doc.clientID), 12);
const d = new YDoc();
applyUpdate(d, result.blob!);
const dtext = d.getText('content');
t.is(dtext.toString(), 'hello world!');
});
+21 -40
View File
@@ -6,6 +6,7 @@ import test from 'ava';
import * as Sinon from 'sinon';
import { ConfigModule } from '../src/config';
import type { EventPayload } from '../src/event';
import { DocHistoryManager } from '../src/modules/doc';
import { PrismaModule, PrismaService } from '../src/prisma';
import { flushDB } from './utils';
@@ -41,21 +42,28 @@ test.afterEach(async () => {
const snapshot: Snapshot = {
workspaceId: '1',
id: 'doc1',
blob: Buffer.from([0, 0]),
state: Buffer.from([0, 0]),
blob: Buffer.from([1, 0]),
state: Buffer.from([0]),
seq: 0,
updatedAt: new Date(),
createdAt: new Date(),
};
function getEventData(
timestamp: Date = new Date()
): EventPayload<'snapshot.updated'> {
return {
workspaceId: snapshot.workspaceId,
id: snapshot.id,
previous: { ...snapshot, updatedAt: timestamp },
};
}
test('should create doc history if never created before', async t => {
Sinon.stub(manager, 'last').resolves(null);
const timestamp = new Date();
await manager.onDocUpdated({
...snapshot,
updatedAt: timestamp,
});
await manager.onDocUpdated(getEventData(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -72,10 +80,7 @@ test('should not create history if timestamp equals to last record', async t =>
const timestamp = new Date();
Sinon.stub(manager, 'last').resolves({ timestamp, state: null });
await manager.onDocUpdated({
...snapshot,
updatedAt: timestamp,
});
await manager.onDocUpdated(getEventData(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -94,10 +99,7 @@ test('should not create history if state equals to last record', async t => {
state: snapshot.state,
});
await manager.onDocUpdated({
...snapshot,
updatedAt: timestamp,
});
await manager.onDocUpdated(getEventData(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -116,10 +118,7 @@ test('should not create history if time diff is less than interval config', asyn
state: Buffer.from([0, 1]),
});
await manager.onDocUpdated({
...snapshot,
updatedAt: timestamp,
});
await manager.onDocUpdated(getEventData(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -138,10 +137,7 @@ test('should create history if time diff is larger than interval config and stat
state: Buffer.from([0, 1]),
});
await manager.onDocUpdated({
...snapshot,
updatedAt: timestamp,
});
await manager.onDocUpdated(getEventData(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -160,13 +156,7 @@ test('should create history with force flag even if time diff in small', async t
state: Buffer.from([0, 1]),
});
await manager.onDocUpdated(
{
...snapshot,
updatedAt: timestamp,
},
true
);
await manager.onDocUpdated(getEventData(timestamp), true);
const history = await db.snapshotHistory.findFirst({
where: {
@@ -224,13 +214,7 @@ test('should correctly list all history records', async t => {
test('should be able to get history data', async t => {
const timestamp = new Date();
await manager.onDocUpdated(
{
...snapshot,
updatedAt: timestamp,
},
true
);
await manager.onDocUpdated(getEventData(timestamp), true);
const history = await manager.get(
snapshot.workspaceId,
@@ -274,10 +258,7 @@ test('should be able to recover from history', async t => {
},
});
const history1Timestamp = snapshot.updatedAt.getTime() - 10;
await manager.onDocUpdated({
...snapshot,
updatedAt: new Date(history1Timestamp),
});
await manager.onDocUpdated(getEventData(new Date(history1Timestamp)));
await manager.recover(
snapshot.workspaceId,
+19 -16
View File
@@ -1,21 +1,6 @@
/* tslint:disable */
/* auto-generated by NAPI-RS */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
export function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
export interface Blob {
contentType: string
lastModified: string
size: number
data: Buffer
}
/**
* Merge updates in form like `Y.applyUpdate(doc, update)` way and return the
* result binary.
*/
export function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
export class Storage {
/** Create a storage instance and establish connection to persist store. */
static connect(database: string, debugOnlyAutoMigrate?: boolean | undefined | null): Promise<Storage>
@@ -30,3 +15,21 @@ export class Storage {
/** Workspace size taken by blobs. */
blobsSize(workspaces: Array<string>): Promise<number>
}
export interface Blob {
contentType: string
lastModified: string
size: number
data: Buffer
}
/**
* Merge updates in form like `Y.applyUpdate(doc, update)` way and return the
* result binary.
*/
export function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
export function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
export function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
+4 -5
View File
@@ -16,27 +16,26 @@
}
},
"napi": {
"name": "storage",
"binaryName": "storage",
"targets": [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"aarch64-pc-windows-msvc",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"universal-apple-darwin"
"x86_64-unknown-linux-gnu"
]
},
"scripts": {
"test": "node --test ./__tests__/**/*.spec.js",
"build": "napi build --release --strip",
"build": "napi build --release --strip --no-const-enum",
"build:debug": "napi build",
"prepublishOnly": "napi prepublish -t npm",
"artifacts": "napi artifacts",
"version": "napi version"
},
"devDependencies": {
"@napi-rs/cli": "^2.16.5",
"@napi-rs/cli": "3.0.0-alpha.15",
"lib0": "^0.2.87",
"nx": "^17.1.3",
"nx-cloud": "^16.5.2",
+3 -4
View File
@@ -3,8 +3,8 @@
"private": true,
"type": "module",
"devDependencies": {
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
"react": "18.2.0",
"react-dom": "18.2.0",
"vitest": "0.34.6",
@@ -21,8 +21,7 @@
},
"peerDependencies": {
"@affine/templates": "workspace:*",
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly",
"@toeverything/infra": "workspace:*"
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly"
},
"dependencies": {
"lit": "^3.0.2"
+4 -6
View File
@@ -6,12 +6,9 @@ import { isDesktop, isServer } from './constant.js';
import { UaHelper } from './ua-helper.js';
export const blockSuiteFeatureFlags = z.object({
enable_set_remote_flag: z.boolean(),
enable_block_hub: z.boolean(),
enable_toggle_block: z.boolean(),
enable_bookmark_operation: z.boolean(),
enable_note_index: z.boolean(),
enable_transformer_clipboard: z.boolean(),
enable_expand_database_block: z.boolean(),
enable_bultin_ledits: z.boolean(),
});
export const runtimeFlagsSchema = z.object({
@@ -21,6 +18,7 @@ export const runtimeFlagsSchema = z.object({
enableBroadcastChannelProvider: z.boolean(),
enableDebugPage: z.boolean(),
changelogUrl: z.string(),
downloadUrl: z.string(),
// see: tools/workers
imageProxyUrl: z.string(),
enablePreloading: z.boolean(),
+1 -22
View File
@@ -1,5 +1,3 @@
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import type {
ActiveDocProvider,
PassiveDocProvider,
@@ -111,7 +109,7 @@ export const settingPanel = {
Export: 'export',
Sync: 'sync',
} as const;
export const settingPanelValues = [...Object.values(settingPanel)] as const;
export const settingPanelValues = Object.values(settingPanel);
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
// built-in workspaces
@@ -134,18 +132,6 @@ type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = {
currentWorkspaceId: string;
};
export type WorkspaceHeaderProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
rightSlot?: ReactNode;
currentEntry:
| {
subPath: WorkspaceSubPath;
}
| {
pageId: string;
};
};
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
onDeleteLocalWorkspace: () => void;
@@ -161,18 +147,11 @@ type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
) => void;
};
type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
currentPageId: string;
onLoadEditor: (page: Page, editor: EditorContainer) => () => void;
};
interface FC<P> {
(props: P): ReactNode;
}
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
PageDetail: FC<PageDetailProps<Flavour>>;
NewSettingsDetail: FC<NewSettingProps<Flavour>>;
Provider: FC<PropsWithChildren>;
LoginCard?: FC<object>;
-3
View File
@@ -7,9 +7,6 @@
"outDir": "lib"
},
"references": [
{
"path": "../infra"
},
{
"path": "../../../tests/fixtures"
}
+10 -8
View File
@@ -54,10 +54,12 @@
"dev": "vite build --watch"
},
"dependencies": {
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/sdk": "workspace:*",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/global": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
"jotai": "^2.5.1",
"jotai-effect": "^0.2.3",
"tinykeys": "^2.1.0",
@@ -66,22 +68,22 @@
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/presets": "0.11.0-nightly-202312070955-2b5bb47",
"@testing-library/react": "^14.0.0",
"async-call-rpc": "^6.3.1",
"electron": "link:../../frontend/electron/node_modules/electron",
"nanoid": "^5.0.3",
"react": "^18.2.0",
"rxjs": "^7.8.1",
"vite": "^4.4.11",
"vite": "^5.0.6",
"vite-plugin-dts": "3.6.0",
"vitest": "0.34.6",
"yjs": "^13.6.10"
},
"peerDependencies": {
"@affine/templates": "*",
"@blocksuite/editor": "*",
"@blocksuite/presets": "*",
"async-call-rpc": "*",
"electron": "*",
"react": "*",
@@ -91,7 +93,7 @@
"@affine/templates": {
"optional": true
},
"@blocksuite/editor": {
"@blocksuite/presets": {
"optional": true
},
"async-call-rpc": {
+8
View File
@@ -0,0 +1,8 @@
import { atom } from 'jotai';
export const loadedPluginNameAtom = atom<string[]>([]);
export * from './layout';
export * from './root-store';
export * from './settings';
export * from './workspace';
@@ -1,34 +1,5 @@
import type { ExpectedLayout } from '@affine/sdk/entry';
import { assertExists } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { atom, createStore } from 'jotai/vanilla';
import { getBlockSuiteWorkspaceAtom } from './__internal__/workspace';
// global store
let rootStore = createStore();
export function getCurrentStore() {
return rootStore;
}
/**
* @internal do not use this function unless you know what you are doing
*/
export function _setCurrentStore(store: ReturnType<typeof createStore>) {
rootStore = store;
}
export const loadedPluginNameAtom = atom<string[]>([]);
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
const workspaceId = get(currentWorkspaceIdAtom);
assertExists(workspaceId);
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
return get(currentWorkspaceAtom);
});
import { atom } from 'jotai';
const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
@@ -0,0 +1,15 @@
import { createStore } from 'jotai';
// global store
let rootStore = createStore();
export function getCurrentStore() {
return rootStore;
}
/**
* @internal do not use this function unless you know what you are doing
*/
export function _setCurrentStore(store: ReturnType<typeof createStore>) {
rootStore = store;
}
@@ -1,5 +1,9 @@
import { setupGlobal } from '@affine/env/global';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { atomEffect } from 'jotai-effect';
setupGlobal();
export type DateFormats =
| 'MM/dd/YYYY'
@@ -63,15 +67,35 @@ const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
type SetStateAction<Value> = Value | ((prev: Value) => Value);
const appSettingEffect = atomEffect(get => {
const settings = get(appSettingBaseAtom);
// some values in settings should be synced into electron side
if (environment.isDesktop) {
console.log('set config', settings);
window.apis?.updater
.setConfig({
autoCheckUpdate: settings.autoCheckUpdate,
autoDownloadUpdate: settings.autoDownloadUpdate,
})
.catch(err => {
console.error(err);
});
}
});
export const appSettingAtom = atom<
AppSetting,
[SetStateAction<Partial<AppSetting>>],
void
>(
get => get(appSettingBaseAtom),
(get, set, apply) => {
const prev = get(appSettingBaseAtom);
const next = typeof apply === 'function' ? apply(prev) : apply;
set(appSettingBaseAtom, { ...prev, ...next });
get => {
get(appSettingEffect);
return get(appSettingBaseAtom);
},
(_get, set, apply) => {
set(appSettingBaseAtom, prev => {
const next = typeof apply === 'function' ? apply(prev) : apply;
return { ...prev, ...next };
});
}
);
@@ -0,0 +1,14 @@
import { assertExists } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { atom } from 'jotai';
import { getBlockSuiteWorkspaceAtom } from '../__internal__/workspace';
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
const workspaceId = get(currentWorkspaceIdAtom);
assertExists(workspaceId);
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
return get(currentWorkspaceAtom);
});
@@ -1,3 +1,4 @@
import { DebugLogger } from '@affine/debug';
// @ts-expect-error upstream type is wrong
import { tinykeys } from 'tinykeys';
@@ -7,12 +8,14 @@ import {
createAffineCommand,
} from './command';
const commandLogger = new DebugLogger('command:registry');
export const AffineCommandRegistry = new (class {
readonly commands: Map<string, AffineCommand> = new Map();
register(options: AffineCommandOptions) {
if (this.commands.has(options.id)) {
console.warn(`Command ${options.id} already registered.`);
commandLogger.warn(`Command ${options.id} already registered.`);
return () => {};
}
const command = createAffineCommand(options);
@@ -38,17 +41,17 @@ export const AffineCommandRegistry = new (class {
});
}
console.debug(`Registered command ${command.id}`);
commandLogger.debug(`Registered command ${command.id}`);
return () => {
unsubKb?.();
this.commands.delete(command.id);
console.debug(`Unregistered command ${command.id}`);
commandLogger.debug(`Unregistered command ${command.id}`);
};
}
get(id: string): AffineCommand | undefined {
if (!this.commands.has(id)) {
console.warn(`Command ${id} not registered.`);
commandLogger.warn(`Command ${id} not registered.`);
return undefined;
}
return this.commands.get(id);
+1 -1
View File
@@ -201,7 +201,7 @@ export type UpdaterHandlers = {
downloadUpdate: () => Promise<void>;
getConfig: () => Promise<UpdaterConfig>;
setConfig: (newConfig: Partial<UpdaterConfig>) => Promise<void>;
checkForUpdatesAndNotify: () => Promise<{ version: string } | null>;
checkForUpdates: () => Promise<{ version: string } | null>;
};
export type WorkspaceHandlers = {
+6
View File
@@ -11,6 +11,12 @@
{
"path": "../sdk"
},
{
"path": "../env"
},
{
"path": "../debug"
},
{
"path": "./tsconfig.node.json"
}
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
entry: {
blocksuite: resolve(root, 'src/blocksuite/index.ts'),
index: resolve(root, 'src/index.ts'),
atom: resolve(root, 'src/atom.ts'),
atom: resolve(root, 'src/atom/index.ts'),
command: resolve(root, 'src/command/index.ts'),
type: resolve(root, 'src/type.ts'),
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
+6 -6
View File
@@ -22,16 +22,16 @@
"dist"
],
"dependencies": {
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/block-std": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/blocks": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/global": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/presets": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
"jotai": "^2.5.1",
"zod": "^3.22.4"
},
"devDependencies": {
"vite": "^4.4.11",
"vite": "^5.0.6",
"vite-plugin-dts": "3.6.0"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import type { BaseSelection } from '@blocksuite/block-std';
import type { EditorContainer } from '@blocksuite/editor';
import type { EditorContainer } from '@blocksuite/presets';
import type { Page } from '@blocksuite/store';
import type { Workspace } from '@blocksuite/store';
import type { Atom, getDefaultStore } from 'jotai/vanilla';
+4 -4
View File
@@ -32,15 +32,15 @@
}
},
"dependencies": {
"idb": "^7.1.1",
"idb": "^8.0.0",
"nanoid": "^5.0.3",
"y-provider": "workspace:*"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
"fake-indexeddb": "^5.0.0",
"vite": "^4.4.11",
"vite": "^5.0.6",
"vite-plugin-dts": "3.6.0",
"vitest": "0.34.6",
"y-indexeddb": "^9.0.11",
@@ -65,7 +65,7 @@ beforeEach(() => {
id = nanoid();
workspace = new Workspace({
id,
isSSR: true,
schema,
});
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
@@ -383,7 +383,7 @@ describe('subDoc', () => {
{
const newWorkspace = new Workspace({
id,
isSSR: true,
schema,
});
const provider = createIndexedDBProvider(newWorkspace.doc, rootDBName);
@@ -423,7 +423,7 @@ describe('utils', () => {
expect(update).toBeInstanceOf(Uint8Array);
const newWorkspace = new Workspace({
id,
isSSR: true,
schema,
});
applyUpdate(newWorkspace.doc, update);
+2 -2
View File
@@ -24,8 +24,8 @@
"build": "vite build"
},
"devDependencies": {
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"vite": "^4.4.11",
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
"vite": "^5.0.6",
"vite-plugin-dts": "3.6.0",
"vitest": "0.34.6",
"yjs": "^13.6.10"
@@ -0,0 +1,54 @@
import { StorybookConfig } from '@storybook/react-vite';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { fileURLToPath } from 'url';
import { mergeConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { getRuntimeConfig } from '../../core/.webpack/runtime-config';
export default {
stories: ['../src/ui/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-mdx-gfm',
'storybook-dark-mode',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
features: {
storyStoreV7: true,
},
docs: {
autodocs: true,
},
async viteFinal(config, _options) {
return mergeConfig(config, {
plugins: [
vanillaExtractPlugin(),
tsconfigPaths({
root: fileURLToPath(new URL('../../../../', import.meta.url)),
ignoreConfigErrors: true,
}),
],
define: {
'process.on': '(() => void 0)',
'process.env': {},
'process.env.COVERAGE': JSON.stringify(!!process.env.COVERAGE),
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
process.env.SHOULD_REPORT_TRACE === 'true'
)}`,
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`,
runtimeConfig: getRuntimeConfig({
distribution: 'browser',
mode: 'development',
channel: 'canary',
coverage: false,
}),
},
});
},
} satisfies StorybookConfig;
@@ -0,0 +1,27 @@
import { darkCssVariables, lightCssVariables } from '@toeverything/theme';
import { globalStyle } from '@vanilla-extract/css';
globalStyle('*', {
margin: 0,
padding: 0,
});
globalStyle('body', {
color: 'var(--affine-text-primary-color)',
fontFamily: 'var(--affine-font-family)',
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-font-height)',
backgroundColor: 'var(--affine-background-primary-color)',
});
globalStyle('html', {
vars: lightCssVariables,
});
globalStyle('html[data-theme="dark"]', {
vars: darkCssVariables,
});
globalStyle('.docs-story', {
backgroundColor: 'var(--affine-background-primary-color)',
});
@@ -0,0 +1,3 @@
<script>
window.global = window;
</script>
@@ -0,0 +1,60 @@
import './preview.css';
import { ThemeProvider, useTheme } from 'next-themes';
import type { ComponentType } from 'react';
import { useEffect } from 'react';
import { useDarkMode } from 'storybook-dark-mode';
import type { Preview } from '@storybook/react';
import React from 'react';
export const parameters: Preview = {
argTypes: {
param: {
table: { category: 'Group' },
},
},
globalTypes: {
theme: {
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
};
const ThemeChange = () => {
const isDark = useDarkMode();
const theme = useTheme();
if (theme.resolvedTheme === 'dark' && !isDark) {
theme.setTheme('light');
} else if (theme.resolvedTheme === 'light' && isDark) {
theme.setTheme('dark');
}
return null;
};
const Component = () => {
const isDark = useDarkMode();
const theme = useTheme();
useEffect(() => {
theme.setTheme(isDark ? 'dark' : 'light');
}, [isDark]);
return null;
};
export const decorators = [
(Story: ComponentType, context) => {
return (
<ThemeProvider themes={['dark', 'light']} enableSystem={true}>
<ThemeChange />
<Component />
<Story {...context} />
</ThemeProvider>
);
},
];
+28 -8
View File
@@ -5,13 +5,17 @@
"exports": {
".": "./src/index.ts",
"./theme/*": "./src/theme/*",
"./ui/*": "./src/ui/*/index.ts",
"./*": "./src/components/*/index.tsx"
},
"scripts": {
"dev": "storybook dev -p 6006"
},
"peerDependencies": {
"@blocksuite/blocks": "*",
"@blocksuite/editor": "*",
"@blocksuite/global": "*",
"@blocksuite/icons": "2.1.34",
"@blocksuite/presets": "*",
"@blocksuite/store": "*"
},
"dependencies": {
@@ -20,7 +24,7 @@
"@affine/i18n": "workspace:*",
"@affine/workspace": "workspace:*",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
@@ -29,11 +33,14 @@
"@popperjs/core": "^2.11.8",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@toeverything/hooks": "workspace:*",
"@toeverything/infra": "workspace:*",
"@toeverything/theme": "^0.7.24",
@@ -64,13 +71,24 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/global": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/icons": "2.1.36",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/presets": "0.11.0-nightly-202312070955-2b5bb47",
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
"@storybook/addon-actions": "^7.5.3",
"@storybook/addon-essentials": "^7.5.3",
"@storybook/addon-interactions": "^7.5.3",
"@storybook/addon-links": "^7.5.3",
"@storybook/addon-mdx-gfm": "^7.5.3",
"@storybook/addon-storysource": "^7.5.3",
"@storybook/blocks": "^7.5.3",
"@storybook/builder-vite": "^7.5.3",
"@storybook/jest": "^0.2.3",
"@storybook/react": "^7.5.3",
"@storybook/react-vite": "^7.5.3",
"@storybook/test-runner": "^0.15.2",
"@storybook/testing-library": "^0.2.2",
"@testing-library/react": "^14.0.0",
"@types/bytes": "^3.1.3",
@@ -80,8 +98,10 @@
"@types/react-dom": "^18.2.13",
"@vanilla-extract/css": "^1.13.0",
"fake-indexeddb": "^5.0.0",
"storybook": "^7.5.3",
"storybook-dark-mode": "^3.0.1",
"typescript": "^5.3.2",
"vite": "^4.4.11",
"vite": "^5.0.6",
"vitest": "0.34.6",
"yjs": "^13.6.10"
},
@@ -40,6 +40,11 @@ export const tipsContainer = style({
position: 'sticky',
gap: '16px',
containerType: 'inline-size',
'@media': {
'screen and (max-width: 520px)': {
flexWrap: 'wrap',
},
},
});
export const tipsMessage = style({
@@ -54,4 +59,9 @@ export const tipsRightItem = style({
justifyContent: 'space-between',
alignItems: 'center',
gap: '16px',
'@media': {
'screen and (max-width: 520px)': {
width: '100%',
},
},
});
@@ -1,5 +1,6 @@
import { Button, IconButton } from '@affine/component/ui/button';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button';
import { useCallback } from 'react';
import * as styles from './index.css';
@@ -17,13 +18,10 @@ export const LocalDemoTips = ({
onLogin,
onEnableCloud,
}: LocalDemoTipsProps) => {
const content = isLoggedIn
? 'This is a local demo workspace, and the data is stored locally. We recommend enabling AFFiNE Cloud.'
: 'This is a local demo workspace, and the data is stored locally in the browser. We recommend Enabling AFFiNE Cloud or downloading the client for a better experience.';
const t = useAFFiNEI18N();
const buttonLabel = isLoggedIn
? 'Enable AFFiNE Cloud'
: 'Sign in with AFFiNE Cloud';
? t['Enable AFFiNE Cloud']()
: t['Sign in and Enable']();
const handleClick = useCallback(() => {
if (isLoggedIn) {
@@ -34,7 +32,9 @@ export const LocalDemoTips = ({
return (
<div className={styles.tipsContainer} data-testid="local-demo-tips">
<div className={styles.tipsMessage}>{content}</div>
<div className={styles.tipsMessage}>
{t['com.affine.banner.local-warning']()}
</div>
<div className={styles.tipsRightItem}>
<div>
@@ -0,0 +1,24 @@
import * as styles from './index.css';
import { useNavConfig } from './use-nav-config';
export const DesktopNavbar = () => {
const config = useNavConfig();
return (
<div className={styles.topNavLinks}>
{config.map(item => {
return (
<a
key={item.title}
href={item.path}
target="_blank"
rel="noreferrer"
className={styles.topNavLink}
>
{item.title}
</a>
);
})}
</div>
);
};
@@ -0,0 +1,89 @@
import { style } from '@vanilla-extract/css';
export const root = style({
height: '100vh',
width: '100vw',
display: 'flex',
flexDirection: 'column',
fontSize: 'var(--affine-font-base)',
position: 'relative',
background: 'var(--affine-background-primary-color)',
});
export const affineLogo = style({
color: 'inherit',
});
export const topNav = style({
top: 0,
left: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px 120px',
selectors: {
'&.mobile': {
padding: '16px 20px',
},
},
});
export const topNavLinks = style({
display: 'flex',
columnGap: 4,
});
export const topNavLink = style({
color: 'var(--affine-text-primary-color)',
fontSize: 'var(--affine-font-sm)',
fontWeight: 500,
textDecoration: 'none',
padding: '4px 18px',
});
export const iconButton = style({
fontSize: '24px',
pointerEvents: 'auto',
selectors: {
'&.plain': {
color: 'var(--affine-text-primary-color)',
},
},
});
export const menu = style({
width: '100vw',
height: '100vh',
padding: '0',
background: 'var(--affine-background-primary-color)',
borderRadius: '0',
border: 'none',
boxShadow: 'none',
});
export const menuItem = style({
color: 'var(--affine-text-primary-color)',
fontSize: 'var(--affine-font-sm)',
fontWeight: 500,
textDecoration: 'none',
padding: '12px 20px',
maxWidth: '100%',
position: 'relative',
borderRadius: '0',
transition: 'background 0.3s ease',
selectors: {
'&:after': {
position: 'absolute',
content: '""',
bottom: 0,
display: 'block',
width: 'calc(100% - 40px)',
height: '0.5px',
background: 'var(--affine-black-10)',
},
'&:not(:last-of-type)': {
marginBottom: '0',
},
},
});
@@ -0,0 +1 @@
export * from './layout';
@@ -0,0 +1,49 @@
import { Button } from '@affine/component/ui/button';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Logo1Icon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useCallback } from 'react';
import { DesktopNavbar } from './desktop-navbar';
import * as styles from './index.css';
import { MobileNavbar } from './mobile-navbar';
export const AffineOtherPageLayout = ({
isSmallScreen,
children,
}: {
isSmallScreen: boolean;
children: React.ReactNode;
}) => {
const t = useAFFiNEI18N();
const openDownloadLink = useCallback(() => {
open(runtimeConfig.downloadUrl, '_blank');
}, []);
return (
<div className={styles.root}>
<div
className={clsx(styles.topNav, {
mobile: isSmallScreen,
})}
>
<a href="/" rel="noreferrer" className={styles.affineLogo}>
<Logo1Icon width={24} height={24} />
</a>
{isSmallScreen ? (
<MobileNavbar />
) : (
<>
<DesktopNavbar />
<Button onClick={openDownloadLink}>
{t['com.affine.auth.open.affine.download-app']()}
</Button>
</>
)}
</div>
{children}
</div>
);
};
@@ -0,0 +1,50 @@
import { IconButton } from '@affine/component/ui/button';
import { Menu, MenuItem } from '@affine/component/ui/menu';
import { CloseIcon, PropertyIcon } from '@blocksuite/icons';
import { useState } from 'react';
import * as styles from './index.css';
import { useNavConfig } from './use-nav-config';
export const MobileNavbar = () => {
const [openMenu, setOpenMenu] = useState(false);
const navConfig = useNavConfig();
const menuItems = (
<>
{navConfig.map(item => {
return (
<MenuItem
key={item.title}
onClick={() => {
open(item.path, '_blank');
}}
className={styles.menuItem}
>
{item.title}
</MenuItem>
);
})}
</>
);
return (
<div>
<Menu
items={menuItems}
contentOptions={{
className: styles.menu,
sideOffset: 20,
}}
rootOptions={{
open: openMenu,
onOpenChange: setOpenMenu,
}}
>
<IconButton type="plain" className={styles.iconButton}>
{openMenu ? <CloseIcon /> : <PropertyIcon />}
</IconButton>
</Menu>
</div>
);
};
@@ -0,0 +1,27 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMemo } from 'react';
export const useNavConfig = () => {
const t = useAFFiNEI18N();
return useMemo(
() => [
{
title: t['com.affine.other-page.nav.official-website'](),
path: 'https://affine.pro',
},
{
title: t['com.affine.other-page.nav.affine-community'](),
path: 'https://community.affine.pro/home',
},
{
title: t['com.affine.other-page.nav.blog'](),
path: 'https://affine.pro/blog',
},
{
title: t['com.affine.other-page.nav.contact-us'](),
path: 'https://affine.pro/about-us',
},
],
[t]
);
};
@@ -85,12 +85,12 @@ export const installLabel = style({
flex: 1,
fontSize: 'var(--affine-font-sm)',
whiteSpace: 'nowrap',
justifyContent: 'space-between',
});
export const installLabelNormal = style([
installLabel,
{
justifyContent: 'space-between',
selectors: {
[`${root}:hover &, ${root}[data-updating=true] &`]: {
display: 'none',
@@ -103,6 +103,7 @@ export const installLabelHover = style([
installLabel,
{
display: 'none',
justifyContent: 'flex-start',
selectors: {
[`${root}:hover &, ${root}[data-updating=true] &`]: {
display: 'flex',
@@ -1,20 +1,11 @@
import { Unreachable } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
import { Tooltip } from '@toeverything/components/tooltip';
import {
changelogCheckedAtom,
currentChangelogUnreadAtom,
currentVersionAtom,
downloadProgressAtom,
updateAvailableAtom,
updateReadyAtom,
useAppUpdater,
} from '@toeverything/hooks/use-app-updater';
import { useAppUpdater } from '@toeverything/hooks/use-app-updater';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import { startTransition, useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { Tooltip } from '../../../ui/tooltip';
import * as styles from './index.css';
export interface AddPageButtonPureProps {
@@ -26,36 +17,178 @@ export interface AddPageButtonPureProps {
version: string;
allowAutoUpdate: boolean;
} | null;
autoDownload: boolean;
downloadProgress: number | null;
appQuitting: boolean;
className?: string;
style?: React.CSSProperties;
}
interface ButtonContentProps {
updateReady: boolean;
updateAvailable: {
version: string;
allowAutoUpdate: boolean;
} | null;
autoDownload: boolean;
downloadProgress: number | null;
appQuitting: boolean;
currentChangelogUnread: boolean;
onDismissCurrentChangelog: () => void;
}
function DownloadUpdate({ updateAvailable }: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<div className={styles.installLabel}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.downloadUpdate']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
);
}
function UpdateReady({ updateAvailable, appQuitting }: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<div className={styles.updateAvailableWrapper}>
<div className={styles.installLabelNormal}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
<div className={styles.installLabelHover}>
<ResetIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t[appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate']()}
</span>
</div>
</div>
);
}
function DownloadingUpdate({
updateAvailable,
downloadProgress,
}: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<div className={clsx([styles.updateAvailableWrapper])}>
<div className={clsx([styles.installLabelNormal])}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.downloading']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
<div className={styles.progress}>
<div
className={styles.progressInner}
style={{ width: `${downloadProgress}%` }}
/>
</div>
</div>
);
}
function OpenDownloadPage({ updateAvailable }: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<>
<div className={styles.installLabelNormal}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
<div className={styles.installLabelHover}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.openDownloadPage']()}
</span>
</div>
</>
);
}
function WhatsNew({ onDismissCurrentChangelog }: ButtonContentProps) {
const t = useAFFiNEI18N();
const onClickClose: React.MouseEventHandler = useCallback(
e => {
onDismissCurrentChangelog();
e.stopPropagation();
},
[onDismissCurrentChangelog]
);
return (
<>
<div className={clsx([styles.whatsNewLabel])}>
<NewIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.whatsNew']()}
</span>
</div>
<div className={styles.closeIcon} onClick={onClickClose}>
<CloseIcon />
</div>
</>
);
}
const getButtonContentRenderer = (props: ButtonContentProps) => {
if (props.updateReady) {
return UpdateReady;
} else if (props.updateAvailable?.allowAutoUpdate) {
if (props.autoDownload && props.updateAvailable.allowAutoUpdate) {
return DownloadingUpdate;
} else {
return DownloadUpdate;
}
} else if (props.updateAvailable && !props.updateAvailable?.allowAutoUpdate) {
return OpenDownloadPage;
} else if (props.currentChangelogUnread) {
return WhatsNew;
}
return null;
};
export function AppUpdaterButtonPure({
updateReady,
onClickUpdate,
onDismissCurrentChangelog,
currentChangelogUnread,
updateAvailable,
autoDownload,
downloadProgress,
appQuitting,
className,
style,
}: AddPageButtonPureProps) {
const t = useAFFiNEI18N();
const contentProps = useMemo(
() => ({
updateReady,
updateAvailable,
currentChangelogUnread,
autoDownload,
downloadProgress,
appQuitting,
onDismissCurrentChangelog,
}),
[
updateReady,
updateAvailable,
currentChangelogUnread,
autoDownload,
downloadProgress,
appQuitting,
onDismissCurrentChangelog,
]
);
if (!updateAvailable && !currentChangelogUnread) {
return null;
}
const updateAvailableNode = updateAvailable
? updateAvailable.allowAutoUpdate
? renderUpdateAvailableAllowAutoUpdate()
: renderUpdateAvailableNotAllowAutoUpdate()
: null;
const whatsNew =
!updateAvailable && currentChangelogUnread ? renderWhatsNew() : null;
const ContentComponent = getButtonContentRenderer(contentProps);
const wrapWithTooltip = (
node: React.ReactElement,
@@ -72,102 +205,38 @@ export function AppUpdaterButtonPure({
);
};
const disabled = useMemo(() => {
if (appQuitting) {
return true;
}
if (updateAvailable?.allowAutoUpdate) {
return !updateReady && autoDownload;
}
return false;
}, [
appQuitting,
autoDownload,
updateAvailable?.allowAutoUpdate,
updateReady,
]);
return wrapWithTooltip(
<button
style={style}
className={clsx([styles.root, className])}
data-has-update={!!updateAvailable}
data-updating={appQuitting}
data-disabled={
(updateAvailable?.allowAutoUpdate && !updateReady) || appQuitting
}
data-disabled={disabled}
onClick={onClickUpdate}
>
{updateAvailableNode}
{whatsNew}
{ContentComponent ? <ContentComponent {...contentProps} /> : null}
<div className={styles.particles} aria-hidden="true"></div>
<span className={styles.halo} aria-hidden="true"></span>
</button>,
updateAvailable?.version
);
function renderUpdateAvailableAllowAutoUpdate() {
return (
<div className={clsx([styles.updateAvailableWrapper])}>
<div className={clsx([styles.installLabelNormal])}>
<span className={styles.ellipsisTextOverflow}>
{!updateReady
? t['com.affine.appUpdater.downloading']()
: t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>
{updateAvailable?.version}
</span>
</div>
{updateReady ? (
<div className={clsx([styles.installLabelHover])}>
<ResetIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t[
appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate'
]()}
</span>
</div>
) : (
<div className={styles.progress}>
<div
className={styles.progressInner}
style={{ width: `${downloadProgress}%` }}
/>
</div>
)}
</div>
);
}
function renderUpdateAvailableNotAllowAutoUpdate() {
return (
<>
<div className={clsx([styles.installLabelNormal])}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>
{updateAvailable?.version}
</span>
</div>
<div className={clsx([styles.installLabelHover])}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.openDownloadPage']()}
</span>
</div>
</>
);
}
function renderWhatsNew() {
return (
<>
<div className={clsx([styles.whatsNewLabel])}>
<NewIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.whatsNew']()}
</span>
</div>
<div
className={styles.closeIcon}
onClick={e => {
onDismissCurrentChangelog();
e.stopPropagation();
}}
>
<CloseIcon />
</div>
</>
);
}
}
// Although it is called an input, it is actually a button.
@@ -178,62 +247,64 @@ export function AppUpdaterButton({
className?: string;
style?: React.CSSProperties;
}) {
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
const updateReady = useAtomValue(updateReadyAtom);
const updateAvailable = useAtomValue(updateAvailableAtom);
const downloadProgress = useAtomValue(downloadProgressAtom);
const currentVersion = useAtomValue(currentVersionAtom);
const { quitAndInstall, appQuitting } = useAppUpdater();
const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom);
const dismissCurrentChangelog = useCallback(() => {
if (!currentVersion) {
return;
}
startTransition(() =>
setChangelogCheckAtom(mapping => {
return {
...mapping,
[currentVersion]: true,
};
})
);
}, [currentVersion, setChangelogCheckAtom]);
const {
quitAndInstall,
appQuitting,
autoDownload,
downloadUpdate,
readChangelog,
changelogUnread,
updateReady,
updateAvailable,
downloadProgress,
currentVersion,
} = useAppUpdater();
const handleClickUpdate = useCallback(() => {
if (updateReady) {
quitAndInstall();
} else if (updateAvailable) {
if (updateAvailable.allowAutoUpdate) {
// wait for download to finish
if (autoDownload) {
// wait for download to finish
} else {
downloadUpdate();
}
} else {
window.open(
`https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`,
'_blank'
);
}
} else if (currentChangelogUnread) {
} else if (changelogUnread) {
window.open(runtimeConfig.changelogUrl, '_blank');
dismissCurrentChangelog();
readChangelog();
} else {
throw new Unreachable();
}
}, [
updateReady,
quitAndInstall,
updateAvailable,
currentChangelogUnread,
dismissCurrentChangelog,
changelogUnread,
quitAndInstall,
autoDownload,
downloadUpdate,
currentVersion,
readChangelog,
]);
if (!updateAvailable && !changelogUnread) {
return null;
}
return (
<AppUpdaterButtonPure
appQuitting={appQuitting}
autoDownload={autoDownload}
updateReady={!!updateReady}
onClickUpdate={handleClickUpdate}
onDismissCurrentChangelog={dismissCurrentChangelog}
currentChangelogUnread={currentChangelogUnread}
onDismissCurrentChangelog={readChangelog}
currentChangelogUnread={changelogUnread}
updateAvailable={updateAvailable}
downloadProgress={downloadProgress}
className={className}
@@ -1,10 +1,10 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { IconButton } from '../../../ui/button';
import { Tooltip } from '../../../ui/tooltip';
import type { History } from '..';
import {
navHeaderButton,
@@ -1,9 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SidebarIcon } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useAtom } from 'jotai';
import { IconButton } from '../../../ui/button';
import { Tooltip } from '../../../ui/tooltip';
import { appSidebarOpenAtom } from '../index.jotai';
import * as styles from './sidebar-switch.css';
@@ -2,7 +2,7 @@ import clsx from 'clsx';
import type { FC, HTMLAttributes } from 'react';
import { Input, type InputProps } from '../../ui/input';
import { authInputWrapper, formHint } from './share.css';
import * as styles from './share.css';
export type AuthInputProps = InputProps & {
label?: string;
error?: boolean;
@@ -22,13 +22,14 @@ export const AuthInput: FC<AuthInputProps> = ({
}) => {
return (
<div
className={clsx(authInputWrapper, className, {
className={clsx(styles.authInputWrapper, className, {
'without-hint': withoutHint,
})}
{...otherWrapperProps}
>
{label ? <label>{label}</label> : null}
<Input
className={styles.input}
size="extraLarge"
status={error ? 'error' : 'default'}
onKeyDown={e => {
@@ -40,7 +41,7 @@ export const AuthInput: FC<AuthInputProps> = ({
/>
{error && errorHint && !withoutHint ? (
<div
className={clsx(formHint, {
className={clsx(styles.formHint, {
error: error,
})}
>
@@ -1,32 +1,41 @@
import type { FC, PropsWithChildren, ReactNode } from 'react';
import {
type FC,
type PropsWithChildren,
type ReactNode,
useEffect,
useState,
} from 'react';
import { Empty } from '../../ui/empty';
import { Wrapper } from '../../ui/layout';
import { Logo } from './logo';
import { AffineOtherPageLayout } from '../affine-other-page-layout';
import { authPageContainer } from './share.css';
export const AuthPageContainer: FC<
PropsWithChildren<{ title?: ReactNode; subtitle?: ReactNode }>
> = ({ children, title, subtitle }) => {
const [isSmallScreen, setIsSmallScreen] = useState(false);
useEffect(() => {
const checkScreenSize = () => {
setIsSmallScreen(window.innerWidth <= 1024);
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () => window.removeEventListener('resize', checkScreenSize);
}, []);
return (
<div className={authPageContainer}>
<Wrapper
style={{
position: 'absolute',
top: 25,
left: 20,
}}
>
<Logo />
</Wrapper>
<div className="wrapper">
<div className="content">
<p className="title">{title}</p>
<p className="subtitle">{subtitle}</p>
{children}
<AffineOtherPageLayout isSmallScreen={isSmallScreen}>
<div className={authPageContainer}>
<div className="wrapper">
<div className="content">
<p className="title">{title}</p>
<p className="subtitle">{subtitle}</p>
{children}
</div>
{isSmallScreen ? null : <Empty />}
</div>
<Empty />
</div>
</div>
</AffineOtherPageLayout>
);
};
@@ -1,8 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowLeftSmallIcon } from '@blocksuite/icons';
import { Button, type ButtonProps } from '@toeverything/components/button';
import { type FC } from 'react';
import { Button, type ButtonProps } from '../../ui/button';
export const BackButton: FC<ButtonProps> = props => {
const t = useAFFiNEI18N();
return (
@@ -1,7 +1,7 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useCallback, useState } from 'react';
import { Button } from '../../ui/button';
import { AuthInput } from './auth-input';
import { AuthPageContainer } from './auth-page-container';
import { emailRegex } from './utils';
@@ -44,7 +44,6 @@ export const ChangeEmailPage = ({
>
<>
<AuthInput
width={320}
label={t['com.affine.settings.email']()}
placeholder={t['com.affine.auth.sign.email.placeholder']()}
value={email}
@@ -1,9 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { Button } from '../../ui/button';
import { pushNotificationAtom } from '../notification-center';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
@@ -1,7 +1,7 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { Button } from '../../ui/button';
import { AuthPageContainer } from './auth-page-container';
export const ConfirmChangeEmail: FC<{
@@ -1,6 +1,7 @@
import { Modal } from '@toeverything/components/modal';
import type { FC, PropsWithChildren } from 'react';
import { Modal } from '../../ui/modal';
export type AuthModalProps = {
open: boolean;
setOpen: (value: boolean) => void;
@@ -4,6 +4,7 @@ import { type FC, useEffect } from 'react';
import { useCallback, useState } from 'react';
import { Input, type InputProps } from '../../../ui/input';
import * as styles from '../share.css';
import { ErrorIcon } from './error';
import { SuccessIcon } from './success';
import { Tag } from './tag';
@@ -74,6 +75,7 @@ export const PasswordInput: FC<
return (
<>
<Input
className={styles.input}
type="password"
size="extraLarge"
style={{ marginBottom: 20 }}
@@ -83,6 +85,7 @@ export const PasswordInput: FC<
{...inputProps}
/>
<Input
className={styles.input}
type="password"
size="extraLarge"
placeholder={t['com.affine.auth.set.password.placeholder.confirm']()}
@@ -1,9 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { Button } from '../../ui/button';
import { pushNotificationAtom } from '../notification-center';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
@@ -1,7 +1,7 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { type FC, useCallback, useRef, useState } from 'react';
import { Button } from '../../ui/button';
import { Wrapper } from '../../ui/layout';
import { PasswordInput } from './password-input';
@@ -19,7 +19,6 @@ export const SetPassword: FC<{
<>
<Wrapper marginTop={30} marginBottom={42}>
<PasswordInput
width={320}
onPass={useCallback(password => {
setPasswordPass(true);
passwordRef.current = password;
@@ -151,14 +151,25 @@ export const authPageContainer = style({
justifyContent: 'center',
alignItems: 'center',
fontSize: 'var(--affine-font-base)',
'@media': {
'screen and (max-width: 1024px)': {
flexDirection: 'column',
padding: '100px 20px',
justifyContent: 'flex-start',
},
},
});
globalStyle(`${authPageContainer} .wrapper`, {
display: 'flex',
alignItems: 'center',
'@media': {
'screen and (max-width: 1024px)': {
flexDirection: 'column',
},
},
});
globalStyle(`${authPageContainer} .content`, {
maxWidth: '700px',
minWidth: '550px',
});
globalStyle(`${authPageContainer} .title`, {
@@ -178,3 +189,12 @@ export const signInPageContainer = style({
width: '400px',
margin: '205px auto 0',
});
export const input = style({
width: '330px',
'@media': {
'screen and (max-width: 520px)': {
width: '100%',
},
},
});
@@ -1,7 +1,7 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { Button } from '../../ui/button';
import { AuthPageContainer } from './auth-page-container';
export const SignInSuccessPage: FC<{
@@ -1,9 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { Button } from '../../ui/button';
import { pushNotificationAtom } from '../notification-center';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
@@ -1,20 +0,0 @@
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
import { useEffect, useRef } from 'react';
export const RootBlockHub = () => {
const ref = useRef<HTMLDivElement>(null);
const blockHub = useAtomValue(rootBlockHubAtom);
useEffect(() => {
if (ref.current) {
const div = ref.current;
if (blockHub) {
if (div.hasChildNodes()) {
(div.firstChild as ChildNode).remove();
}
div.append(blockHub);
}
}
}, [blockHub]);
return <div ref={ref} data-testid="block-hub" />;
};
@@ -1,5 +1,5 @@
import { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import { EditorContainer } from '@blocksuite/presets';
import type { Page } from '@blocksuite/store';
import clsx from 'clsx';
import { use } from 'foxact/use';
@@ -2,16 +2,16 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Divider } from '@toeverything/components/divider';
import { Tooltip } from '@toeverything/components/tooltip';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { useAtomValue } from 'jotai/react';
import { useCallback } from 'react';
import { Avatar } from '../../../ui/avatar';
import { Divider } from '../../../ui/divider';
import { Skeleton } from '../../../ui/skeleton';
import { Tooltip } from '../../../ui/tooltip';
import {
StyledCard,
StyledIconContainer,
@@ -1,6 +1,5 @@
import { IconButton } from '@toeverything/components/button';
import { displayFlex, styled, textEllipsis } from '../../../styles';
import { IconButton } from '../../../ui/button';
export const StyledWorkspaceInfo = styled('div')(() => {
return {
@@ -1,8 +1,6 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ConfirmModal,
type ConfirmModalProps,
} from '@toeverything/components/modal';
import { ConfirmModal, type ConfirmModalProps } from '../../ui/modal';
export const PublicLinkDisableModal = (props: ConfirmModalProps) => {
const t = useAFFiNEI18N();
@@ -6,9 +6,9 @@ import {
NewIcon,
NotionIcon,
} from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { IconButton } from '../../ui/button';
import { Tooltip } from '../../ui/tooltip';
import { BlockCard } from '../card/block-card';
import {
importPageBodyStyle,
@@ -1,9 +1,9 @@
import { AuthPageContainer } from '@affine/component/auth-components';
import { type GetInviteInfoQuery } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button';
import { Avatar } from '../../ui/avatar';
import { Button } from '../../ui/button';
import { FlexWrapper } from '../../ui/layout';
import * as styles from './styles.css';
export const AcceptInvitePage = ({
@@ -1,8 +1,8 @@
import { Permission } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ConfirmModal } from '@toeverything/components/modal';
import { useCallback, useEffect, useState } from 'react';
import { ConfirmModal } from '../../ui/modal';
import { AuthInput } from '..//auth-components';
import { emailRegex } from '..//auth-components/utils';
@@ -1,9 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SignOutIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button, IconButton } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { Avatar } from '../../ui/avatar';
import { Button, IconButton } from '../../ui/button';
import { Tooltip } from '../../ui/tooltip';
import { NotFoundPattern } from './not-found-pattern';
import {
largeButtonEffect,
@@ -8,6 +8,7 @@ export const notFoundPageContainer = style({
alignItems: 'center',
justifyContent: 'center',
width: '100vw',
padding: '0 20px',
});
export const wrapper = style({

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