mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +08:00
Compare commits
39 Commits
v0.26.3-be
...
v0.26.3-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79460072bb | ||
|
|
41b3b0e82e | ||
|
|
9c99293c92 | ||
|
|
6aba4350ac | ||
|
|
046e126054 | ||
|
|
c2c7dde06c | ||
|
|
5fb1c11a96 | ||
|
|
3e39dbb298 | ||
|
|
e617740974 | ||
|
|
744c78abbb | ||
|
|
91c5869053 | ||
|
|
6d805b302c | ||
|
|
fb9f49b948 | ||
|
|
ef6717e59a | ||
|
|
ad988dbd1e | ||
|
|
3d01766f55 | ||
|
|
2414aa5848 | ||
|
|
0de1bd0da8 | ||
|
|
186ec5431d | ||
|
|
da57bfe8e7 | ||
|
|
c9bffc13b5 | ||
|
|
d8cc0acdd0 | ||
|
|
35e1411407 | ||
|
|
8f833388eb | ||
|
|
850e646ab9 | ||
|
|
728e02cab7 | ||
|
|
792164edd1 | ||
|
|
e3177e6837 | ||
|
|
42f2d2b337 | ||
|
|
9d7f4acaf1 | ||
|
|
9a1f600fc9 | ||
|
|
0f906ad623 | ||
|
|
09aa65c52a | ||
|
|
25227a09f7 | ||
|
|
c0694c589b | ||
|
|
819402d9f1 | ||
|
|
33bc3e2fe9 | ||
|
|
2b71b3f345 | ||
|
|
3bc28ba78c |
@@ -222,7 +222,7 @@
|
|||||||
},
|
},
|
||||||
"SMTP.sender": {
|
"SMTP.sender": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`",
|
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`",
|
||||||
"default": "AFFiNE Self Hosted <noreply@example.com>"
|
"default": "AFFiNE Self Hosted <noreply@example.com>"
|
||||||
},
|
},
|
||||||
"SMTP.ignoreTLS": {
|
"SMTP.ignoreTLS": {
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
},
|
},
|
||||||
"fallbackSMTP.sender": {
|
"fallbackSMTP.sender": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
|
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"fallbackSMTP.ignoreTLS": {
|
"fallbackSMTP.ignoreTLS": {
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,6 +3,6 @@ contact_links:
|
|||||||
- name: Something else?
|
- name: Something else?
|
||||||
url: https://github.com/toeverything/AFFiNE/discussions
|
url: https://github.com/toeverything/AFFiNE/discussions
|
||||||
about: Feel free to ask and answer questions over in GitHub Discussions
|
about: Feel free to ask and answer questions over in GitHub Discussions
|
||||||
- name: AFFiNE Community Support
|
- name: AFFiNE Community Support (Discord)
|
||||||
url: https://community.affine.pro
|
url: https://affine.pro/redirect/discord
|
||||||
about: AFFiNE Community - a place to ask, learn and engage with others
|
about: AFFiNE Community - a place to ask, learn and engage with others
|
||||||
|
|||||||
20
.github/actionlint.yaml
vendored
Normal file
20
.github/actionlint.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
self-hosted-runner:
|
||||||
|
# Labels of self-hosted runner in array of strings.
|
||||||
|
labels:
|
||||||
|
- win-signer
|
||||||
|
|
||||||
|
# Configuration variables in array of strings defined in your repository or
|
||||||
|
# organization. `null` means disabling configuration variables check.
|
||||||
|
# Empty array means no configuration variable is allowed.
|
||||||
|
config-variables: null
|
||||||
|
|
||||||
|
# Configuration for file paths. The keys are glob patterns to match to file
|
||||||
|
# paths relative to the repository root. The values are the configurations for
|
||||||
|
# the file paths. Note that the path separator is always '/'.
|
||||||
|
# The following configurations are available.
|
||||||
|
#
|
||||||
|
# "ignore" is an array of regular expression patterns. Matched error messages
|
||||||
|
# are ignored. This is similar to the "-ignore" command line option.
|
||||||
|
paths:
|
||||||
|
# .github/workflows/**/*.yml:
|
||||||
|
# ignore: []
|
||||||
1
.github/actions/setup-version/action.yml
vendored
1
.github/actions/setup-version/action.yml
vendored
@@ -7,7 +7,6 @@ inputs:
|
|||||||
ios-app-version:
|
ios-app-version:
|
||||||
description: 'iOS App Store Version (Optional, use App version if empty)'
|
description: 'iOS App Store Version (Optional, use App version if empty)'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
|
||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
2
.github/helm/affine/Chart.yaml
vendored
2
.github/helm/affine/Chart.yaml
vendored
@@ -3,4 +3,4 @@ name: affine
|
|||||||
description: AFFiNE cloud chart
|
description: AFFiNE cloud chart
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.1"
|
appVersion: "0.26.3"
|
||||||
|
|||||||
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: doc
|
|||||||
description: AFFiNE doc server
|
description: AFFiNE doc server
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.1"
|
appVersion: "0.26.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: gcloud-sql-proxy
|
- name: gcloud-sql-proxy
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|||||||
2
.github/helm/affine/charts/front/Chart.yaml
vendored
2
.github/helm/affine/charts/front/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: front
|
|||||||
description: AFFiNE front server
|
description: AFFiNE front server
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.1"
|
appVersion: "0.26.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: gcloud-sql-proxy
|
- name: gcloud-sql-proxy
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|||||||
@@ -96,12 +96,20 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /info
|
path: /info
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
initialDelaySeconds: {{ default .Values.probe.initialDelaySeconds .Values.probe.liveness.initialDelaySeconds }}
|
||||||
|
timeoutSeconds: {{ default .Values.probe.timeoutSeconds .Values.probe.liveness.timeoutSeconds }}
|
||||||
|
periodSeconds: {{ default .Values.probe.periodSeconds .Values.probe.liveness.periodSeconds }}
|
||||||
|
failureThreshold: {{ default .Values.probe.failureThreshold .Values.probe.liveness.failureThreshold }}
|
||||||
|
successThreshold: {{ default .Values.probe.successThreshold .Values.probe.liveness.successThreshold }}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /info
|
path: /info
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
initialDelaySeconds: {{ default .Values.probe.initialDelaySeconds .Values.probe.readiness.initialDelaySeconds }}
|
||||||
|
timeoutSeconds: {{ default .Values.probe.timeoutSeconds .Values.probe.readiness.timeoutSeconds }}
|
||||||
|
periodSeconds: {{ default .Values.probe.periodSeconds .Values.probe.readiness.periodSeconds }}
|
||||||
|
failureThreshold: {{ default .Values.probe.failureThreshold .Values.probe.readiness.failureThreshold }}
|
||||||
|
successThreshold: {{ default .Values.probe.successThreshold .Values.probe.readiness.successThreshold }}
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
{{- with .Values.nodeSelector }}
|
{{- with .Values.nodeSelector }}
|
||||||
|
|||||||
10
.github/helm/affine/charts/front/values.yaml
vendored
10
.github/helm/affine/charts/front/values.yaml
vendored
@@ -31,13 +31,21 @@ podSecurityContext:
|
|||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: '1'
|
cpu: '1'
|
||||||
memory: 2Gi
|
memory: 4Gi
|
||||||
requests:
|
requests:
|
||||||
cpu: '1'
|
cpu: '1'
|
||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
|
|
||||||
probe:
|
probe:
|
||||||
initialDelaySeconds: 20
|
initialDelaySeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 6
|
||||||
|
successThreshold: 1
|
||||||
|
liveness:
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
failureThreshold: 12
|
||||||
|
readiness: {}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
sync:
|
sync:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: graphql
|
|||||||
description: AFFiNE GraphQL server
|
description: AFFiNE GraphQL server
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.1"
|
appVersion: "0.26.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: gcloud-sql-proxy
|
- name: gcloud-sql-proxy
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|||||||
6
.github/workflows/auto-labeler.yml
vendored
6
.github/workflows/auto-labeler.yml
vendored
@@ -1,6 +1,10 @@
|
|||||||
name: 'Pull Request Labeler'
|
name: 'Pull Request Labeler'
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
|
|||||||
394
.github/workflows/build-test.yml
vendored
394
.github/workflows/build-test.yml
vendored
@@ -68,9 +68,26 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Go (for actionlint)
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Install actionlint
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.11
|
||||||
|
- name: Run actionlint
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
"$(go env GOPATH)/bin/actionlint"
|
||||||
- name: Run oxlint
|
- name: Run oxlint
|
||||||
# oxlint is fast, so wrong code will fail quickly
|
# oxlint is fast, so wrong code will fail quickly
|
||||||
run: yarn dlx $(node -e "console.log(require('./package.json').scripts['lint:ox'].replace('oxlint', 'oxlint@' + require('./package.json').devDependencies.oxlint))")
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
oxlint_version="$(node -e "console.log(require('./package.json').devDependencies.oxlint)")"
|
||||||
|
yarn dlx "oxlint@${oxlint_version}" --deny-warnings
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
@@ -108,20 +125,45 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn affine bs-docs build
|
yarn affine bs-docs build
|
||||||
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
||||||
git status --porcelain | grep . && {
|
if git status --porcelain | grep -q .; then
|
||||||
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
|
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
|
||||||
exit 1
|
exit 1
|
||||||
} || {
|
else
|
||||||
echo "All changes are submitted"
|
echo "All changes are submitted"
|
||||||
}
|
fi
|
||||||
|
|
||||||
|
rust-test-filter:
|
||||||
|
name: Rust test filter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
run-rust: ${{ steps.rust-filter.outputs.rust }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: rust-filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
rust:
|
||||||
|
- '**/*.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '.cargo/**'
|
||||||
|
- 'rust-toolchain*'
|
||||||
|
- '.github/actions/build-rust/**'
|
||||||
|
|
||||||
lint-rust:
|
lint-rust:
|
||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
|
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- rust-test-filter
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/build-rust
|
- uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
package: 'affine'
|
||||||
no-build: 'true'
|
no-build: 'true'
|
||||||
- name: fmt check
|
- name: fmt check
|
||||||
run: |
|
run: |
|
||||||
@@ -159,12 +201,12 @@ jobs:
|
|||||||
yarn affine i18n build
|
yarn affine i18n build
|
||||||
yarn affine server genconfig
|
yarn affine server genconfig
|
||||||
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
||||||
git status --porcelain | grep . && {
|
if git status --porcelain | grep -q .; then
|
||||||
echo "Run 'yarn affine init && yarn affine gql build && yarn affine i18n build && yarn affine server genconfig' and make sure all changes are submitted"
|
echo "Run 'yarn affine init && yarn affine gql build && yarn affine i18n build && yarn affine server genconfig' and make sure all changes are submitted"
|
||||||
exit 1
|
exit 1
|
||||||
} || {
|
else
|
||||||
echo "All changes are submitted"
|
echo "All changes are submitted"
|
||||||
}
|
fi
|
||||||
|
|
||||||
check-yarn-binary:
|
check-yarn-binary:
|
||||||
name: Check yarn binary
|
name: Check yarn binary
|
||||||
@@ -173,7 +215,9 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Run check
|
- name: Run check
|
||||||
run: |
|
run: |
|
||||||
yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
|
set -euo pipefail
|
||||||
|
yarn_version="$(node -e "console.log(require('./package.json').packageManager.split('@')[1])")"
|
||||||
|
yarn set version "$yarn_version"
|
||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
|
|
||||||
e2e-blocksuite-test:
|
e2e-blocksuite-test:
|
||||||
@@ -188,6 +232,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-test/blocksuite @blocksuite/playground @blocksuite/integration-test
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
playwright-platform: 'chromium'
|
playwright-platform: 'chromium'
|
||||||
electron-install: false
|
electron-install: false
|
||||||
@@ -210,18 +255,14 @@ jobs:
|
|||||||
e2e-blocksuite-cross-browser-test:
|
e2e-blocksuite-cross-browser-test:
|
||||||
name: E2E BlockSuite Cross Browser Test
|
name: E2E BlockSuite Cross Browser Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
shard: [1]
|
|
||||||
browser: ['chromium', 'firefox', 'webkit']
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-test/blocksuite @blocksuite/playground @blocksuite/integration-test
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
playwright-platform: ${{ matrix.browser }}
|
playwright-platform: 'chromium,firefox,webkit'
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
@@ -229,18 +270,64 @@ jobs:
|
|||||||
run: yarn workspace @blocksuite/playground build
|
run: yarn workspace @blocksuite/playground build
|
||||||
|
|
||||||
- name: Run playwright tests
|
- name: Run playwright tests
|
||||||
env:
|
run: |
|
||||||
BROWSER: ${{ matrix.browser }}
|
yarn workspace @blocksuite/integration-test test:unit
|
||||||
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results-e2e-bs-cross-browser-${{ matrix.browser }}-${{ matrix.shard }}
|
name: test-results-e2e-bs-cross-browser
|
||||||
path: ./test-results
|
path: ./test-results
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
bundler-matrix:
|
||||||
|
name: Bundler Matrix (${{ matrix.bundler }})
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
bundler: [webpack, rspack]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
playwright-install: false
|
||||||
|
electron-install: false
|
||||||
|
full-cache: true
|
||||||
|
|
||||||
|
- name: Run frontend build matrix
|
||||||
|
env:
|
||||||
|
AFFINE_BUNDLER: ${{ matrix.bundler }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
packages=(
|
||||||
|
"@affine/web"
|
||||||
|
"@affine/mobile"
|
||||||
|
"@affine/ios"
|
||||||
|
"@affine/android"
|
||||||
|
"@affine/admin"
|
||||||
|
"@affine/electron-renderer"
|
||||||
|
)
|
||||||
|
summary="test-results-bundler-${AFFINE_BUNDLER}.txt"
|
||||||
|
: > "$summary"
|
||||||
|
for pkg in "${packages[@]}"; do
|
||||||
|
start=$(date +%s)
|
||||||
|
yarn affine "$pkg" build
|
||||||
|
end=$(date +%s)
|
||||||
|
echo "${pkg},$((end-start))" >> "$summary"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Upload bundler timing
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-bundler-${{ matrix.bundler }}
|
||||||
|
path: ./test-results-bundler-${{ matrix.bundler }}.txt
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
e2e-test:
|
e2e-test:
|
||||||
name: E2E Test
|
name: E2E Test
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
@@ -257,6 +344,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-local @affine/web @affine/server
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
playwright-platform: 'chromium'
|
playwright-platform: 'chromium'
|
||||||
electron-install: false
|
electron-install: false
|
||||||
@@ -288,6 +376,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-mobile @affine/mobile
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
@@ -307,7 +396,7 @@ jobs:
|
|||||||
name: Unit Test
|
name: Unit Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-native
|
- build-native-linux
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
strategy:
|
strategy:
|
||||||
@@ -321,6 +410,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
electron-install: true
|
electron-install: true
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
|
playwright-platform: 'chromium,firefox,webkit'
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
- name: Download affine.linux-x64-gnu.node
|
- name: Download affine.linux-x64-gnu.node
|
||||||
@@ -341,7 +431,39 @@ jobs:
|
|||||||
name: affine
|
name: affine
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
||||||
build-native:
|
build-native-linux:
|
||||||
|
name: Build AFFiNE native (x86_64-unknown-linux-gnu)
|
||||||
|
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/native
|
||||||
|
electron-install: false
|
||||||
|
- name: Setup filename
|
||||||
|
id: filename
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
PLATFORM_ARCH_ABI="$(node -e "console.log(require('@napi-rs/cli').parseTriple('x86_64-unknown-linux-gnu').platformArchABI)")"
|
||||||
|
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Build AFFiNE native
|
||||||
|
uses: ./.github/actions/build-rust
|
||||||
|
with:
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
package: '@affine/native'
|
||||||
|
- name: Upload ${{ steps.filename.outputs.filename }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ${{ steps.filename.outputs.filename }}
|
||||||
|
path: ${{ github.workspace }}/packages/frontend/native/${{ steps.filename.outputs.filename }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-native-macos:
|
||||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
env:
|
env:
|
||||||
@@ -350,7 +472,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
spec:
|
spec:
|
||||||
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
|
||||||
- { os: macos-latest, target: x86_64-apple-darwin }
|
- { os: macos-latest, target: x86_64-apple-darwin }
|
||||||
- { os: macos-latest, target: aarch64-apple-darwin }
|
- { os: macos-latest, target: aarch64-apple-darwin }
|
||||||
|
|
||||||
@@ -366,7 +487,7 @@ jobs:
|
|||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
|
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"
|
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||||
- name: Build AFFiNE native
|
- name: Build AFFiNE native
|
||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
@@ -383,7 +504,7 @@ jobs:
|
|||||||
|
|
||||||
# Split Windows build because it's too slow
|
# Split Windows build because it's too slow
|
||||||
# and other ci jobs required linux native
|
# and other ci jobs required linux native
|
||||||
build-windows-native:
|
build-native-windows:
|
||||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
env:
|
env:
|
||||||
@@ -415,7 +536,7 @@ jobs:
|
|||||||
working-directory: ${{ env.DEV_DRIVE_WORKSPACE }}
|
working-directory: ${{ env.DEV_DRIVE_WORKSPACE }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
|
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"
|
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||||
- name: Build AFFiNE native
|
- name: Build AFFiNE native
|
||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
@@ -463,6 +584,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine/electron-renderer @affine/nbstore @toeverything/infra
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
- name: Build Electron renderer
|
- name: Build Electron renderer
|
||||||
@@ -483,7 +605,7 @@ jobs:
|
|||||||
name: Native Unit Test
|
name: Native Unit Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-native
|
- build-native-linux
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -544,6 +666,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
@@ -577,8 +700,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||||
@@ -626,6 +747,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
@@ -642,8 +764,6 @@ jobs:
|
|||||||
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only
|
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||||
CI_NODE_INDEX: ${{ matrix.node_index }}
|
|
||||||
CI_NODE_TOTAL: ${{ matrix.total_nodes }}
|
|
||||||
|
|
||||||
- name: Upload server test coverage results
|
- name: Upload server test coverage results
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
@@ -690,6 +810,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
@@ -716,7 +837,10 @@ jobs:
|
|||||||
|
|
||||||
miri:
|
miri:
|
||||||
name: miri code check
|
name: miri code check
|
||||||
|
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- rust-test-filter
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -741,7 +865,10 @@ jobs:
|
|||||||
|
|
||||||
loom:
|
loom:
|
||||||
name: loom thread test
|
name: loom thread test
|
||||||
|
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- rust-test-filter
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: --cfg loom
|
RUSTFLAGS: --cfg loom
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
@@ -764,7 +891,10 @@ jobs:
|
|||||||
|
|
||||||
fuzzing:
|
fuzzing:
|
||||||
name: fuzzing
|
name: fuzzing
|
||||||
|
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- rust-test-filter
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
steps:
|
steps:
|
||||||
@@ -800,7 +930,10 @@ jobs:
|
|||||||
|
|
||||||
rust-test:
|
rust-test:
|
||||||
name: Run native tests
|
name: Run native tests
|
||||||
|
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- rust-test-filter
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
steps:
|
steps:
|
||||||
@@ -808,6 +941,7 @@ jobs:
|
|||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
package: 'affine'
|
package: 'affine'
|
||||||
no-build: 'true'
|
no-build: 'true'
|
||||||
|
|
||||||
@@ -819,11 +953,51 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||||
|
|
||||||
|
copilot-test-filter:
|
||||||
|
name: Copilot test filter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
run-api: ${{ steps.decision.outputs.run_api }}
|
||||||
|
run-e2e: ${{ steps.decision.outputs.run_e2e }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: copilot-filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
api:
|
||||||
|
- 'packages/backend/server/src/plugins/copilot/**'
|
||||||
|
- 'packages/backend/server/tests/copilot.*'
|
||||||
|
e2e:
|
||||||
|
- 'packages/backend/server/src/plugins/copilot/**'
|
||||||
|
- 'packages/backend/server/tests/copilot.*'
|
||||||
|
- 'packages/frontend/core/src/blocksuite/ai/**'
|
||||||
|
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
||||||
|
- 'tests/affine-cloud-copilot/**'
|
||||||
|
|
||||||
|
- name: Decide test scope
|
||||||
|
id: decision
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.copilot-filter.outputs.api }}" == "true" ]]; then
|
||||||
|
echo "run_api=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "run_api=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${{ steps.copilot-filter.outputs.e2e }}" == "true" ]]; then
|
||||||
|
echo "run_e2e=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "run_e2e=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
copilot-api-test:
|
copilot-api-test:
|
||||||
name: Server Copilot Api Test
|
name: Server Copilot Api Test
|
||||||
|
if: ${{ needs.copilot-test-filter.outputs.run-api == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
|
- copilot-test-filter
|
||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
@@ -857,53 +1031,30 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check blocksuite update
|
|
||||||
id: check-blocksuite-update
|
|
||||||
env:
|
|
||||||
BASE_REF: ${{ github.base_ref }}
|
|
||||||
run: |
|
|
||||||
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
id: apifilter
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
changed:
|
|
||||||
- 'packages/backend/server/src/plugins/copilot/**'
|
|
||||||
- 'packages/backend/server/tests/copilot.*'
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
- name: Download server-native.node
|
- name: Download server-native.node
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: server-native.node
|
name: server-native.node
|
||||||
path: ./packages/backend/native
|
path: ./packages/backend/native
|
||||||
|
|
||||||
- name: Prepare Server Test Environment
|
- name: Prepare Server Test Environment
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
|
||||||
env:
|
env:
|
||||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||||
uses: ./.github/actions/server-test-env
|
uses: ./.github/actions/server-test-env
|
||||||
|
|
||||||
- name: Run server tests
|
- name: Run server tests
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
|
||||||
run: yarn affine @affine/server test:copilot:coverage --forbid-only
|
run: yarn affine @affine/server test:copilot:coverage --forbid-only
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||||
|
|
||||||
- name: Upload server test coverage results
|
- name: Upload server test coverage results
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -914,6 +1065,7 @@ jobs:
|
|||||||
|
|
||||||
copilot-e2e-test:
|
copilot-e2e-test:
|
||||||
name: Frontend Copilot E2E Test
|
name: Frontend Copilot E2E Test
|
||||||
|
if: ${{ needs.copilot-test-filter.outputs.run-e2e == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
@@ -928,6 +1080,7 @@ jobs:
|
|||||||
shardTotal: [5]
|
shardTotal: [5]
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
|
- copilot-test-filter
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: pgvector/pgvector:pg16
|
image: pgvector/pgvector:pg16
|
||||||
@@ -951,52 +1104,27 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check blocksuite update
|
|
||||||
id: check-blocksuite-update
|
|
||||||
env:
|
|
||||||
BASE_REF: ${{ github.base_ref }}
|
|
||||||
run: |
|
|
||||||
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
id: e2efilter
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
changed:
|
|
||||||
- 'packages/backend/server/src/plugins/copilot/**'
|
|
||||||
- 'packages/backend/server/tests/copilot.*'
|
|
||||||
- 'packages/frontend/core/src/blocksuite/ai/**'
|
|
||||||
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
|
||||||
- 'tests/affine-cloud-copilot/**'
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-cloud-copilot @affine/web @affine/server
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
playwright-platform: 'chromium'
|
playwright-platform: 'chromium'
|
||||||
electron-install: false
|
electron-install: false
|
||||||
hard-link-nm: false
|
hard-link-nm: false
|
||||||
|
|
||||||
- name: Download server-native.node
|
- name: Download server-native.node
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: server-native.node
|
name: server-native.node
|
||||||
path: ./packages/backend/native
|
path: ./packages/backend/native
|
||||||
|
|
||||||
- name: Prepare Server Test Environment
|
- name: Prepare Server Test Environment
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
|
||||||
env:
|
env:
|
||||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||||
uses: ./.github/actions/server-test-env
|
uses: ./.github/actions/server-test-env
|
||||||
|
|
||||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
|
||||||
uses: ./.github/actions/copilot-test
|
uses: ./.github/actions/copilot-test
|
||||||
with:
|
with:
|
||||||
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||||
@@ -1006,7 +1134,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-native
|
- build-native-linux
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||||
@@ -1062,7 +1190,10 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-cloud @affine-test/affine-desktop-cloud @affine/web @affine/server @affine/electron @affine/electron-renderer @affine/nbstore @toeverything/infra
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
|
playwright-platform: 'chromium'
|
||||||
|
electron-install: ${{ matrix.tests.shard == 'desktop' && 'true' || 'false' }}
|
||||||
hard-link-nm: false
|
hard-link-nm: false
|
||||||
|
|
||||||
- name: Download server-native.node
|
- name: Download server-native.node
|
||||||
@@ -1099,7 +1230,9 @@ jobs:
|
|||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
needs:
|
needs:
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
- build-native
|
- build-native-linux
|
||||||
|
- build-native-macos
|
||||||
|
- build-native-windows
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -1139,7 +1272,8 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
with:
|
with:
|
||||||
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop @affine/nbstore @toeverything/infra
|
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop @affine/nbstore @toeverything/infra
|
||||||
playwright-install: true
|
playwright-install: ${{ matrix.spec.test && 'true' || 'false' }}
|
||||||
|
playwright-platform: 'chromium'
|
||||||
hard-link-nm: false
|
hard-link-nm: false
|
||||||
enableScripts: false
|
enableScripts: false
|
||||||
|
|
||||||
@@ -1147,7 +1281,7 @@ jobs:
|
|||||||
id: filename
|
id: filename
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
|
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"
|
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Download ${{ steps.filename.outputs.filename }}
|
- name: Download ${{ steps.filename.outputs.filename }}
|
||||||
@@ -1182,84 +1316,6 @@ jobs:
|
|||||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||||
run: yarn affine @affine-test/affine-desktop e2e
|
run: yarn affine @affine-test/affine-desktop e2e
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
|
||||||
path: ./test-results
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
desktop-bundle-check:
|
|
||||||
name: Desktop bundle check (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
|
|
||||||
runs-on: ${{ matrix.spec.os }}
|
|
||||||
needs:
|
|
||||||
- build-electron-renderer
|
|
||||||
- build-native
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
spec:
|
|
||||||
- {
|
|
||||||
os: macos-latest,
|
|
||||||
platform: macos,
|
|
||||||
arch: x64,
|
|
||||||
target: x86_64-apple-darwin,
|
|
||||||
test: false,
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
os: macos-latest,
|
|
||||||
platform: macos,
|
|
||||||
arch: arm64,
|
|
||||||
target: aarch64-apple-darwin,
|
|
||||||
test: true,
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
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 @affine/nbstore @toeverything/infra
|
|
||||||
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@v4
|
|
||||||
with:
|
|
||||||
name: ${{ steps.filename.outputs.filename }}
|
|
||||||
path: ./packages/frontend/native
|
|
||||||
|
|
||||||
- name: Download web artifact
|
|
||||||
uses: ./.github/actions/download-web
|
|
||||||
with:
|
|
||||||
path: packages/frontend/apps/electron/resources/web-static
|
|
||||||
|
|
||||||
- name: Build Desktop Layers
|
|
||||||
run: yarn affine @affine/electron build
|
|
||||||
|
|
||||||
- name: Make bundle (macOS)
|
- name: Make bundle (macOS)
|
||||||
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
|
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
|
||||||
env:
|
env:
|
||||||
@@ -1299,6 +1355,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
|
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
||||||
|
path: ./test-results
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
test-done:
|
test-done:
|
||||||
needs:
|
needs:
|
||||||
- analyze
|
- analyze
|
||||||
@@ -1312,8 +1376,9 @@ jobs:
|
|||||||
- e2e-blocksuite-cross-browser-test
|
- e2e-blocksuite-cross-browser-test
|
||||||
- e2e-mobile-test
|
- e2e-mobile-test
|
||||||
- unit-test
|
- unit-test
|
||||||
- build-native
|
- build-native-linux
|
||||||
- build-windows-native
|
- build-native-macos
|
||||||
|
- build-native-windows
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
- native-unit-test
|
- native-unit-test
|
||||||
@@ -1323,10 +1388,11 @@ jobs:
|
|||||||
- server-test
|
- server-test
|
||||||
- server-e2e-test
|
- server-e2e-test
|
||||||
- rust-test
|
- rust-test
|
||||||
|
- rust-test-filter
|
||||||
|
- copilot-test-filter
|
||||||
- copilot-api-test
|
- copilot-api-test
|
||||||
- copilot-e2e-test
|
- copilot-e2e-test
|
||||||
- desktop-test
|
- desktop-test
|
||||||
- desktop-bundle-check
|
|
||||||
- cloud-e2e-test
|
- cloud-e2e-test
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
1
.github/workflows/pr-title-lint.yml
vendored
1
.github/workflows/pr-title-lint.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
|||||||
check-pull-request-title:
|
check-pull-request-title:
|
||||||
name: Check pull request title
|
name: Check pull request title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p builds
|
mkdir -p builds
|
||||||
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
|
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
|
||||||
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
|
mv packages/frontend/apps/electron/out/*/make/AppImage/${{ inputs.arch }}/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
|
||||||
mv packages/frontend/apps/electron/out/*/make/deb/${{ inputs.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
|
mv packages/frontend/apps/electron/out/*/make/deb/${{ inputs.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
|
||||||
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
|
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
|
||||||
|
|
||||||
|
|||||||
78
.github/workflows/release-desktop.yml
vendored
78
.github/workflows/release-desktop.yml
vendored
@@ -201,13 +201,44 @@ jobs:
|
|||||||
nmHoistingLimits: workspaces
|
nmHoistingLimits: workspaces
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.spec.arch }}
|
npm_config_arch: ${{ matrix.spec.arch }}
|
||||||
- name: Download and overwrite packaged artifacts
|
- name: Download packaged artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||||
|
path: packaged-unsigned
|
||||||
|
- name: unzip packaged artifacts
|
||||||
|
run: Expand-Archive -Path packaged-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out
|
||||||
|
- name: Download signed packaged file diff
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||||
path: .
|
path: signed-packaged-diff
|
||||||
- name: unzip file
|
- name: Apply signed packaged file diff
|
||||||
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$DiffRoot = 'signed-packaged-diff/files'
|
||||||
|
$TargetRoot = 'packages/frontend/apps/electron/out'
|
||||||
|
if (!(Test-Path -LiteralPath $DiffRoot)) {
|
||||||
|
throw "Signed diff directory not found: $DiffRoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path (Join-Path $DiffRoot '*') -Destination $TargetRoot -Recurse -Force
|
||||||
|
|
||||||
|
$ManifestPath = 'signed-packaged-diff/manifest.json'
|
||||||
|
if (Test-Path -LiteralPath $ManifestPath) {
|
||||||
|
$ManifestEntries = @(Get-Content -LiteralPath $ManifestPath | ConvertFrom-Json)
|
||||||
|
foreach ($Entry in $ManifestEntries) {
|
||||||
|
$TargetPath = Join-Path $TargetRoot $Entry.path
|
||||||
|
if (!(Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
|
||||||
|
throw "Applied signed file not found: $($Entry.path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$TargetHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
|
||||||
|
if ($TargetHash -ne $Entry.sha256) {
|
||||||
|
throw "Signed file hash mismatch: $($Entry.path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- name: Make squirrel.windows installer
|
- name: Make squirrel.windows installer
|
||||||
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||||
@@ -267,13 +298,44 @@ jobs:
|
|||||||
arch: arm64
|
arch: arm64
|
||||||
runs-on: ${{ matrix.spec.runner }}
|
runs-on: ${{ matrix.spec.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download and overwrite installer artifacts
|
- name: Download installer artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||||
|
path: installer-unsigned
|
||||||
|
- name: unzip installer artifacts
|
||||||
|
run: Expand-Archive -Path installer-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
|
||||||
|
- name: Download signed installer file diff
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||||
path: .
|
path: signed-installer-diff
|
||||||
- name: unzip file
|
- name: Apply signed installer file diff
|
||||||
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$DiffRoot = 'signed-installer-diff/files'
|
||||||
|
$TargetRoot = 'packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make'
|
||||||
|
if (!(Test-Path -LiteralPath $DiffRoot)) {
|
||||||
|
throw "Signed diff directory not found: $DiffRoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path (Join-Path $DiffRoot '*') -Destination $TargetRoot -Recurse -Force
|
||||||
|
|
||||||
|
$ManifestPath = 'signed-installer-diff/manifest.json'
|
||||||
|
if (Test-Path -LiteralPath $ManifestPath) {
|
||||||
|
$ManifestEntries = @(Get-Content -LiteralPath $ManifestPath | ConvertFrom-Json)
|
||||||
|
foreach ($Entry in $ManifestEntries) {
|
||||||
|
$TargetPath = Join-Path $TargetRoot $Entry.path
|
||||||
|
if (!(Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
|
||||||
|
throw "Applied signed file not found: $($Entry.path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$TargetHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
|
||||||
|
if ($TargetHash -ne $Entry.sha256) {
|
||||||
|
throw "Signed file hash mismatch: $($Entry.path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- name: Save artifacts
|
- name: Save artifacts
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
10
.github/workflows/release-mobile.yml
vendored
10
.github/workflows/release-mobile.yml
vendored
@@ -128,9 +128,9 @@ jobs:
|
|||||||
- name: Testflight
|
- name: Testflight
|
||||||
working-directory: packages/frontend/apps/ios/App
|
working-directory: packages/frontend/apps/ios/App
|
||||||
run: |
|
run: |
|
||||||
echo -n "${{ env.BUILD_PROVISION_PROFILE }}" | base64 --decode -o $PP_PATH
|
printf '%s' "$BUILD_PROVISION_PROFILE" | base64 --decode -o "$PP_PATH"
|
||||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
cp "$PP_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
fastlane beta
|
fastlane beta
|
||||||
env:
|
env:
|
||||||
BUILD_TARGET: distribution
|
BUILD_TARGET: distribution
|
||||||
@@ -160,7 +160,9 @@ jobs:
|
|||||||
- name: Load Google Service file
|
- name: Load Google Service file
|
||||||
env:
|
env:
|
||||||
DATA: ${{ secrets.FIREBASE_ANDROID_GOOGLE_SERVICE_JSON }}
|
DATA: ${{ secrets.FIREBASE_ANDROID_GOOGLE_SERVICE_JSON }}
|
||||||
run: echo $DATA | base64 -di > packages/frontend/apps/android/App/app/google-services.json
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
printf '%s' "$DATA" | base64 -di > packages/frontend/apps/android/App/app/google-services.json
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -148,7 +148,7 @@ jobs:
|
|||||||
name: Wait for approval
|
name: Wait for approval
|
||||||
with:
|
with:
|
||||||
secret: ${{ secrets.GITHUB_TOKEN }}
|
secret: ${{ secrets.GITHUB_TOKEN }}
|
||||||
approvers: darkskygit,pengx17,L-Sun,EYHN
|
approvers: darkskygit
|
||||||
minimum-approvals: 1
|
minimum-approvals: 1
|
||||||
fail-on-denial: true
|
fail-on-denial: true
|
||||||
issue-title: Please confirm to release docker image
|
issue-title: Please confirm to release docker image
|
||||||
|
|||||||
40
.github/workflows/windows-signer.yml
vendored
40
.github/workflows/windows-signer.yml
vendored
@@ -30,13 +30,43 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd ${{ env.ARCHIVE_DIR }}/out
|
cd ${{ env.ARCHIVE_DIR }}/out
|
||||||
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||||
- name: zip file
|
- name: collect signed file diff
|
||||||
shell: cmd
|
shell: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {0}
|
||||||
run: |
|
run: |
|
||||||
cd ${{ env.ARCHIVE_DIR }}
|
$OutDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'out'
|
||||||
7za a signed.zip .\out\*
|
$DiffDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'signed-diff'
|
||||||
|
$FilesDir = Join-Path $DiffDir 'files'
|
||||||
|
New-Item -ItemType Directory -Path $FilesDir -Force | Out-Null
|
||||||
|
|
||||||
|
$SignedFiles = [regex]::Matches('${{ inputs.files }}', '"([^"]+)"') | ForEach-Object { $_.Groups[1].Value }
|
||||||
|
if ($SignedFiles.Count -eq 0) {
|
||||||
|
throw 'No files to sign were provided.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$Manifest = @()
|
||||||
|
foreach ($RelativePath in $SignedFiles) {
|
||||||
|
$SourcePath = Join-Path $OutDir $RelativePath
|
||||||
|
if (!(Test-Path -LiteralPath $SourcePath -PathType Leaf)) {
|
||||||
|
throw "Signed file not found: $RelativePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$TargetPath = Join-Path $FilesDir $RelativePath
|
||||||
|
$TargetDir = Split-Path -Parent $TargetPath
|
||||||
|
if ($TargetDir) {
|
||||||
|
New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
|
||||||
|
$Manifest += [PSCustomObject]@{
|
||||||
|
path = $RelativePath
|
||||||
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Manifest | ConvertTo-Json -Depth 4 | Out-File -FilePath (Join-Path $DiffDir 'manifest.json') -Encoding utf8
|
||||||
|
Write-Host "Collected $($SignedFiles.Count) signed files."
|
||||||
- name: upload
|
- name: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed-${{ inputs.artifact-name }}
|
name: signed-${{ inputs.artifact-name }}
|
||||||
path: ${{ env.ARCHIVE_DIR }}/signed.zip
|
path: ${{ env.ARCHIVE_DIR }}/signed-diff
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"correctness": "error",
|
"correctness": "error",
|
||||||
"perf": "error"
|
"perf": "error"
|
||||||
},
|
},
|
||||||
|
"env": {
|
||||||
|
"builtin": true,
|
||||||
|
"es2026": true
|
||||||
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
".yarn",
|
".yarn",
|
||||||
@@ -44,6 +48,34 @@
|
|||||||
"**/test-blocks.json"
|
"**/test-blocks.json"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"no-empty-static-block": "error",
|
||||||
|
"no-misleading-character-class": "error",
|
||||||
|
"no-new-native-nonconstructor": "error",
|
||||||
|
"no-unused-private-class-members": "error",
|
||||||
|
"no-useless-backreference": "error",
|
||||||
|
"react/display-name": "error",
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/exhaustive-deps": "warn",
|
||||||
|
"@typescript-eslint/prefer-for-of": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||||
|
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"group": ["**/dist"],
|
||||||
|
"message": "Don't import from dist",
|
||||||
|
"allowTypeImports": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": ["**/src"],
|
||||||
|
"message": "Don't import from src",
|
||||||
|
"allowTypeImports": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"no-await-in-loop": "allow",
|
"no-await-in-loop": "allow",
|
||||||
"no-redeclare": "allow",
|
"no-redeclare": "allow",
|
||||||
"promise/no-callback-in-promise": "allow",
|
"promise/no-callback-in-promise": "allow",
|
||||||
@@ -70,6 +102,14 @@
|
|||||||
"no-func-assign": "error",
|
"no-func-assign": "error",
|
||||||
"no-global-assign": "error",
|
"no-global-assign": "error",
|
||||||
"no-unused-vars": "error",
|
"no-unused-vars": "error",
|
||||||
|
"no-unused-expressions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowShortCircuit": true,
|
||||||
|
"allowTernary": true,
|
||||||
|
"allowTaggedTemplates": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"no-ex-assign": "error",
|
"no-ex-assign": "error",
|
||||||
"no-loss-of-precision": "error",
|
"no-loss-of-precision": "error",
|
||||||
"no-fallthrough": "error",
|
"no-fallthrough": "error",
|
||||||
@@ -126,6 +166,7 @@
|
|||||||
"react/no-render-return-value": "error",
|
"react/no-render-return-value": "error",
|
||||||
"react/jsx-no-target-blank": "error",
|
"react/jsx-no-target-blank": "error",
|
||||||
"react/jsx-no-comment-textnodes": "error",
|
"react/jsx-no-comment-textnodes": "error",
|
||||||
|
"react/no-array-index-key": "off",
|
||||||
"typescript/consistent-type-imports": "error",
|
"typescript/consistent-type-imports": "error",
|
||||||
"typescript/no-non-null-assertion": "error",
|
"typescript/no-non-null-assertion": "error",
|
||||||
"typescript/triple-slash-reference": "error",
|
"typescript/triple-slash-reference": "error",
|
||||||
@@ -241,6 +282,42 @@
|
|||||||
"typescript/consistent-type-imports": "off",
|
"typescript/consistent-type-imports": "off",
|
||||||
"import/no-cycle": "off"
|
"import/no-cycle": "off"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"packages/**/*.{ts,tsx}",
|
||||||
|
"tools/**/*.{ts,tsx}",
|
||||||
|
"blocksuite/**/*.{ts,tsx}"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/exhaustive-deps": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"additionalHooks": "(useAsyncCallback|useCatchEventCallback|useDraggable|useDropTarget|useRefEffect)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/__tests__/**/*",
|
||||||
|
"**/*.stories.tsx",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/tests/**/*",
|
||||||
|
"scripts/**/*",
|
||||||
|
"**/benchmark/**/*",
|
||||||
|
"**/__debug__/**/*",
|
||||||
|
"**/e2e/**/*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-restricted-imports": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["**/*.{ts,js,mjs}"],
|
||||||
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2
.vscode/settings.template.json
vendored
2
.vscode/settings.template.json
vendored
@@ -17,7 +17,7 @@
|
|||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
|
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
|
||||||
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, eslint.*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json, oxlint.json, nyc.config.*",
|
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, eslint.*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json, .oxlintrc.json, oxlint.json, nyc.config.*",
|
||||||
"Cargo.toml": "Cargo.lock, rust-toolchain*, rustfmt.toml, .taplo.toml",
|
"Cargo.toml": "Cargo.lock, rust-toolchain*, rustfmt.toml, .taplo.toml",
|
||||||
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*",
|
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*",
|
||||||
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
|
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
|
||||||
|
|||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -111,10 +111,12 @@ dependencies = [
|
|||||||
"base64-simd",
|
"base64-simd",
|
||||||
"chrono",
|
"chrono",
|
||||||
"homedir",
|
"homedir",
|
||||||
|
"lru",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2572,6 +2574,15 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.16.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ resolver = "3"
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
loom = { version = "0.7", features = ["checkpoint"] }
|
loom = { version = "0.7", features = ["checkpoint"] }
|
||||||
|
lru = "0.16"
|
||||||
memory-indexer = "0.3.0"
|
memory-indexer = "0.3.0"
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
mp4parse = "0.17"
|
mp4parse = "0.17"
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -90,10 +90,10 @@ Thanks for checking us out, we appreciate your interest and sincerely hope that
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
| Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community |
|
| Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community |
|
||||||
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||||
| [Create a bug report](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=bug%2Cproduct-review&template=BUG-REPORT.yml&title=TITLE) | [Submit a feature request](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=feat%2Cproduct-review&template=FEATURE-REQUEST.yml&title=TITLE) | [Check GitHub Discussion](https://github.com/toeverything/AFFiNE/discussions) | [Visit the AFFiNE Community](https://community.affine.pro) |
|
| [Create a bug report](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=bug%2Cproduct-review&template=BUG-REPORT.yml&title=TITLE) | [Submit a feature request](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=feat%2Cproduct-review&template=FEATURE-REQUEST.yml&title=TITLE) | [Check GitHub Discussion](https://github.com/toeverything/AFFiNE/discussions) | [Visit the AFFiNE's Discord](https://affine.pro/redirect/discord) |
|
||||||
| Something isn't working as expected | An idea for a new feature, or improvements | Discuss and ask questions | A place to ask, learn and engage with others |
|
| Something isn't working as expected | An idea for a new feature, or improvements | Discuss and ask questions | A place to ask, learn and engage with others |
|
||||||
|
|
||||||
Calling all developers, testers, tech writers and more! Contributions of all types are more than welcome, you can read more in [docs/types-of-contributions.md](docs/types-of-contributions.md). If you are interested in contributing code, read our [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and feel free to check out our GitHub issues to get stuck in to show us what you’re made of.
|
Calling all developers, testers, tech writers and more! Contributions of all types are more than welcome, you can read more in [docs/types-of-contributions.md](docs/types-of-contributions.md). If you are interested in contributing code, read our [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and feel free to check out our GitHub issues to get stuck in to show us what you’re made of.
|
||||||
|
|
||||||
@@ -101,11 +101,9 @@ Calling all developers, testers, tech writers and more! Contributions of all typ
|
|||||||
|
|
||||||
For **bug reports**, **feature requests** and other **suggestions** you can also [create a new issue](https://github.com/toeverything/AFFiNE/issues/new/choose) and choose the most appropriate template for your feedback.
|
For **bug reports**, **feature requests** and other **suggestions** you can also [create a new issue](https://github.com/toeverything/AFFiNE/issues/new/choose) and choose the most appropriate template for your feedback.
|
||||||
|
|
||||||
For **translation** and **language support** you can visit our [i18n General Space](https://community.affine.pro/c/i18n-general).
|
For **translation** and **language support** you can visit our [Discord](https://affine.pro/redirect/discord).
|
||||||
|
|
||||||
Looking for **other ways to contribute** and wondering where to start? Check out the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador), we work closely with passionate community members and provide them with a wide range of support and resources.
|
If you have questions, you are welcome to contact us. One of the best places to get more info and learn more is in the [Discord](https://affine.pro/redirect/discord) where you can engage with other like-minded individuals.
|
||||||
|
|
||||||
If you have questions, you are welcome to contact us. One of the best places to get more info and learn more is in the [AFFiNE Community](https://community.affine.pro) where you can engage with other like-minded individuals.
|
|
||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
|
|
||||||
@@ -182,20 +180,16 @@ Begin with Docker to deploy your own feature-rich, unrestricted version of AFFiN
|
|||||||
|
|
||||||
[](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
|
[](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
|
||||||
|
|
||||||
## Hiring
|
|
||||||
|
|
||||||
Some amazing companies, including AFFiNE, are looking for developers! Are you interested in joining AFFiNE or its partners? Check out our [Discord channel](https://affine.pro/redirect/discord) for some of the latest jobs available.
|
|
||||||
|
|
||||||
## Feature Request
|
## Feature Request
|
||||||
|
|
||||||
For feature requests, please see [community.affine.pro](https://community.affine.pro/c/feature-requests/).
|
For feature requests, please see [discussions](https://github.com/toeverything/AFFiNE/discussions/categories/ideas).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Codespaces
|
### Codespaces
|
||||||
|
|
||||||
From the GitHub repo main page, click the green "Code" button and select "Create codespace on master". This will open a new Codespace with the (supposedly auto-forked
|
From the GitHub repo main page, click the green "Code" button and select "Create codespace on master". This will open a new Codespace with the (supposedly auto-forked
|
||||||
AFFiNE repo cloned, built, and ready to go.
|
AFFiNE repo cloned, built, and ready to go).
|
||||||
|
|
||||||
### Local
|
### Local
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1",
|
"version": "0.26.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||||
"msw": "^2.12.4",
|
"msw": "^2.12.4",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"file-type": "^21.0.0",
|
"file-type": "^21.0.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -41,5 +40,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -45,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -45,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,9 +216,13 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
|||||||
override renderBlock() {
|
override renderBlock() {
|
||||||
const icon = this.model.props.icon$.value;
|
const icon = this.model.props.icon$.value;
|
||||||
const backgroundColorName = this.model.props.backgroundColorName$.value;
|
const backgroundColorName = this.model.props.backgroundColorName$.value;
|
||||||
|
const normalizedBackgroundName =
|
||||||
|
backgroundColorName === 'default' || backgroundColorName === ''
|
||||||
|
? 'grey'
|
||||||
|
: backgroundColorName;
|
||||||
const backgroundColor = (
|
const backgroundColor = (
|
||||||
cssVarV2.block.callout.background as Record<string, string>
|
cssVarV2.block.callout.background as Record<string, string>
|
||||||
)[backgroundColorName ?? ''];
|
)[normalizedBackgroundName ?? 'grey'];
|
||||||
|
|
||||||
const iconContent = getIcon(icon);
|
const iconContent = getIcon(icon);
|
||||||
|
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ const backgroundColorAction = {
|
|||||||
${repeat(colors, color => {
|
${repeat(colors, color => {
|
||||||
const isDefault = color === 'default';
|
const isDefault = color === 'default';
|
||||||
const value = isDefault
|
const value = isDefault
|
||||||
? null
|
? cssVarV2.block.callout.background.grey
|
||||||
: `var(--affine-text-highlight-${color})`;
|
: `var(--affine-text-highlight-${color})`;
|
||||||
const displayName = `${color} Background`;
|
const displayName = `${color} Background`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<editor-menu-action
|
<editor-menu-action
|
||||||
data-testid="background-${color}"
|
data-testid="background-${color}"
|
||||||
@click=${() => updateBackground(color)}
|
@click=${() => updateBackground(isDefault ? 'grey' : color)}
|
||||||
>
|
>
|
||||||
<affine-text-duotone-icon
|
<affine-text-duotone-icon
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"shiki": "^3.19.0",
|
"shiki": "^3.19.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -48,5 +47,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ export const codeBlockStyles = css`
|
|||||||
|
|
||||||
${scrollbarStyle('.affine-code-block-container rich-text')}
|
${scrollbarStyle('.affine-code-block-container rich-text')}
|
||||||
|
|
||||||
|
/* In Chromium 121+, non-auto scrollbar-width/color override ::-webkit-scrollbar styles. */
|
||||||
|
@supports not selector(::-webkit-scrollbar) {
|
||||||
|
.affine-code-block-container rich-text {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: ${unsafeCSSVarV2('icon/secondary', '#b1b1b1')}
|
||||||
|
transparent;
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.affine-code-block-container .inline-editor {
|
.affine-code-block-container .inline-editor {
|
||||||
font-family: var(--affine-font-code-family);
|
font-family: var(--affine-font-code-family);
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -42,5 +41,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"date-fns": "^4.0.0",
|
"date-fns": "^4.0.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -48,5 +47,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -39,5 +38,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -43,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -49,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -49,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -44,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"file-type": "^21.0.0",
|
"file-type": "^21.0.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -44,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ import {
|
|||||||
|
|
||||||
@Peekable()
|
@Peekable()
|
||||||
export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockModel> {
|
export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockModel> {
|
||||||
|
private static readonly LOD_MIN_IMAGE_BYTES = 1024 * 1024;
|
||||||
|
private static readonly LOD_MIN_IMAGE_PIXELS = 1920 * 1080;
|
||||||
|
private static readonly LOD_MAX_ZOOM = 0.4;
|
||||||
|
private static readonly LOD_THUMBNAIL_MAX_EDGE = 256;
|
||||||
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
affine-edgeless-image {
|
affine-edgeless-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -63,6 +68,11 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
affine-edgeless-image .resizable-img {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
resourceController = new ResourceController(
|
resourceController = new ResourceController(
|
||||||
@@ -70,6 +80,12 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
'Image'
|
'Image'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _lodThumbnailUrl: string | null = null;
|
||||||
|
private _lodSourceUrl: string | null = null;
|
||||||
|
private _lodGeneratingSourceUrl: string | null = null;
|
||||||
|
private _lodGenerationToken = 0;
|
||||||
|
private _lastShouldUseLod = false;
|
||||||
|
|
||||||
get blobUrl() {
|
get blobUrl() {
|
||||||
return this.resourceController.blobUrl$.value;
|
return this.resourceController.blobUrl$.value;
|
||||||
}
|
}
|
||||||
@@ -96,6 +112,134 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _isLargeImage() {
|
||||||
|
const { width = 0, height = 0, size = 0 } = this.model.props;
|
||||||
|
const pixels = width * height;
|
||||||
|
return (
|
||||||
|
size >= ImageEdgelessBlockComponent.LOD_MIN_IMAGE_BYTES ||
|
||||||
|
pixels >= ImageEdgelessBlockComponent.LOD_MIN_IMAGE_PIXELS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _shouldUseLod(blobUrl: string | null, zoom = this.gfx.viewport.zoom) {
|
||||||
|
return (
|
||||||
|
Boolean(blobUrl) &&
|
||||||
|
this._isLargeImage() &&
|
||||||
|
zoom <= ImageEdgelessBlockComponent.LOD_MAX_ZOOM
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _revokeLodThumbnail() {
|
||||||
|
if (!this._lodThumbnailUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL.revokeObjectURL(this._lodThumbnailUrl);
|
||||||
|
this._lodThumbnailUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resetLodSource(blobUrl: string | null) {
|
||||||
|
if (this._lodSourceUrl === blobUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lodGenerationToken += 1;
|
||||||
|
this._lodGeneratingSourceUrl = null;
|
||||||
|
this._lodSourceUrl = blobUrl;
|
||||||
|
this._revokeLodThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createImageElement(src: string) {
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.decoding = 'async';
|
||||||
|
image.onload = () => resolve(image);
|
||||||
|
image.onerror = () => reject(new Error('Failed to load image'));
|
||||||
|
image.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createThumbnailBlob(image: HTMLImageElement) {
|
||||||
|
const maxEdge = ImageEdgelessBlockComponent.LOD_THUMBNAIL_MAX_EDGE;
|
||||||
|
const longestEdge = Math.max(image.naturalWidth, image.naturalHeight);
|
||||||
|
const scale = longestEdge > maxEdge ? maxEdge / longestEdge : 1;
|
||||||
|
const targetWidth = Math.max(1, Math.round(image.naturalWidth * scale));
|
||||||
|
const targetHeight = Math.max(1, Math.round(image.naturalHeight * scale));
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = targetWidth;
|
||||||
|
canvas.height = targetHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return Promise.resolve<Blob | null>(null);
|
||||||
|
}
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.imageSmoothingQuality = 'low';
|
||||||
|
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
||||||
|
|
||||||
|
return new Promise<Blob | null>(resolve => {
|
||||||
|
canvas.toBlob(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ensureLodThumbnail(blobUrl: string) {
|
||||||
|
if (
|
||||||
|
this._lodThumbnailUrl ||
|
||||||
|
this._lodGeneratingSourceUrl === blobUrl ||
|
||||||
|
!this._shouldUseLod(blobUrl)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = ++this._lodGenerationToken;
|
||||||
|
this._lodGeneratingSourceUrl = blobUrl;
|
||||||
|
|
||||||
|
void this._createImageElement(blobUrl)
|
||||||
|
.then(image => this._createThumbnailBlob(image))
|
||||||
|
.then(blob => {
|
||||||
|
if (!blob || token !== this._lodGenerationToken || !this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailUrl = URL.createObjectURL(blob);
|
||||||
|
if (token !== this._lodGenerationToken || !this.isConnected) {
|
||||||
|
URL.revokeObjectURL(thumbnailUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._revokeLodThumbnail();
|
||||||
|
this._lodThumbnailUrl = thumbnailUrl;
|
||||||
|
|
||||||
|
if (this._shouldUseLod(this.blobUrl)) {
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (token !== this._lodGenerationToken || !this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (token === this._lodGenerationToken) {
|
||||||
|
this._lodGeneratingSourceUrl = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateLodFromViewport(zoom: number) {
|
||||||
|
const shouldUseLod = this._shouldUseLod(this.blobUrl, zoom);
|
||||||
|
if (shouldUseLod === this._lastShouldUseLod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastShouldUseLod = shouldUseLod;
|
||||||
|
if (shouldUseLod && this.blobUrl) {
|
||||||
|
this._ensureLodThumbnail(this.blobUrl);
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -108,14 +252,32 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
|
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.model.props.sourceId$.subscribe(() => {
|
this.model.props.sourceId$.subscribe(() => {
|
||||||
|
this._resetLodSource(null);
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.disposables.add(
|
||||||
|
this.gfx.viewport.viewportUpdated.subscribe(({ zoom }) => {
|
||||||
|
this._updateLodFromViewport(zoom);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._lastShouldUseLod = this._shouldUseLod(this.blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
this._lodGenerationToken += 1;
|
||||||
|
this._lodGeneratingSourceUrl = null;
|
||||||
|
this._lodSourceUrl = null;
|
||||||
|
this._revokeLodThumbnail();
|
||||||
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderGfxBlock() {
|
override renderGfxBlock() {
|
||||||
const blobUrl = this.blobUrl;
|
const blobUrl = this.blobUrl;
|
||||||
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
|
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
|
||||||
|
this._resetLodSource(blobUrl);
|
||||||
|
|
||||||
const containerStyleMap = styleMap({
|
const containerStyleMap = styleMap({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -138,6 +300,13 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { loading, icon, description, error, needUpload } = resovledState;
|
const { loading, icon, description, error, needUpload } = resovledState;
|
||||||
|
const shouldUseLod = this._shouldUseLod(blobUrl);
|
||||||
|
if (shouldUseLod && blobUrl) {
|
||||||
|
this._ensureLodThumbnail(blobUrl);
|
||||||
|
}
|
||||||
|
this._lastShouldUseLod = shouldUseLod;
|
||||||
|
const imageUrl =
|
||||||
|
shouldUseLod && this._lodThumbnailUrl ? this._lodThumbnailUrl : blobUrl;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="affine-image-container" style=${containerStyleMap}>
|
<div class="affine-image-container" style=${containerStyleMap}>
|
||||||
@@ -149,7 +318,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
class="drag-target"
|
class="drag-target"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${blobUrl}
|
src=${imageUrl ?? ''}
|
||||||
alt=${caption}
|
alt=${caption}
|
||||||
@error=${this._handleError}
|
@error=${this._handleError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"katex": "^0.16.27",
|
"katex": "^0.16.27",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -46,5 +45,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -46,5 +45,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"@vanilla-extract/css": "^1.17.0",
|
"@vanilla-extract/css": "^1.17.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -49,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -42,5 +41,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -67,5 +66,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ import {
|
|||||||
ReleaseFromGroupIcon,
|
ReleaseFromGroupIcon,
|
||||||
UnlockIcon,
|
UnlockIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
batchAddChildren,
|
||||||
|
batchRemoveChildren,
|
||||||
|
type GfxModel,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import { renderAlignmentMenu } from './alignment';
|
import { renderAlignmentMenu } from './alignment';
|
||||||
@@ -61,14 +65,13 @@ export const builtinMiscToolbarConfig = {
|
|||||||
|
|
||||||
const group = firstModel.group;
|
const group = firstModel.group;
|
||||||
|
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
batchRemoveChildren(group, [firstModel]);
|
||||||
group.removeChild(firstModel);
|
|
||||||
|
|
||||||
firstModel.index = ctx.gfx.layer.generateIndex();
|
firstModel.index = ctx.gfx.layer.generateIndex();
|
||||||
|
|
||||||
const parent = group.group;
|
const parent = group.group;
|
||||||
if (parent && parent instanceof GroupElementModel) {
|
if (parent && parent instanceof GroupElementModel) {
|
||||||
parent.addChild(firstModel);
|
batchAddChildren(parent, [firstModel]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -255,9 +258,12 @@ export const builtinMiscToolbarConfig = {
|
|||||||
|
|
||||||
// release other elements from their groups and group with top element
|
// release other elements from their groups and group with top element
|
||||||
otherElements.forEach(element => {
|
otherElements.forEach(element => {
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
if (element.group) {
|
||||||
element.group?.removeChild(element);
|
batchRemoveChildren(element.group, [element]);
|
||||||
topElement.group?.addChild(element);
|
}
|
||||||
|
if (topElement.group) {
|
||||||
|
batchAddChildren(topElement.group, [element]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (otherElements.length === 0) {
|
if (otherElements.length === 0) {
|
||||||
|
|||||||
@@ -45,5 +45,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,5 +46,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,146 @@ export const SurfaceBlockSchemaExtension =
|
|||||||
|
|
||||||
export class SurfaceBlockModel extends BaseSurfaceModel {
|
export class SurfaceBlockModel extends BaseSurfaceModel {
|
||||||
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
||||||
|
private readonly _connectorIdsByEndpoint = new Map<string, Set<string>>();
|
||||||
|
private readonly _connectorIndexDisposables = new DisposableGroup();
|
||||||
|
private readonly _connectorEndpoints = new Map<
|
||||||
|
string,
|
||||||
|
{ sourceId: string | null; targetId: string | null }
|
||||||
|
>();
|
||||||
|
|
||||||
|
private _addConnectorEndpoint(endpointId: string, connectorId: string) {
|
||||||
|
const connectorIds = this._connectorIdsByEndpoint.get(endpointId);
|
||||||
|
|
||||||
|
if (connectorIds) {
|
||||||
|
connectorIds.add(connectorId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._connectorIdsByEndpoint.set(endpointId, new Set([connectorId]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isConnectorModel(model: unknown): model is ConnectorElementModel {
|
||||||
|
return (
|
||||||
|
!!model &&
|
||||||
|
typeof model === 'object' &&
|
||||||
|
'type' in model &&
|
||||||
|
(model as { type?: string }).type === 'connector'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeConnectorEndpoint(endpointId: string, connectorId: string) {
|
||||||
|
const connectorIds = this._connectorIdsByEndpoint.get(endpointId);
|
||||||
|
|
||||||
|
if (!connectorIds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorIds.delete(connectorId);
|
||||||
|
|
||||||
|
if (connectorIds.size === 0) {
|
||||||
|
this._connectorIdsByEndpoint.delete(endpointId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeConnectorFromIndex(connectorId: string) {
|
||||||
|
const endpoints = this._connectorEndpoints.get(connectorId);
|
||||||
|
|
||||||
|
if (!endpoints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoints.sourceId) {
|
||||||
|
this._removeConnectorEndpoint(endpoints.sourceId, connectorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoints.targetId) {
|
||||||
|
this._removeConnectorEndpoint(endpoints.targetId, connectorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._connectorEndpoints.delete(connectorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rebuildConnectorIndex() {
|
||||||
|
this._connectorIdsByEndpoint.clear();
|
||||||
|
this._connectorEndpoints.clear();
|
||||||
|
|
||||||
|
this.getElementsByType('connector').forEach(connector => {
|
||||||
|
this._setConnectorEndpoints(connector as ConnectorElementModel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setConnectorEndpoints(connector: ConnectorElementModel) {
|
||||||
|
const sourceId = connector.source?.id ?? null;
|
||||||
|
const targetId = connector.target?.id ?? null;
|
||||||
|
const previousEndpoints = this._connectorEndpoints.get(connector.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousEndpoints?.sourceId === sourceId &&
|
||||||
|
previousEndpoints?.targetId === targetId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousEndpoints?.sourceId) {
|
||||||
|
this._removeConnectorEndpoint(previousEndpoints.sourceId, connector.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousEndpoints?.targetId) {
|
||||||
|
this._removeConnectorEndpoint(previousEndpoints.targetId, connector.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceId) {
|
||||||
|
this._addConnectorEndpoint(sourceId, connector.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
this._addConnectorEndpoint(targetId, connector.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._connectorEndpoints.set(connector.id, {
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
override _init() {
|
override _init() {
|
||||||
this._extendElement(elementsCtorMap);
|
this._extendElement(elementsCtorMap);
|
||||||
super._init();
|
super._init();
|
||||||
|
this._rebuildConnectorIndex();
|
||||||
|
this._connectorIndexDisposables.add(
|
||||||
|
this.elementAdded.subscribe(({ id }) => {
|
||||||
|
const model = this.getElementById(id);
|
||||||
|
|
||||||
|
if (this._isConnectorModel(model)) {
|
||||||
|
this._setConnectorEndpoints(model);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this._connectorIndexDisposables.add(
|
||||||
|
this.elementUpdated.subscribe(({ id, props }) => {
|
||||||
|
if (!props['source'] && !props['target']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = this.getElementById(id);
|
||||||
|
|
||||||
|
if (this._isConnectorModel(model)) {
|
||||||
|
this._setConnectorEndpoints(model);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this._connectorIndexDisposables.add(
|
||||||
|
this.elementRemoved.subscribe(({ id, type }) => {
|
||||||
|
if (type === 'connector') {
|
||||||
|
this._removeConnectorFromIndex(id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.deleted.subscribe(() => {
|
||||||
|
this._connectorIndexDisposables.dispose();
|
||||||
|
this._connectorIdsByEndpoint.clear();
|
||||||
|
this._connectorEndpoints.clear();
|
||||||
|
});
|
||||||
this.store.provider
|
this.store.provider
|
||||||
.getAll(surfaceMiddlewareIdentifier)
|
.getAll(surfaceMiddlewareIdentifier)
|
||||||
.forEach(({ middleware }) => {
|
.forEach(({ middleware }) => {
|
||||||
@@ -52,13 +188,31 @@ export class SurfaceBlockModel extends BaseSurfaceModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getConnectors(id: string) {
|
getConnectors(id: string) {
|
||||||
const connectors = this.getElementsByType(
|
const connectorIds = this._connectorIdsByEndpoint.get(id);
|
||||||
'connector'
|
|
||||||
) as unknown[] as ConnectorElementModel[];
|
|
||||||
|
|
||||||
return connectors.filter(
|
if (!connectorIds?.size) {
|
||||||
connector => connector.source?.id === id || connector.target?.id === id
|
return [];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const staleConnectorIds: string[] = [];
|
||||||
|
const connectors: ConnectorElementModel[] = [];
|
||||||
|
|
||||||
|
connectorIds.forEach(connectorId => {
|
||||||
|
const model = this.getElementById(connectorId);
|
||||||
|
|
||||||
|
if (!this._isConnectorModel(model)) {
|
||||||
|
staleConnectorIds.push(connectorId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectors.push(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
staleConnectorIds.forEach(connectorId => {
|
||||||
|
this._removeConnectorFromIndex(connectorId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return connectors;
|
||||||
}
|
}
|
||||||
|
|
||||||
override getElementsByType<K extends keyof SurfaceElementModelMap>(
|
override getElementsByType<K extends keyof SurfaceElementModelMap>(
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,5 +82,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
import type { GroupBy } from '../core/common/types.js';
|
import type { GroupBy } from '../core/common/types.js';
|
||||||
import type { DataSource } from '../core/data-source/base.js';
|
import type { DataSource } from '../core/data-source/base.js';
|
||||||
import { DetailSelection } from '../core/detail/selection.js';
|
import { DetailSelection } from '../core/detail/selection.js';
|
||||||
|
import type { FilterGroup } from '../core/filter/types.js';
|
||||||
import { groupByMatchers } from '../core/group-by/define.js';
|
import { groupByMatchers } from '../core/group-by/define.js';
|
||||||
import { t } from '../core/logical/type-presets.js';
|
import { t } from '../core/logical/type-presets.js';
|
||||||
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
||||||
@@ -17,7 +18,10 @@ import {
|
|||||||
pickKanbanGroupColumn,
|
pickKanbanGroupColumn,
|
||||||
resolveKanbanGroupBy,
|
resolveKanbanGroupBy,
|
||||||
} from '../view-presets/kanban/group-by-utils.js';
|
} from '../view-presets/kanban/group-by-utils.js';
|
||||||
import { materializeKanbanColumns } from '../view-presets/kanban/kanban-view-manager.js';
|
import {
|
||||||
|
KanbanSingleView,
|
||||||
|
materializeKanbanColumns,
|
||||||
|
} from '../view-presets/kanban/kanban-view-manager.js';
|
||||||
import type { KanbanCard } from '../view-presets/kanban/pc/card.js';
|
import type { KanbanCard } from '../view-presets/kanban/pc/card.js';
|
||||||
import { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js';
|
import { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js';
|
||||||
import type { KanbanGroup } from '../view-presets/kanban/pc/group.js';
|
import type { KanbanGroup } from '../view-presets/kanban/pc/group.js';
|
||||||
@@ -270,6 +274,73 @@ describe('kanban', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('filtering', () => {
|
||||||
|
const sharedFilter: FilterGroup = {
|
||||||
|
type: 'group',
|
||||||
|
op: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'filter',
|
||||||
|
left: {
|
||||||
|
type: 'ref',
|
||||||
|
name: 'status',
|
||||||
|
},
|
||||||
|
function: 'is',
|
||||||
|
args: [{ type: 'literal', value: 'Done' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharedTitleProperty = {
|
||||||
|
id: 'title',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'Task 1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('evaluates filters with hidden columns', () => {
|
||||||
|
const statusProperty = {
|
||||||
|
id: 'status',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'Done',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
filter$: { value: sharedFilter },
|
||||||
|
// Simulate status being hidden in current view.
|
||||||
|
properties$: { value: [sharedTitleProperty] },
|
||||||
|
propertiesRaw$: { value: [sharedTitleProperty, statusProperty] },
|
||||||
|
} as unknown as KanbanSingleView;
|
||||||
|
|
||||||
|
expect(KanbanSingleView.prototype.isShow.call(view, 'row-1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when hidden filtered column does not match', () => {
|
||||||
|
const statusProperty = {
|
||||||
|
id: 'status',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'In Progress',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
filter$: { value: sharedFilter },
|
||||||
|
// Simulate status being hidden in current view.
|
||||||
|
properties$: { value: [sharedTitleProperty] },
|
||||||
|
propertiesRaw$: { value: [sharedTitleProperty, statusProperty] },
|
||||||
|
} as unknown as KanbanSingleView;
|
||||||
|
|
||||||
|
expect(KanbanSingleView.prototype.isShow.call(view, 'row-1')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('drag indicator', () => {
|
describe('drag indicator', () => {
|
||||||
it('shows drop preview when insert position exists', () => {
|
it('shows drop preview when insert position exists', () => {
|
||||||
const controller = createDragController();
|
const controller = createDragController();
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import type { FilterGroup } from '../core/filter/types.js';
|
||||||
import { numberFormats } from '../property-presets/number/utils/formats.js';
|
import { numberFormats } from '../property-presets/number/utils/formats.js';
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
NumberFormatSchema,
|
NumberFormatSchema,
|
||||||
parseNumber,
|
parseNumber,
|
||||||
} from '../property-presets/number/utils/formatter.js';
|
} from '../property-presets/number/utils/formatter.js';
|
||||||
|
import { DEFAULT_COLUMN_WIDTH } from '../view-presets/table/consts.js';
|
||||||
import { mobileEffects } from '../view-presets/table/mobile/effect.js';
|
import { mobileEffects } from '../view-presets/table/mobile/effect.js';
|
||||||
import type { MobileTableGroup } from '../view-presets/table/mobile/group.js';
|
import type { MobileTableGroup } from '../view-presets/table/mobile/group.js';
|
||||||
import { pcEffects } from '../view-presets/table/pc/effect.js';
|
import { pcEffects } from '../view-presets/table/pc/effect.js';
|
||||||
import type { TableGroup } from '../view-presets/table/pc/group.js';
|
import type { TableGroup } from '../view-presets/table/pc/group.js';
|
||||||
|
import {
|
||||||
|
materializeTableColumns,
|
||||||
|
TableSingleView,
|
||||||
|
} from '../view-presets/table/table-view-manager.js';
|
||||||
|
|
||||||
/** @vitest-environment happy-dom */
|
/** @vitest-environment happy-dom */
|
||||||
|
|
||||||
@@ -41,6 +47,146 @@ describe('TableGroup', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('table column materialization', () => {
|
||||||
|
test('appends missing properties while preserving existing order and state', () => {
|
||||||
|
const columns = [
|
||||||
|
{ id: 'status', width: 240, hide: true },
|
||||||
|
{ id: 'title', width: 320 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const next = materializeTableColumns(columns, ['title', 'status', 'date']);
|
||||||
|
|
||||||
|
expect(next).toEqual([
|
||||||
|
{ id: 'status', width: 240, hide: true },
|
||||||
|
{ id: 'title', width: 320 },
|
||||||
|
{ id: 'date', width: DEFAULT_COLUMN_WIDTH },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drops stale columns that no longer exist in data source', () => {
|
||||||
|
const columns = [
|
||||||
|
{ id: 'title', width: 320 },
|
||||||
|
{ id: 'removed', width: 200, hide: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const next = materializeTableColumns(columns, ['title']);
|
||||||
|
|
||||||
|
expect(next).toEqual([{ id: 'title', width: 320 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns original reference when columns are already materialized', () => {
|
||||||
|
const columns = [
|
||||||
|
{ id: 'title', width: 320 },
|
||||||
|
{ id: 'status', width: 240, hide: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const next = materializeTableColumns(columns, ['title', 'status']);
|
||||||
|
|
||||||
|
expect(next).toBe(columns);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports type-aware default width when materializing missing columns', () => {
|
||||||
|
const next = materializeTableColumns([], ['title', 'status'], id =>
|
||||||
|
id === 'title' ? 260 : DEFAULT_COLUMN_WIDTH
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(next).toEqual([
|
||||||
|
{ id: 'title', width: 260 },
|
||||||
|
{ id: 'status', width: DEFAULT_COLUMN_WIDTH },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('table filtering', () => {
|
||||||
|
test('evaluates filters with hidden columns', () => {
|
||||||
|
const filter: FilterGroup = {
|
||||||
|
type: 'group',
|
||||||
|
op: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'filter',
|
||||||
|
left: {
|
||||||
|
type: 'ref',
|
||||||
|
name: 'status',
|
||||||
|
},
|
||||||
|
function: 'is',
|
||||||
|
args: [{ type: 'literal', value: 'Done' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleProperty = {
|
||||||
|
id: 'title',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'Task 1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const statusProperty = {
|
||||||
|
id: 'status',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'Done',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
filter$: { value: filter },
|
||||||
|
// Simulate status being hidden in current view.
|
||||||
|
properties$: { value: [titleProperty] },
|
||||||
|
propertiesRaw$: { value: [titleProperty, statusProperty] },
|
||||||
|
} as unknown as TableSingleView;
|
||||||
|
|
||||||
|
expect(TableSingleView.prototype.isShow.call(view, 'row-1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when hidden filtered column does not match', () => {
|
||||||
|
const filter: FilterGroup = {
|
||||||
|
type: 'group',
|
||||||
|
op: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'filter',
|
||||||
|
left: {
|
||||||
|
type: 'ref',
|
||||||
|
name: 'status',
|
||||||
|
},
|
||||||
|
function: 'is',
|
||||||
|
args: [{ type: 'literal', value: 'Done' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleProperty = {
|
||||||
|
id: 'title',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'Task 1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const statusProperty = {
|
||||||
|
id: 'status',
|
||||||
|
cellGetOrCreate: () => ({
|
||||||
|
jsonValue$: {
|
||||||
|
value: 'In Progress',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
filter$: { value: filter },
|
||||||
|
// Simulate status being hidden in current view.
|
||||||
|
properties$: { value: [titleProperty] },
|
||||||
|
propertiesRaw$: { value: [titleProperty, statusProperty] },
|
||||||
|
} as unknown as TableSingleView;
|
||||||
|
|
||||||
|
expect(TableSingleView.prototype.isShow.call(view, 'row-1')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('number formatter', () => {
|
describe('number formatter', () => {
|
||||||
test('number format menu should expose all schema formats', () => {
|
test('number format menu should expose all schema formats', () => {
|
||||||
const menuFormats = numberFormats.map(format => format.type);
|
const menuFormats = numberFormats.map(format => format.type);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const autoScrollOnBoundary = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelBoxListen = effect(() => {
|
const cancelBoxListen = effect(() => {
|
||||||
box.value;
|
void box.value;
|
||||||
startUpdate();
|
startUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ export class KanbanSingleView extends SingleViewBase<KanbanViewData> {
|
|||||||
isShow(rowId: string): boolean {
|
isShow(rowId: string): boolean {
|
||||||
if (this.filter$.value?.conditions.length) {
|
if (this.filter$.value?.conditions.length) {
|
||||||
const rowMap = Object.fromEntries(
|
const rowMap = Object.fromEntries(
|
||||||
this.properties$.value.map(column => [
|
this.propertiesRaw$.value.map(column => [
|
||||||
column.id,
|
column.id,
|
||||||
column.cellGetOrCreate(rowId).jsonValue$.value,
|
column.cellGetOrCreate(rowId).jsonValue$.value,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ export class DatabaseCellContainer extends SignalWatcher(
|
|||||||
const selectionView = this.selectionView;
|
const selectionView = this.selectionView;
|
||||||
if (selectionView) {
|
if (selectionView) {
|
||||||
const selection = selectionView.selection;
|
const selection = selectionView.selection;
|
||||||
if (selection && this.isSelected(selection) && editing) {
|
const shouldEnterEditMode =
|
||||||
|
editing && this.cell?.beforeEnterEditMode() !== false;
|
||||||
|
if (selection && this.isSelected(selection) && shouldEnterEditMode) {
|
||||||
selectionView.selection = TableViewAreaSelection.create({
|
selectionView.selection = TableViewAreaSelection.create({
|
||||||
groupKey: this.groupKey,
|
groupKey: this.groupKey,
|
||||||
focus: {
|
focus: {
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ import {
|
|||||||
DataViewUIBase,
|
DataViewUIBase,
|
||||||
DataViewUILogicBase,
|
DataViewUILogicBase,
|
||||||
} from '../../../core/view/data-view-base.js';
|
} from '../../../core/view/data-view-base.js';
|
||||||
|
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||||
import {
|
import {
|
||||||
type TableSingleView,
|
|
||||||
TableViewRowSelection,
|
TableViewRowSelection,
|
||||||
type TableViewSelectionWithType,
|
type TableViewSelectionWithType,
|
||||||
} from '../../index.js';
|
} from '../selection.js';
|
||||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
import type { TableSingleView } from '../table-view-manager.js';
|
||||||
import { TableClipboardController } from './controller/clipboard.js';
|
import { TableClipboardController } from './controller/clipboard.js';
|
||||||
import { TableDragController } from './controller/drag.js';
|
import { TableDragController } from './controller/drag.js';
|
||||||
import { TableHotkeysController } from './controller/hotkeys.js';
|
import { TableHotkeysController } from './controller/hotkeys.js';
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export class TableViewCellContainer extends SignalWatcher(
|
|||||||
const selectionView = this.selectionController;
|
const selectionView = this.selectionController;
|
||||||
if (selectionView) {
|
if (selectionView) {
|
||||||
const selection = selectionView.selection;
|
const selection = selectionView.selection;
|
||||||
if (selection && this.isSelected(selection) && editing) {
|
const shouldEnterEditMode =
|
||||||
|
editing && this.cell?.beforeEnterEditMode() !== false;
|
||||||
|
if (selection && this.isSelected(selection) && shouldEnterEditMode) {
|
||||||
selectionView.selection = TableViewAreaSelection.create({
|
selectionView.selection = TableViewAreaSelection.create({
|
||||||
groupKey: this.groupKey,
|
groupKey: this.groupKey,
|
||||||
focus: {
|
focus: {
|
||||||
|
|||||||
@@ -26,6 +26,52 @@ import type { ViewManager } from '../../core/view-manager/view-manager.js';
|
|||||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_COLUMN_WIDTH } from './consts.js';
|
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_COLUMN_WIDTH } from './consts.js';
|
||||||
import type { TableViewData } from './define.js';
|
import type { TableViewData } from './define.js';
|
||||||
|
|
||||||
|
export const materializeColumnsByPropertyIds = (
|
||||||
|
columns: TableColumnData[],
|
||||||
|
propertyIds: string[],
|
||||||
|
getDefaultWidth: (id: string) => number = () => DEFAULT_COLUMN_WIDTH
|
||||||
|
) => {
|
||||||
|
const needShow = new Set(propertyIds);
|
||||||
|
const orderedColumns: TableColumnData[] = [];
|
||||||
|
|
||||||
|
for (const column of columns) {
|
||||||
|
if (needShow.has(column.id)) {
|
||||||
|
orderedColumns.push(column);
|
||||||
|
needShow.delete(column.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of needShow) {
|
||||||
|
orderedColumns.push({ id, width: getDefaultWidth(id), hide: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const materializeTableColumns = (
|
||||||
|
columns: TableColumnData[],
|
||||||
|
propertyIds: string[],
|
||||||
|
getDefaultWidth?: (id: string) => number
|
||||||
|
) => {
|
||||||
|
const nextColumns = materializeColumnsByPropertyIds(
|
||||||
|
columns,
|
||||||
|
propertyIds,
|
||||||
|
getDefaultWidth
|
||||||
|
);
|
||||||
|
const unchanged =
|
||||||
|
columns.length === nextColumns.length &&
|
||||||
|
columns.every((column, index) => {
|
||||||
|
const nextColumn = nextColumns[index];
|
||||||
|
return (
|
||||||
|
nextColumn != null &&
|
||||||
|
column.id === nextColumn.id &&
|
||||||
|
column.hide === nextColumn.hide
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unchanged ? columns : nextColumns;
|
||||||
|
};
|
||||||
|
|
||||||
export class TableSingleView extends SingleViewBase<TableViewData> {
|
export class TableSingleView extends SingleViewBase<TableViewData> {
|
||||||
propertiesRaw$ = computed(() => {
|
propertiesRaw$ = computed(() => {
|
||||||
const needShow = new Set(this.dataSource.properties$.value);
|
const needShow = new Set(this.dataSource.properties$.value);
|
||||||
@@ -220,14 +266,10 @@ export class TableSingleView extends SingleViewBase<TableViewData> {
|
|||||||
return this.data$.value?.mode ?? 'table';
|
return this.data$.value?.mode ?? 'table';
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(viewManager: ViewManager, viewId: string) {
|
|
||||||
super(viewManager, viewId);
|
|
||||||
}
|
|
||||||
|
|
||||||
isShow(rowId: string): boolean {
|
isShow(rowId: string): boolean {
|
||||||
if (this.filter$.value?.conditions.length) {
|
if (this.filter$.value?.conditions.length) {
|
||||||
const rowMap = Object.fromEntries(
|
const rowMap = Object.fromEntries(
|
||||||
this.properties$.value.map(column => [
|
this.propertiesRaw$.value.map(column => [
|
||||||
column.id,
|
column.id,
|
||||||
column.cellGetOrCreate(rowId).jsonValue$.value,
|
column.cellGetOrCreate(rowId).jsonValue$.value,
|
||||||
])
|
])
|
||||||
@@ -290,6 +332,33 @@ export class TableSingleView extends SingleViewBase<TableViewData> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private materializeColumns() {
|
||||||
|
const data = this.data$.value;
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextColumns = materializeTableColumns(
|
||||||
|
data.columns,
|
||||||
|
this.dataSource.properties$.value,
|
||||||
|
id => this.propertyGetOrCreate(id).width$.value
|
||||||
|
);
|
||||||
|
if (nextColumns === data.columns) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataUpdate(() => ({ columns: nextColumns }));
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(viewManager: ViewManager, viewId: string) {
|
||||||
|
super(viewManager, viewId);
|
||||||
|
// Materialize view columns on view activation so newly added properties
|
||||||
|
// can participate in hide/order operations in table.
|
||||||
|
queueMicrotask(() => {
|
||||||
|
this.materializeColumns();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TableColumnData = TableViewData['columns'][number];
|
type TableColumnData = TableViewData['columns'][number];
|
||||||
|
|||||||
@@ -26,5 +26,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,9 @@ export class BaseExtensionProvider<
|
|||||||
* @param context - The context object containing scope and registration function
|
* @param context - The context object containing scope and registration function
|
||||||
* @param option - Optional configuration options for the provider
|
* @param option - Optional configuration options for the provider
|
||||||
*/
|
*/
|
||||||
setup(context: Context<Scope>, option?: Options) {
|
setup(_context: Context<Scope>, option?: Options) {
|
||||||
if (option) {
|
if (option) {
|
||||||
this.schema.parse(option);
|
this.schema.parse(option);
|
||||||
}
|
}
|
||||||
context;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,5 +35,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -40,5 +39,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -42,5 +41,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@vanilla-extract/css": "^1.17.0",
|
"@vanilla-extract/css": "^1.17.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -41,5 +40,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -43,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -44,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -884,7 +884,7 @@ export class ConnectionOverlay extends Overlay {
|
|||||||
private _setupThemeListener(): void {
|
private _setupThemeListener(): void {
|
||||||
const themeService = this.gfx.std.get(ThemeProvider);
|
const themeService = this.gfx.std.get(ThemeProvider);
|
||||||
this._themeDisposer = effect(() => {
|
this._themeDisposer = effect(() => {
|
||||||
themeService.theme$;
|
void themeService.theme$.value;
|
||||||
this._emphasisColor = this._getEmphasisColor();
|
this._emphasisColor = this._getEmphasisColor();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export const connectorWatcher: SurfaceMiddleware = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
pendingFlag = false;
|
||||||
|
pendingList.clear();
|
||||||
disposables.forEach(d => d.unsubscribe());
|
disposables.forEach(d => d.unsubscribe());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,13 +26,16 @@
|
|||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"fractional-indexing": "^3.2.0",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./view": "./src/view.ts",
|
"./view": "./src/view.ts",
|
||||||
@@ -44,5 +47,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
152
blocksuite/affine/gfx/group/src/__tests__/group-api.unit.spec.ts
Normal file
152
blocksuite/affine/gfx/group/src/__tests__/group-api.unit.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('fractional-indexing', () => ({
|
||||||
|
generateKeyBetween: vi.fn(),
|
||||||
|
generateNKeysBetween: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
|
||||||
|
|
||||||
|
import { ungroupCommand } from '../command/group-api.js';
|
||||||
|
|
||||||
|
type TestElement = {
|
||||||
|
id: string;
|
||||||
|
index: string;
|
||||||
|
group: TestElement | null;
|
||||||
|
childElements: TestElement[];
|
||||||
|
removeChildren?: (elements: TestElement[]) => void;
|
||||||
|
addChildren?: (elements: TestElement[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockedGenerateNKeysBetween = vi.mocked(generateNKeysBetween);
|
||||||
|
const mockedGenerateKeyBetween = vi.mocked(generateKeyBetween);
|
||||||
|
|
||||||
|
const createElement = (
|
||||||
|
id: string,
|
||||||
|
index: string,
|
||||||
|
group: TestElement | null
|
||||||
|
): TestElement => ({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
group,
|
||||||
|
childElements: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const createUngroupFixture = () => {
|
||||||
|
const parent = createElement('parent', 'p0', null);
|
||||||
|
const left = createElement('left', 'a0', parent);
|
||||||
|
const right = createElement('right', 'a0', parent);
|
||||||
|
const group = createElement('group', 'm0', parent);
|
||||||
|
const childA = createElement('child-a', 'c0', group);
|
||||||
|
const childB = createElement('child-b', 'c1', group);
|
||||||
|
|
||||||
|
group.childElements = [childB, childA];
|
||||||
|
parent.childElements = [left, group, right];
|
||||||
|
|
||||||
|
parent.removeChildren = vi.fn();
|
||||||
|
parent.addChildren = vi.fn();
|
||||||
|
group.removeChildren = vi.fn();
|
||||||
|
|
||||||
|
const elementOrder = new Map<TestElement, number>([
|
||||||
|
[left, 0],
|
||||||
|
[group, 1],
|
||||||
|
[right, 2],
|
||||||
|
[childA, 3],
|
||||||
|
[childB, 4],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectionSet = vi.fn();
|
||||||
|
const gfx = {
|
||||||
|
layer: {
|
||||||
|
compare: (a: TestElement, b: TestElement) =>
|
||||||
|
(elementOrder.get(a) ?? 0) - (elementOrder.get(b) ?? 0),
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
set: selectionSet,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const std = {
|
||||||
|
get: vi.fn(() => gfx),
|
||||||
|
store: {
|
||||||
|
transact: (callback: () => void) => callback(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
childA,
|
||||||
|
childB,
|
||||||
|
group,
|
||||||
|
parent,
|
||||||
|
selectionSet,
|
||||||
|
std,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ungroupCommand', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedGenerateNKeysBetween.mockReset();
|
||||||
|
mockedGenerateKeyBetween.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to open-ended key generation when sibling interval is invalid', () => {
|
||||||
|
const fixture = createUngroupFixture();
|
||||||
|
mockedGenerateNKeysBetween
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error('interval reversed');
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce(['n0', 'n1']);
|
||||||
|
|
||||||
|
const next = vi.fn();
|
||||||
|
ungroupCommand(
|
||||||
|
{
|
||||||
|
std: fixture.std,
|
||||||
|
group: fixture.group as any,
|
||||||
|
} as any,
|
||||||
|
next
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockedGenerateNKeysBetween).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'a0',
|
||||||
|
'a0',
|
||||||
|
2
|
||||||
|
);
|
||||||
|
expect(mockedGenerateNKeysBetween).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'a0',
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
expect(fixture.childA.index).toBe('n0');
|
||||||
|
expect(fixture.childB.index).toBe('n1');
|
||||||
|
expect(fixture.selectionSet).toHaveBeenCalledWith({
|
||||||
|
editing: false,
|
||||||
|
elements: ['child-a', 'child-b'],
|
||||||
|
});
|
||||||
|
expect(next).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to key-by-key generation when all batched strategies fail', () => {
|
||||||
|
const fixture = createUngroupFixture();
|
||||||
|
mockedGenerateNKeysBetween.mockImplementation(() => {
|
||||||
|
throw new Error('invalid range');
|
||||||
|
});
|
||||||
|
|
||||||
|
let seq = 0;
|
||||||
|
mockedGenerateKeyBetween.mockImplementation(() => `k${seq++}`);
|
||||||
|
|
||||||
|
ungroupCommand(
|
||||||
|
{
|
||||||
|
std: fixture.std,
|
||||||
|
group: fixture.group as any,
|
||||||
|
} as any,
|
||||||
|
vi.fn()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockedGenerateNKeysBetween).toHaveBeenCalledTimes(4);
|
||||||
|
expect(mockedGenerateKeyBetween).toHaveBeenCalledTimes(2);
|
||||||
|
expect(fixture.childA.index).toBe('k0');
|
||||||
|
expect(fixture.childB.index).toBe('k1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,80 @@ import {
|
|||||||
MindmapElementModel,
|
MindmapElementModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import type { Command } from '@blocksuite/std';
|
import type { Command } from '@blocksuite/std';
|
||||||
import { GfxControllerIdentifier, type GfxModel } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
batchAddChildren,
|
||||||
|
batchRemoveChildren,
|
||||||
|
type GfxController,
|
||||||
|
GfxControllerIdentifier,
|
||||||
|
type GfxModel,
|
||||||
|
measureOperation,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
|
||||||
|
|
||||||
|
const getTopLevelOrderedElements = (gfx: GfxController) => {
|
||||||
|
const topLevelElements = gfx.layer.layers.reduce<GfxModel[]>(
|
||||||
|
(elements, layer) => {
|
||||||
|
layer.elements.forEach(element => {
|
||||||
|
if (element.group === null) {
|
||||||
|
elements.push(element as GfxModel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
topLevelElements.sort((a, b) => gfx.layer.compare(a, b));
|
||||||
|
return topLevelElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUngroupIndexes = (
|
||||||
|
orderedElements: GfxModel[],
|
||||||
|
afterIndex: string | null,
|
||||||
|
beforeIndex: string | null,
|
||||||
|
fallbackAnchorIndex: string
|
||||||
|
) => {
|
||||||
|
if (orderedElements.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = orderedElements.length;
|
||||||
|
const tryGenerateN = (left: string | null, right: string | null) => {
|
||||||
|
try {
|
||||||
|
const generated = generateNKeysBetween(left, right, count);
|
||||||
|
return generated.length === count ? generated : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryGenerateOneByOne = (left: string | null, right: string | null) => {
|
||||||
|
try {
|
||||||
|
let cursor = left;
|
||||||
|
return orderedElements.map(() => {
|
||||||
|
cursor = generateKeyBetween(cursor, right);
|
||||||
|
return cursor;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preferred: keep ungrouped children in the original group slot.
|
||||||
|
return (
|
||||||
|
tryGenerateN(afterIndex, beforeIndex) ??
|
||||||
|
// Fallback: ignore the upper bound when legacy/broken data has reversed interval.
|
||||||
|
tryGenerateN(afterIndex, null) ??
|
||||||
|
// Fallback: use group index as anchor when sibling interval is unavailable.
|
||||||
|
tryGenerateN(fallbackAnchorIndex, null) ??
|
||||||
|
// Last resort: always valid.
|
||||||
|
tryGenerateN(null, null) ??
|
||||||
|
// Defensive fallback for unexpected library behavior.
|
||||||
|
tryGenerateOneByOne(null, null) ??
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const createGroupCommand: Command<
|
export const createGroupCommand: Command<
|
||||||
{ elements: GfxModel[] | string[] },
|
{ elements: GfxModel[] | string[] },
|
||||||
@@ -39,96 +112,118 @@ export const createGroupFromSelectedCommand: Command<
|
|||||||
{},
|
{},
|
||||||
{ groupId: string }
|
{ groupId: string }
|
||||||
> = (ctx, next) => {
|
> = (ctx, next) => {
|
||||||
const { std } = ctx;
|
measureOperation('edgeless:create-group-from-selected', () => {
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
const { std } = ctx;
|
||||||
const { selection, surface } = gfx;
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
|
const { selection, surface } = gfx;
|
||||||
|
|
||||||
if (!surface) {
|
if (!surface) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selection.selectedElements.length === 0 ||
|
selection.selectedElements.length === 0 ||
|
||||||
!selection.selectedElements.every(
|
!selection.selectedElements.every(
|
||||||
element =>
|
element =>
|
||||||
element.group === selection.firstElement.group &&
|
element.group === selection.firstElement.group &&
|
||||||
!(element.group instanceof MindmapElementModel)
|
!(element.group instanceof MindmapElementModel)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = selection.firstElement.group as GroupElementModel;
|
const parent = selection.firstElement.group;
|
||||||
|
let groupId: string | undefined;
|
||||||
|
std.store.transact(() => {
|
||||||
|
const [_, result] = std.command.exec(createGroupCommand, {
|
||||||
|
elements: selection.selectedElements,
|
||||||
|
});
|
||||||
|
|
||||||
if (parent !== null) {
|
if (!result.groupId) {
|
||||||
selection.selectedElements.forEach(element => {
|
return;
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
}
|
||||||
parent.removeChild(element);
|
|
||||||
|
groupId = result.groupId;
|
||||||
|
const group = surface.getElementById(groupId);
|
||||||
|
|
||||||
|
if (parent !== null && group) {
|
||||||
|
batchRemoveChildren(parent, selection.selectedElements);
|
||||||
|
batchAddChildren(parent, [group]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const [_, result] = std.command.exec(createGroupCommand, {
|
if (!groupId) {
|
||||||
elements: selection.selectedElements,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.set({
|
||||||
|
editing: false,
|
||||||
|
elements: [groupId],
|
||||||
|
});
|
||||||
|
|
||||||
|
next({ groupId });
|
||||||
});
|
});
|
||||||
if (!result.groupId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const group = surface.getElementById(result.groupId);
|
|
||||||
|
|
||||||
if (parent !== null && group) {
|
|
||||||
parent.addChild(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
selection.set({
|
|
||||||
editing: false,
|
|
||||||
elements: [result.groupId],
|
|
||||||
});
|
|
||||||
|
|
||||||
next({ groupId: result.groupId });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ungroupCommand: Command<{ group: GroupElementModel }, {}> = (
|
export const ungroupCommand: Command<{ group: GroupElementModel }, {}> = (
|
||||||
ctx,
|
ctx,
|
||||||
next
|
next
|
||||||
) => {
|
) => {
|
||||||
const { std, group } = ctx;
|
measureOperation('edgeless:ungroup', () => {
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
const { std, group } = ctx;
|
||||||
const { selection } = gfx;
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
const parent = group.group as GroupElementModel;
|
const { selection } = gfx;
|
||||||
const elements = group.childElements;
|
const parent = group.group;
|
||||||
|
const elements = [...group.childElements];
|
||||||
|
|
||||||
if (group instanceof MindmapElementModel) {
|
if (group instanceof MindmapElementModel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent !== null) {
|
const orderedElements = [...elements].sort((a, b) =>
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
gfx.layer.compare(a, b)
|
||||||
parent.removeChild(group);
|
);
|
||||||
}
|
const siblings = parent
|
||||||
|
? [...parent.childElements].sort((a, b) => gfx.layer.compare(a, b))
|
||||||
|
: getTopLevelOrderedElements(gfx);
|
||||||
|
const groupPosition = siblings.indexOf(group);
|
||||||
|
const beforeSiblingIndex =
|
||||||
|
groupPosition > 0 ? (siblings[groupPosition - 1]?.index ?? null) : null;
|
||||||
|
const afterSiblingIndex =
|
||||||
|
groupPosition === -1
|
||||||
|
? null
|
||||||
|
: (siblings[groupPosition + 1]?.index ?? null);
|
||||||
|
const nextIndexes = buildUngroupIndexes(
|
||||||
|
orderedElements,
|
||||||
|
beforeSiblingIndex,
|
||||||
|
afterSiblingIndex,
|
||||||
|
group.index
|
||||||
|
);
|
||||||
|
|
||||||
elements.forEach(element => {
|
std.store.transact(() => {
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
if (parent !== null) {
|
||||||
group.removeChild(element);
|
batchRemoveChildren(parent, [group]);
|
||||||
});
|
}
|
||||||
|
|
||||||
// keep relative index order of group children after ungroup
|
batchRemoveChildren(group, elements);
|
||||||
elements
|
|
||||||
.sort((a, b) => gfx.layer.compare(a, b))
|
// keep relative index order of group children after ungroup
|
||||||
.forEach(element => {
|
orderedElements.forEach((element, idx) => {
|
||||||
std.store.transact(() => {
|
const index = nextIndexes[idx];
|
||||||
element.index = gfx.layer.generateIndex();
|
if (element.index !== index) {
|
||||||
|
element.index = index;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (parent !== null) {
|
||||||
|
batchAddChildren(parent, orderedElements);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parent !== null) {
|
selection.set({
|
||||||
elements.forEach(element => {
|
editing: false,
|
||||||
parent.addChild(element);
|
elements: orderedElements.map(ele => ele.id),
|
||||||
});
|
});
|
||||||
}
|
next();
|
||||||
|
|
||||||
selection.set({
|
|
||||||
editing: false,
|
|
||||||
elements: elements.map(ele => ele.id),
|
|
||||||
});
|
});
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
|
|||||||
25
blocksuite/affine/gfx/group/vitest.config.ts
Normal file
25
blocksuite/affine/gfx/group/vitest.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
esbuild: {
|
||||||
|
target: 'es2018',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globalSetup: '../../../scripts/vitest-global.js',
|
||||||
|
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||||
|
testTimeout: 1000,
|
||||||
|
coverage: {
|
||||||
|
provider: 'istanbul',
|
||||||
|
reporter: ['lcov'],
|
||||||
|
reportsDirectory: '../../../.coverage/affine-gfx-group',
|
||||||
|
},
|
||||||
|
onConsoleLog(log, type) {
|
||||||
|
if (log.includes('lit.dev/msg/dev-mode')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.warn(`Unexpected ${type} log`, log);
|
||||||
|
throw new Error(log);
|
||||||
|
},
|
||||||
|
environment: 'happy-dom',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -45,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"simple-xml-to-json": "^1.2.2",
|
"simple-xml-to-json": "^1.2.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
@@ -51,5 +50,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -45,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,13 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./view": "./src/view.ts"
|
"./view": "./src/view.ts"
|
||||||
@@ -42,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdaptiveCooldownController,
|
||||||
|
AdaptiveStrideController,
|
||||||
|
} from '../snap/adaptive-load-controller.js';
|
||||||
|
|
||||||
|
describe('AdaptiveStrideController', () => {
|
||||||
|
test('increases stride under heavy cost and respects maxStride', () => {
|
||||||
|
const controller = new AdaptiveStrideController({
|
||||||
|
heavyCostMs: 6,
|
||||||
|
maxStride: 3,
|
||||||
|
recoveryCostMs: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.reportCost(10);
|
||||||
|
controller.reportCost(12);
|
||||||
|
controller.reportCost(15);
|
||||||
|
|
||||||
|
// stride should be capped at 3, so only every 3rd tick runs.
|
||||||
|
expect(controller.shouldSkip()).toBe(false);
|
||||||
|
expect(controller.shouldSkip()).toBe(true);
|
||||||
|
expect(controller.shouldSkip()).toBe(true);
|
||||||
|
expect(controller.shouldSkip()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decreases stride when cost recovers and reset clears state', () => {
|
||||||
|
const controller = new AdaptiveStrideController({
|
||||||
|
heavyCostMs: 8,
|
||||||
|
maxStride: 4,
|
||||||
|
recoveryCostMs: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.reportCost(12);
|
||||||
|
controller.reportCost(12);
|
||||||
|
controller.reportCost(1);
|
||||||
|
|
||||||
|
// From stride 3 recovered to stride 2: run every other tick.
|
||||||
|
expect(controller.shouldSkip()).toBe(false);
|
||||||
|
expect(controller.shouldSkip()).toBe(true);
|
||||||
|
expect(controller.shouldSkip()).toBe(false);
|
||||||
|
|
||||||
|
controller.reset();
|
||||||
|
expect(controller.shouldSkip()).toBe(false);
|
||||||
|
expect(controller.shouldSkip()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AdaptiveCooldownController', () => {
|
||||||
|
test('enters cooldown when cost exceeds threshold', () => {
|
||||||
|
const controller = new AdaptiveCooldownController({
|
||||||
|
cooldownFrames: 2,
|
||||||
|
maxCostMs: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.reportCost(9);
|
||||||
|
expect(controller.shouldRun()).toBe(false);
|
||||||
|
expect(controller.shouldRun()).toBe(false);
|
||||||
|
expect(controller.shouldRun()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reset exits cooldown immediately', () => {
|
||||||
|
const controller = new AdaptiveCooldownController({
|
||||||
|
cooldownFrames: 3,
|
||||||
|
maxCostMs: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.reportCost(6);
|
||||||
|
expect(controller.shouldRun()).toBe(false);
|
||||||
|
controller.reset();
|
||||||
|
expect(controller.shouldRun()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
|
import { MouseButton } from '@blocksuite/std/gfx';
|
||||||
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { PanTool } from '../tools/pan-tool.js';
|
||||||
|
|
||||||
|
type PointerDownHandler = (event: {
|
||||||
|
raw: {
|
||||||
|
button: number;
|
||||||
|
preventDefault: () => void;
|
||||||
|
};
|
||||||
|
}) => unknown;
|
||||||
|
|
||||||
|
const mockRaf = () => {
|
||||||
|
let callback: FrameRequestCallback | undefined;
|
||||||
|
const requestAnimationFrameMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((cb: FrameRequestCallback) => {
|
||||||
|
callback = cb;
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
const cancelAnimationFrameMock = vi.fn();
|
||||||
|
|
||||||
|
vi.stubGlobal('requestAnimationFrame', requestAnimationFrameMock);
|
||||||
|
vi.stubGlobal('cancelAnimationFrame', cancelAnimationFrameMock);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCallback: () => callback,
|
||||||
|
requestAnimationFrameMock,
|
||||||
|
cancelAnimationFrameMock,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createToolFixture = (options?: {
|
||||||
|
currentToolName?: string;
|
||||||
|
currentToolOptions?: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
|
const applyDeltaCenter = vi.fn();
|
||||||
|
const selectionSet = vi.fn();
|
||||||
|
const setTool = vi.fn();
|
||||||
|
const navigatorSettingUpdated = {
|
||||||
|
next: vi.fn(),
|
||||||
|
};
|
||||||
|
const currentToolName = options?.currentToolName;
|
||||||
|
const currentToolOption = {
|
||||||
|
toolType: currentToolName
|
||||||
|
? ({
|
||||||
|
toolName: currentToolName,
|
||||||
|
} as any)
|
||||||
|
: undefined,
|
||||||
|
options: options?.currentToolOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gfx = {
|
||||||
|
viewport: {
|
||||||
|
zoom: 2,
|
||||||
|
applyDeltaCenter,
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
surfaceSelections: [{ elements: ['shape-1'] }],
|
||||||
|
set: selectionSet,
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
currentTool$: {
|
||||||
|
peek: () => null,
|
||||||
|
},
|
||||||
|
currentToolOption$: {
|
||||||
|
peek: () => currentToolOption,
|
||||||
|
},
|
||||||
|
setTool,
|
||||||
|
},
|
||||||
|
std: {
|
||||||
|
get: (identifier: unknown) => {
|
||||||
|
if (identifier === EdgelessLegacySlotIdentifier) {
|
||||||
|
return { navigatorSettingUpdated };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
doc: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tool = new PanTool(gfx as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyDeltaCenter,
|
||||||
|
navigatorSettingUpdated,
|
||||||
|
selectionSet,
|
||||||
|
setTool,
|
||||||
|
tool,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PanTool', () => {
|
||||||
|
test('flushes accumulated delta on dragEnd', () => {
|
||||||
|
mockRaf();
|
||||||
|
const { tool, applyDeltaCenter } = createToolFixture();
|
||||||
|
|
||||||
|
tool.dragStart({ x: 100, y: 100 } as any);
|
||||||
|
tool.dragMove({ x: 80, y: 60 } as any);
|
||||||
|
tool.dragMove({ x: 70, y: 40 } as any);
|
||||||
|
|
||||||
|
expect(applyDeltaCenter).not.toHaveBeenCalled();
|
||||||
|
tool.dragEnd({} as any);
|
||||||
|
|
||||||
|
expect(applyDeltaCenter).toHaveBeenCalledTimes(1);
|
||||||
|
expect(applyDeltaCenter).toHaveBeenCalledWith(15, 30);
|
||||||
|
expect(tool.panning$.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancel in unmounted drops pending deltas', () => {
|
||||||
|
mockRaf();
|
||||||
|
const { tool, applyDeltaCenter } = createToolFixture();
|
||||||
|
|
||||||
|
tool.dragStart({ x: 100, y: 100 } as any);
|
||||||
|
tool.dragMove({ x: 80, y: 60 } as any);
|
||||||
|
tool.unmounted();
|
||||||
|
tool.dragEnd({} as any);
|
||||||
|
|
||||||
|
expect(applyDeltaCenter).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('middle click temporary pan restores frameNavigator with restoredAfterPan', () => {
|
||||||
|
const { tool, navigatorSettingUpdated, selectionSet, setTool } =
|
||||||
|
createToolFixture({
|
||||||
|
currentToolName: 'frameNavigator',
|
||||||
|
currentToolOptions: { mode: 'fit' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const hooks: Partial<Record<'pointerDown', PointerDownHandler>> = {};
|
||||||
|
(tool as any).eventTarget = {
|
||||||
|
addHook: (eventName: 'pointerDown', handler: PointerDownHandler) => {
|
||||||
|
hooks[eventName] = handler;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.mounted();
|
||||||
|
|
||||||
|
const preventDefault = vi.fn();
|
||||||
|
const pointerDown = hooks.pointerDown!;
|
||||||
|
const ret = pointerDown({
|
||||||
|
raw: {
|
||||||
|
button: MouseButton.MIDDLE,
|
||||||
|
preventDefault,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ret).toBe(false);
|
||||||
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
|
expect(navigatorSettingUpdated.next).toHaveBeenCalledWith({
|
||||||
|
blackBackground: false,
|
||||||
|
});
|
||||||
|
expect(setTool).toHaveBeenNthCalledWith(1, PanTool, {
|
||||||
|
panning: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new PointerEvent('pointerup', { button: MouseButton.MIDDLE })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selectionSet).toHaveBeenCalledWith([{ elements: ['shape-1'] }]);
|
||||||
|
expect(setTool).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
toolName: 'frameNavigator',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
mode: 'fit',
|
||||||
|
restoredAfterPan: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
export class AdaptiveStrideController {
|
||||||
|
private _stride = 1;
|
||||||
|
|
||||||
|
private _ticks = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly _options: {
|
||||||
|
heavyCostMs: number;
|
||||||
|
maxStride: number;
|
||||||
|
recoveryCostMs: number;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
reportCost(costMs: number) {
|
||||||
|
if (costMs > this._options.heavyCostMs) {
|
||||||
|
this._stride = Math.min(this._options.maxStride, this._stride + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (costMs < this._options.recoveryCostMs && this._stride > 1) {
|
||||||
|
this._stride -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._stride = 1;
|
||||||
|
this._ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldSkip() {
|
||||||
|
const shouldSkip = this._stride > 1 && this._ticks % this._stride !== 0;
|
||||||
|
this._ticks += 1;
|
||||||
|
return shouldSkip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdaptiveCooldownController {
|
||||||
|
private _remainingFrames = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly _options: {
|
||||||
|
cooldownFrames: number;
|
||||||
|
maxCostMs: number;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
reportCost(costMs: number) {
|
||||||
|
if (costMs > this._options.maxCostMs) {
|
||||||
|
this._remainingFrames = this._options.cooldownFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._remainingFrames = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRun() {
|
||||||
|
if (this._remainingFrames <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._remainingFrames -= 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,18 @@ import {
|
|||||||
InteractivityExtension,
|
InteractivityExtension,
|
||||||
} from '@blocksuite/std/gfx';
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
|
import { AdaptiveStrideController } from './adaptive-load-controller';
|
||||||
import type { SnapOverlay } from './snap-overlay';
|
import type { SnapOverlay } from './snap-overlay';
|
||||||
|
|
||||||
export class SnapExtension extends InteractivityExtension {
|
export class SnapExtension extends InteractivityExtension {
|
||||||
static override key = 'snap-manager';
|
static override key = 'snap-manager';
|
||||||
|
|
||||||
|
private static readonly MAX_ALIGN_SKIP_STRIDE = 3;
|
||||||
|
|
||||||
|
private static readonly ALIGN_HEAVY_COST_MS = 5;
|
||||||
|
|
||||||
|
private static readonly ALIGN_RECOVERY_COST_MS = 2;
|
||||||
|
|
||||||
get snapOverlay() {
|
get snapOverlay() {
|
||||||
return this.std.getOptional(
|
return this.std.getOptional(
|
||||||
OverlayIdentifier('snap-manager')
|
OverlayIdentifier('snap-manager')
|
||||||
@@ -29,6 +36,11 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let alignBound: Bound | null = null;
|
let alignBound: Bound | null = null;
|
||||||
|
const alignStride = new AdaptiveStrideController({
|
||||||
|
heavyCostMs: SnapExtension.ALIGN_HEAVY_COST_MS,
|
||||||
|
maxStride: SnapExtension.MAX_ALIGN_SKIP_STRIDE,
|
||||||
|
recoveryCostMs: SnapExtension.ALIGN_RECOVERY_COST_MS,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
@@ -42,6 +54,7 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
return pre;
|
return pre;
|
||||||
}, [] as GfxModel[])
|
}, [] as GfxModel[])
|
||||||
);
|
);
|
||||||
|
alignStride.reset();
|
||||||
},
|
},
|
||||||
onDragMove(context: ExtensionDragMoveContext) {
|
onDragMove(context: ExtensionDragMoveContext) {
|
||||||
if (
|
if (
|
||||||
@@ -53,14 +66,22 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (alignStride.shouldSkip()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentBound = alignBound.moveDelta(context.dx, context.dy);
|
const currentBound = alignBound.moveDelta(context.dx, context.dy);
|
||||||
|
const alignStart = performance.now();
|
||||||
const alignRst = snapOverlay.align(currentBound);
|
const alignRst = snapOverlay.align(currentBound);
|
||||||
|
const alignCost = performance.now() - alignStart;
|
||||||
|
alignStride.reportCost(alignCost);
|
||||||
|
|
||||||
context.dx = alignRst.dx + context.dx;
|
context.dx = alignRst.dx + context.dx;
|
||||||
context.dy = alignRst.dy + context.dy;
|
context.dy = alignRst.dy + context.dy;
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
alignBound = null;
|
alignBound = null;
|
||||||
|
alignStride.reset();
|
||||||
snapOverlay.clear();
|
snapOverlay.clear();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
||||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
|
import { AdaptiveCooldownController } from './adaptive-load-controller';
|
||||||
|
|
||||||
interface Distance {
|
interface Distance {
|
||||||
horiz?: {
|
horiz?: {
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +37,9 @@ interface Distance {
|
|||||||
const ALIGN_THRESHOLD = 8;
|
const ALIGN_THRESHOLD = 8;
|
||||||
const DISTRIBUTION_LINE_OFFSET = 1;
|
const DISTRIBUTION_LINE_OFFSET = 1;
|
||||||
const STROKE_WIDTH = 2;
|
const STROKE_WIDTH = 2;
|
||||||
|
const DISTRIBUTE_ALIGN_MAX_CANDIDATES = 160;
|
||||||
|
const DISTRIBUTE_ALIGN_MAX_COST_MS = 5;
|
||||||
|
const DISTRIBUTE_ALIGN_COOLDOWN_FRAMES = 2;
|
||||||
|
|
||||||
export class SnapOverlay extends Overlay {
|
export class SnapOverlay extends Overlay {
|
||||||
static override overlayName: string = 'snap-manager';
|
static override overlayName: string = 'snap-manager';
|
||||||
@@ -75,6 +80,11 @@ export class SnapOverlay extends Overlay {
|
|||||||
vertical: [],
|
vertical: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _distributeCooldown = new AdaptiveCooldownController({
|
||||||
|
cooldownFrames: DISTRIBUTE_ALIGN_COOLDOWN_FRAMES,
|
||||||
|
maxCostMs: DISTRIBUTE_ALIGN_MAX_COST_MS,
|
||||||
|
});
|
||||||
|
|
||||||
override clear() {
|
override clear() {
|
||||||
this._referenceBounds = {
|
this._referenceBounds = {
|
||||||
vertical: [],
|
vertical: [],
|
||||||
@@ -87,6 +97,7 @@ export class SnapOverlay extends Overlay {
|
|||||||
};
|
};
|
||||||
this._distributedAlignLines = [];
|
this._distributedAlignLines = [];
|
||||||
this._skippedElements.clear();
|
this._skippedElements.clear();
|
||||||
|
this._distributeCooldown.reset();
|
||||||
|
|
||||||
super.clear();
|
super.clear();
|
||||||
}
|
}
|
||||||
@@ -673,13 +684,24 @@ export class SnapOverlay extends Overlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// point align priority is higher than distribute align
|
const shouldTryDistribute =
|
||||||
if (rst.dx === 0) {
|
this._referenceBounds.all.length <= DISTRIBUTE_ALIGN_MAX_CANDIDATES &&
|
||||||
this._alignDistributeHorizontally(rst, bound, threshold, viewport);
|
this._distributeCooldown.shouldRun();
|
||||||
}
|
|
||||||
|
|
||||||
if (rst.dy === 0) {
|
if (shouldTryDistribute) {
|
||||||
this._alignDistributeVertically(rst, bound, threshold, viewport);
|
const distributeStart = performance.now();
|
||||||
|
|
||||||
|
// point align priority is higher than distribute align
|
||||||
|
if (rst.dx === 0) {
|
||||||
|
this._alignDistributeHorizontally(rst, bound, threshold, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rst.dy === 0) {
|
||||||
|
this._alignDistributeVertically(rst, bound, threshold, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
const distributeCost = performance.now() - distributeStart;
|
||||||
|
this._distributeCooldown.reportCost(distributeCost);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._renderer?.refresh();
|
this._renderer?.refresh();
|
||||||
@@ -776,24 +798,26 @@ export class SnapOverlay extends Overlay {
|
|||||||
});
|
});
|
||||||
const verticalBounds: Bound[] = [];
|
const verticalBounds: Bound[] = [];
|
||||||
const horizBounds: Bound[] = [];
|
const horizBounds: Bound[] = [];
|
||||||
const allBounds: Bound[] = [];
|
const allCandidateElements = new Set<GfxModel>();
|
||||||
|
|
||||||
vertCandidates.forEach(candidate => {
|
vertCandidates.forEach(candidate => {
|
||||||
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
||||||
verticalBounds.push(candidate.elementBound);
|
const bound = candidate.elementBound;
|
||||||
allBounds.push(candidate.elementBound);
|
verticalBounds.push(bound);
|
||||||
|
allCandidateElements.add(candidate);
|
||||||
});
|
});
|
||||||
|
|
||||||
horizCandidates.forEach(candidate => {
|
horizCandidates.forEach(candidate => {
|
||||||
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
||||||
horizBounds.push(candidate.elementBound);
|
const bound = candidate.elementBound;
|
||||||
allBounds.push(candidate.elementBound);
|
horizBounds.push(bound);
|
||||||
|
allCandidateElements.add(candidate);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._referenceBounds = {
|
this._referenceBounds = {
|
||||||
horizontal: horizBounds,
|
horizontal: horizBounds,
|
||||||
vertical: verticalBounds,
|
vertical: verticalBounds,
|
||||||
all: allBounds,
|
all: [...allCandidateElements].map(element => element.elementBound),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import {
|
|||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import { on } from '@blocksuite/affine-shared/utils';
|
import { on } from '@blocksuite/affine-shared/utils';
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import type { PointerEventState } from '@blocksuite/std';
|
||||||
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
BaseTool,
|
||||||
|
createRafCoalescer,
|
||||||
|
MouseButton,
|
||||||
|
type ToolOptions,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import { Signal } from '@preact/signals-core';
|
import { Signal } from '@preact/signals-core';
|
||||||
|
|
||||||
interface RestorablePresentToolOptions {
|
interface RestorablePresentToolOptions {
|
||||||
@@ -21,13 +26,30 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
|
|
||||||
private _lastPoint: [number, number] | null = null;
|
private _lastPoint: [number, number] | null = null;
|
||||||
|
|
||||||
|
private _pendingDelta: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
private readonly _deltaFlushCoalescer = createRafCoalescer<void>(() => {
|
||||||
|
this._flushPendingDelta();
|
||||||
|
});
|
||||||
|
|
||||||
readonly panning$ = new Signal<boolean>(false);
|
readonly panning$ = new Signal<boolean>(false);
|
||||||
|
|
||||||
|
private _flushPendingDelta() {
|
||||||
|
if (this._pendingDelta[0] === 0 && this._pendingDelta[1] === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [deltaX, deltaY] = this._pendingDelta;
|
||||||
|
this._pendingDelta = [0, 0];
|
||||||
|
this.gfx.viewport.applyDeltaCenter(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
override get allowDragWithRightButton(): boolean {
|
override get allowDragWithRightButton(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragEnd(_: PointerEventState): void {
|
override dragEnd(_: PointerEventState): void {
|
||||||
|
this._deltaFlushCoalescer.flush();
|
||||||
this._lastPoint = null;
|
this._lastPoint = null;
|
||||||
this.panning$.value = false;
|
this.panning$.value = false;
|
||||||
}
|
}
|
||||||
@@ -43,12 +65,14 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
const deltaY = lastY - e.y;
|
const deltaY = lastY - e.y;
|
||||||
|
|
||||||
this._lastPoint = [e.x, e.y];
|
this._lastPoint = [e.x, e.y];
|
||||||
|
this._pendingDelta[0] += deltaX / zoom;
|
||||||
viewport.applyDeltaCenter(deltaX / zoom, deltaY / zoom);
|
this._pendingDelta[1] += deltaY / zoom;
|
||||||
|
this._deltaFlushCoalescer.schedule(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragStart(e: PointerEventState): void {
|
override dragStart(e: PointerEventState): void {
|
||||||
this._lastPoint = [e.x, e.y];
|
this._lastPoint = [e.x, e.y];
|
||||||
|
this._pendingDelta = [0, 0];
|
||||||
this.panning$.value = true;
|
this.panning$.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,4 +144,8 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override unmounted(): void {
|
||||||
|
this._deltaFlushCoalescer.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
blocksuite/affine/gfx/pointer/vitest.config.ts
Normal file
25
blocksuite/affine/gfx/pointer/vitest.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
esbuild: {
|
||||||
|
target: 'es2018',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globalSetup: '../../../scripts/vitest-global.js',
|
||||||
|
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||||
|
testTimeout: 1000,
|
||||||
|
coverage: {
|
||||||
|
provider: 'istanbul',
|
||||||
|
reporter: ['lcov'],
|
||||||
|
reportsDirectory: '../../../.coverage/affine-gfx-pointer',
|
||||||
|
},
|
||||||
|
onConsoleLog(log, type) {
|
||||||
|
if (log.includes('lit.dev/msg/dev-mode')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.warn(`Unexpected ${type} log`, log);
|
||||||
|
throw new Error(log);
|
||||||
|
},
|
||||||
|
environment: 'happy-dom',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -44,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -44,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/await-thenable */
|
||||||
import type {
|
import type {
|
||||||
Template,
|
Template,
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -43,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,5 +25,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,5 +50,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,5 +56,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.1"
|
"version": "0.26.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,9 +155,22 @@ export class FrameBlockModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeChild(element: GfxModel): void {
|
removeChild(element: GfxModel): void {
|
||||||
|
this.removeChildren([element]);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChildren(elements: GfxModel[]): void {
|
||||||
|
const childIds = [...new Set(elements.map(element => element.id))];
|
||||||
|
if (!this.props.childElementIds || childIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.store.transact(() => {
|
this.store.transact(() => {
|
||||||
this.props.childElementIds &&
|
const childElementIds = this.props.childElementIds;
|
||||||
delete this.props.childElementIds[element.id];
|
if (!childElementIds) return;
|
||||||
|
|
||||||
|
childIds.forEach(childId => {
|
||||||
|
delete childElementIds[childId];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user