mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 07:36:42 +08:00
Merge branch 'canary' into stable
This commit is contained in:
@@ -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:*'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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/**/*'
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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://affine-storybook.vercel.app/) |
|
||||
| [@toeverything/y-indexeddb](packages/common/y-indexeddb) | IndexedDB database adapter for Yjs | [](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
|
||||
| [@toeverything/theme](packages/common/theme) | AFFiNE theme | [](https://www.npmjs.com/package/@toeverything/theme) |
|
||||
| Name | | |
|
||||
| -------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [@affine/component](packages/frontend/component) | AFFiNE Component Resources | [](https://affine-storybook.vercel.app/) |
|
||||
| [@toeverything/y-indexeddb](packages/common/y-indexeddb) | IndexedDB database adapter for Yjs | [](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
|
||||
| [@toeverything/theme](packages/common/theme) | AFFiNE theme | [](https://www.npmjs.com/package/@toeverything/theme) |
|
||||
|
||||
## Plugins
|
||||
|
||||
|
||||
+5
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+19
-16
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+3
-4
@@ -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"
|
||||
|
||||
Vendored
+4
-6
@@ -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(),
|
||||
|
||||
Vendored
+1
-22
@@ -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>;
|
||||
|
||||
Vendored
-3
@@ -7,9 +7,6 @@
|
||||
"outDir": "lib"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../infra"
|
||||
},
|
||||
{
|
||||
"path": "../../../tests/fixtures"
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+29
-5
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
{
|
||||
"path": "../sdk"
|
||||
},
|
||||
{
|
||||
"path": "../env"
|
||||
},
|
||||
{
|
||||
"path": "../debug"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
|
||||
+24
@@ -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]
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -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',
|
||||
|
||||
+207
-136
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
})}
|
||||
>
|
||||
|
||||
+29
-20
@@ -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
Reference in New Issue
Block a user