mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-23 15:50:43 +08:00
Compare commits
98 Commits
v0.26.2
...
darksky/bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52f4461fff | ||
|
|
d7d67841b8 | ||
|
|
29a27b561b | ||
|
|
02744cec00 | ||
|
|
6d710f3bdc | ||
|
|
0b47f92134 | ||
|
|
9c55edeb62 | ||
|
|
9742e9735e | ||
|
|
86d65b2f64 | ||
|
|
f34e25e122 | ||
|
|
b5d5b71f95 | ||
|
|
09fa1a8e4e | ||
|
|
c249011238 | ||
|
|
7f5f7e79df | ||
|
|
fff04395bc | ||
|
|
bbc01533d7 | ||
|
|
e31cca3354 | ||
|
|
11bc333714 | ||
|
|
99b07c2ee1 | ||
|
|
fc9b99cd17 | ||
|
|
2137f68871 | ||
|
|
75efa854bf | ||
|
|
c0139abf79 | ||
|
|
5a38e765bd | ||
|
|
d3dcdd47ee | ||
|
|
727c9d6d71 | ||
|
|
274f491e49 | ||
|
|
478138493a | ||
|
|
5464d1a9ce | ||
|
|
4c40dcacd9 | ||
|
|
76d28aaa38 | ||
|
|
86f48240ce | ||
|
|
c5d622531c | ||
|
|
60acd81d4b | ||
|
|
78f567a178 | ||
|
|
784382cfb1 | ||
|
|
342451be1b | ||
|
|
2b6146727b | ||
|
|
d5245a3273 | ||
|
|
fff63562b1 | ||
|
|
4136abdd97 | ||
|
|
e249e2e884 | ||
|
|
2e95d91093 | ||
|
|
2cb171f553 | ||
|
|
a4e2242b8d | ||
|
|
c90f173821 | ||
|
|
e1e0ac2345 | ||
|
|
bdccf4e9fd | ||
|
|
11cf1928b5 | ||
|
|
5215c73166 | ||
|
|
895e774569 | ||
|
|
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 | ||
|
|
72df9cb457 | ||
|
|
98e5747fdc | ||
|
|
4460604dd3 | ||
|
|
b4be9118ad | ||
|
|
b46bf91575 | ||
|
|
3ad482351b | ||
|
|
03b1d15a8f | ||
|
|
52c7b04a01 |
@@ -19,3 +19,8 @@ rustflags = [
|
||||
# pthread_key_create() destructors and segfault after a DSO unloading
|
||||
[target.'cfg(all(target_env = "gnu", not(target_os = "windows")))']
|
||||
rustflags = ["-C", "link-args=-Wl,-z,nodelete"]
|
||||
|
||||
# Temporary local llm_adapter override.
|
||||
# Uncomment when verifying AFFiNE against the sibling llm_adapter workspace.
|
||||
# [patch.crates-io]
|
||||
# llm_adapter = { path = "../llm_adapter" }
|
||||
|
||||
@@ -197,8 +197,8 @@
|
||||
"properties": {
|
||||
"SMTP.name": {
|
||||
"type": "string",
|
||||
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
|
||||
"default": "AFFiNE Server"
|
||||
"description": "Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"\n@environment `MAILER_SERVERNAME`",
|
||||
"default": ""
|
||||
},
|
||||
"SMTP.host": {
|
||||
"type": "string",
|
||||
@@ -222,7 +222,7 @@
|
||||
},
|
||||
"SMTP.sender": {
|
||||
"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>"
|
||||
},
|
||||
"SMTP.ignoreTLS": {
|
||||
@@ -237,8 +237,8 @@
|
||||
},
|
||||
"fallbackSMTP.name": {
|
||||
"type": "string",
|
||||
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
|
||||
"default": "AFFiNE Server"
|
||||
"description": "Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"",
|
||||
"default": ""
|
||||
},
|
||||
"fallbackSMTP.host": {
|
||||
"type": "string",
|
||||
@@ -262,7 +262,7 @@
|
||||
},
|
||||
"fallbackSMTP.sender": {
|
||||
"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": ""
|
||||
},
|
||||
"fallbackSMTP.ignoreTLS": {
|
||||
@@ -971,7 +971,7 @@
|
||||
},
|
||||
"scenarios": {
|
||||
"type": "object",
|
||||
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
|
||||
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-5-mini\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
|
||||
"default": {
|
||||
"override_enabled": false,
|
||||
"scenarios": {
|
||||
@@ -979,15 +979,24 @@
|
||||
"chat": "gemini-2.5-flash",
|
||||
"embedding": "gemini-embedding-001",
|
||||
"image": "gpt-image-1",
|
||||
"rerank": "gpt-4.1",
|
||||
"coding": "claude-sonnet-4-5@20250929",
|
||||
"complex_text_generation": "gpt-4o-2024-08-06",
|
||||
"complex_text_generation": "gpt-5-mini",
|
||||
"quick_decision_making": "gpt-5-mini",
|
||||
"quick_text_generation": "gemini-2.5-flash",
|
||||
"polish_and_summarize": "gemini-2.5-flash"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providers.profiles": {
|
||||
"type": "array",
|
||||
"description": "The profile list for copilot providers.\n@default []",
|
||||
"default": []
|
||||
},
|
||||
"providers.defaults": {
|
||||
"type": "object",
|
||||
"description": "The default provider ids for model output types and global fallback.\n@default {}",
|
||||
"default": {}
|
||||
},
|
||||
"providers.openai": {
|
||||
"type": "object",
|
||||
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.openai.com/v1\"}\n@link https://github.com/openai/openai-node",
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,6 +3,6 @@ contact_links:
|
||||
- name: Something else?
|
||||
url: https://github.com/toeverything/AFFiNE/discussions
|
||||
about: Feel free to ask and answer questions over in GitHub Discussions
|
||||
- name: AFFiNE Community Support
|
||||
url: https://community.affine.pro
|
||||
- name: AFFiNE Community Support (Discord)
|
||||
url: https://affine.pro/redirect/discord
|
||||
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: []
|
||||
10
.github/actions/build-rust/action.yml
vendored
10
.github/actions/build-rust/action.yml
vendored
@@ -50,8 +50,14 @@ runs:
|
||||
# https://github.com/tree-sitter/tree-sitter/issues/4186
|
||||
# pass -D_BSD_SOURCE to clang to fix the tree-sitter build issue
|
||||
run: |
|
||||
echo "CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
|
||||
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
|
||||
if [[ "${{ inputs.target }}" == "aarch64-unknown-linux-gnu" ]]; then
|
||||
# napi cross-toolchain 1.0.3 headers miss AT_HWCAP2 in elf.h
|
||||
echo "CC=clang -D_BSD_SOURCE -DAT_HWCAP2=26" >> "$GITHUB_ENV"
|
||||
echo "TARGET_CC=clang -D_BSD_SOURCE -DAT_HWCAP2=26" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
|
||||
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
33
.github/actions/deploy/deploy.mjs
vendored
33
.github/actions/deploy/deploy.mjs
vendored
@@ -25,30 +25,30 @@ const buildType = BUILD_TYPE || 'canary';
|
||||
|
||||
const isProduction = buildType === 'stable';
|
||||
const isBeta = buildType === 'beta';
|
||||
const isCanary = buildType === 'canary';
|
||||
const isInternal = buildType === 'internal';
|
||||
const isSpotEnabled = isBeta || isCanary;
|
||||
|
||||
const replicaConfig = {
|
||||
stable: {
|
||||
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2,
|
||||
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
|
||||
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
|
||||
},
|
||||
beta: {
|
||||
front: Number(process.env.BETA_FRONT_REPLICA) || 1,
|
||||
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
|
||||
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
|
||||
},
|
||||
canary: { front: 1, graphql: 1, doc: 1 },
|
||||
canary: { front: 1, graphql: 1 },
|
||||
};
|
||||
|
||||
const cpuConfig = {
|
||||
beta: { front: '1', graphql: '1', doc: '1' },
|
||||
canary: { front: '500m', graphql: '1', doc: '500m' },
|
||||
beta: { front: '1', graphql: '1' },
|
||||
canary: { front: '500m', graphql: '1' },
|
||||
};
|
||||
|
||||
const memoryConfig = {
|
||||
beta: { front: '1Gi', graphql: '1Gi', doc: '1Gi' },
|
||||
canary: { front: '512Mi', graphql: '512Mi', doc: '512Mi' },
|
||||
beta: { front: '2Gi', graphql: '1Gi' },
|
||||
canary: { front: '512Mi', graphql: '512Mi' },
|
||||
};
|
||||
|
||||
const createHelmCommand = ({ isDryRun }) => {
|
||||
@@ -72,10 +72,12 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
`--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`,
|
||||
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
|
||||
];
|
||||
const cloudSqlNodeSelector = isBeta
|
||||
? `{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\", \\"cloud.google.com/gke-spot\\": \\"true\\" }`
|
||||
: `{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }`;
|
||||
const serviceAnnotations = [
|
||||
`--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||
`--set-json doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||
].concat(
|
||||
isProduction || isBeta || isInternal
|
||||
? [
|
||||
@@ -84,10 +86,17 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
`--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
||||
`--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
||||
`--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`,
|
||||
`--set-json cloud-sql-proxy.nodeSelector="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`,
|
||||
`--set-json cloud-sql-proxy.nodeSelector="${cloudSqlNodeSelector}"`,
|
||||
]
|
||||
: []
|
||||
);
|
||||
const spotNodeSelector = `{ \\"cloud.google.com/gke-spot\\": \\"true\\" }`;
|
||||
const spotScheduling = isSpotEnabled
|
||||
? [
|
||||
`--set-json front.nodeSelector="${spotNodeSelector}"`,
|
||||
`--set-json graphql.nodeSelector="${spotNodeSelector}"`,
|
||||
]
|
||||
: [];
|
||||
|
||||
const cpu = cpuConfig[buildType];
|
||||
const memory = memoryConfig[buildType];
|
||||
@@ -96,14 +105,12 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
resources = resources.concat([
|
||||
`--set front.resources.requests.cpu="${cpu.front}"`,
|
||||
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
|
||||
`--set doc.resources.requests.cpu="${cpu.doc}"`,
|
||||
]);
|
||||
}
|
||||
if (memory) {
|
||||
resources = resources.concat([
|
||||
`--set front.resources.requests.memory="${memory.front}"`,
|
||||
`--set graphql.resources.requests.memory="${memory.graphql}"`,
|
||||
`--set doc.resources.requests.memory="${memory.doc}"`,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -142,10 +149,8 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
`--set graphql.replicaCount=${replica.graphql}`,
|
||||
`--set-string graphql.image.tag="${imageTag}"`,
|
||||
`--set-string graphql.app.host="${primaryHost}"`,
|
||||
`--set-string doc.image.tag="${imageTag}"`,
|
||||
`--set-string doc.app.host="${primaryHost}"`,
|
||||
`--set doc.replicaCount=${replica.doc}`,
|
||||
...serviceAnnotations,
|
||||
...spotScheduling,
|
||||
...resources,
|
||||
`--timeout 10m`,
|
||||
flag,
|
||||
|
||||
14
.github/actions/setup-node/action.yml
vendored
14
.github/actions/setup-node/action.yml
vendored
@@ -53,7 +53,7 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: https://npm.pkg.github.com
|
||||
@@ -93,7 +93,7 @@ runs:
|
||||
run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache non-full yarn cache on Linux
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }}
|
||||
with:
|
||||
path: |
|
||||
@@ -105,7 +105,7 @@ runs:
|
||||
# and the decompression performance on Windows is very terrible
|
||||
# so we reduce the number of cached files on non-Linux systems by remove node_modules from cache path.
|
||||
- name: Cache non-full yarn cache on non-Linux
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
if: ${{ inputs.full-cache != 'true' && runner.os != 'Linux' }}
|
||||
with:
|
||||
path: |
|
||||
@@ -113,7 +113,7 @@ runs:
|
||||
key: node_modules-cache-${{ github.job }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
|
||||
|
||||
- name: Cache full yarn cache on Linux
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
if: ${{ inputs.full-cache == 'true' && runner.os == 'Linux' }}
|
||||
with:
|
||||
path: |
|
||||
@@ -122,7 +122,7 @@ runs:
|
||||
key: node_modules-cache-full-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
|
||||
|
||||
- name: Cache full yarn cache on non-Linux
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
if: ${{ inputs.full-cache == 'true' && runner.os != 'Linux' }}
|
||||
with:
|
||||
path: |
|
||||
@@ -154,7 +154,7 @@ runs:
|
||||
# Note: Playwright's cache directory is hard coded because that's what it
|
||||
# says to do in the docs. There doesn't appear to be a command that prints
|
||||
# it out for us.
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: playwright-cache
|
||||
if: ${{ inputs.playwright-install == 'true' }}
|
||||
with:
|
||||
@@ -189,7 +189,7 @@ runs:
|
||||
run: |
|
||||
echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: electron-cache
|
||||
if: ${{ inputs.electron-install == 'true' }}
|
||||
with:
|
||||
|
||||
1
.github/actions/setup-version/action.yml
vendored
1
.github/actions/setup-version/action.yml
vendored
@@ -7,7 +7,6 @@ inputs:
|
||||
ios-app-version:
|
||||
description: 'iOS App Store Version (Optional, use App version if empty)'
|
||||
required: false
|
||||
type: string
|
||||
runs:
|
||||
using: 'composite'
|
||||
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
|
||||
type: application
|
||||
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
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.26.1"
|
||||
appVersion: "0.26.3"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "doc.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "doc.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "doc.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "doc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
@@ -1,63 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "doc.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "doc.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "doc.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "doc.labels" -}}
|
||||
helm.sh/chart: {{ include "doc.chart" . }}
|
||||
{{ include "doc.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
monitoring: enabled
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "doc.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "doc.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "doc.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "doc.fullname" .) .Values.global.docService.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.global.docService.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,118 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "doc.fullname" . }}
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "doc.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "doc.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "doc.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: AFFINE_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.global.secret.secretName }}"
|
||||
key: key
|
||||
- name: NODE_ENV
|
||||
value: "{{ .Values.env }}"
|
||||
- name: NODE_OPTIONS
|
||||
value: "--max-old-space-size=4096"
|
||||
- name: NO_COLOR
|
||||
value: "1"
|
||||
- name: DEPLOYMENT_TYPE
|
||||
value: "{{ .Values.global.deployment.type }}"
|
||||
- name: DEPLOYMENT_PLATFORM
|
||||
value: "{{ .Values.global.deployment.platform }}"
|
||||
- name: SERVER_FLAVOR
|
||||
value: "doc"
|
||||
- name: AFFINE_ENV
|
||||
value: "{{ .Release.Namespace }}"
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pg-postgresql
|
||||
key: postgres-password
|
||||
- name: DATABASE_URL
|
||||
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
|
||||
- name: REDIS_SERVER_ENABLED
|
||||
value: "true"
|
||||
- name: REDIS_SERVER_HOST
|
||||
value: "{{ .Values.global.redis.host }}"
|
||||
- name: REDIS_SERVER_PORT
|
||||
value: "{{ .Values.global.redis.port }}"
|
||||
- name: REDIS_SERVER_USER
|
||||
value: "{{ .Values.global.redis.username }}"
|
||||
- name: REDIS_SERVER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_DATABASE
|
||||
value: "{{ .Values.global.redis.database }}"
|
||||
- name: AFFINE_INDEXER_SEARCH_PROVIDER
|
||||
value: "{{ .Values.global.indexer.provider }}"
|
||||
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
|
||||
value: "{{ .Values.global.indexer.endpoint }}"
|
||||
- name: AFFINE_INDEXER_SEARCH_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: indexer
|
||||
key: indexer-apiKey
|
||||
- name: AFFINE_SERVER_PORT
|
||||
value: "{{ .Values.global.docService.port }}"
|
||||
- name: AFFINE_SERVER_SUB_PATH
|
||||
value: "{{ .Values.app.path }}"
|
||||
- name: AFFINE_SERVER_HOST
|
||||
value: "{{ .Values.app.host }}"
|
||||
- name: AFFINE_SERVER_HTTPS
|
||||
value: "{{ .Values.app.https }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.global.docService.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "doc.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "doc.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "doc.fullname" . }}:{{ .Values.global.docService.port }}']
|
||||
restartPolicy: Never
|
||||
5
.github/helm/affine/charts/doc/values.yaml
vendored
5
.github/helm/affine/charts/doc/values.yaml
vendored
@@ -30,9 +30,12 @@ podSecurityContext:
|
||||
fsGroup: 2000
|
||||
|
||||
resources:
|
||||
requests:
|
||||
limits:
|
||||
cpu: '1'
|
||||
memory: 4Gi
|
||||
requests:
|
||||
cpu: '1'
|
||||
memory: 2Gi
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
|
||||
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
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.26.1"
|
||||
appVersion: "0.26.3"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -88,8 +88,6 @@ spec:
|
||||
value: "{{ .Values.app.host }}"
|
||||
- name: AFFINE_SERVER_HTTPS
|
||||
value: "{{ .Values.app.https }}"
|
||||
- name: DOC_SERVICE_ENDPOINT
|
||||
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.app.port }}
|
||||
@@ -98,12 +96,20 @@ spec:
|
||||
httpGet:
|
||||
path: /info
|
||||
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:
|
||||
httpGet:
|
||||
path: /info
|
||||
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:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "doc.fullname" . }}
|
||||
name: {{ .Values.global.docService.name }}
|
||||
labels:
|
||||
{{- include "doc.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
{{- include "front.labels" . | nindent 4 }}
|
||||
{{- with .Values.services.doc.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
type: {{ .Values.services.doc.type }}
|
||||
ports:
|
||||
- port: {{ .Values.global.docService.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "doc.selectorLabels" . | nindent 4 }}
|
||||
{{- include "front.selectorLabels" . | nindent 4 }}
|
||||
16
.github/helm/affine/charts/front/values.yaml
vendored
16
.github/helm/affine/charts/front/values.yaml
vendored
@@ -29,12 +29,23 @@ podSecurityContext:
|
||||
fsGroup: 2000
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: '1'
|
||||
memory: 6Gi
|
||||
requests:
|
||||
cpu: '1'
|
||||
memory: 2Gi
|
||||
memory: 4Gi
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
timeoutSeconds: 5
|
||||
periodSeconds: 10
|
||||
failureThreshold: 6
|
||||
successThreshold: 1
|
||||
liveness:
|
||||
initialDelaySeconds: 60
|
||||
failureThreshold: 12
|
||||
readiness: {}
|
||||
|
||||
services:
|
||||
sync:
|
||||
@@ -54,6 +65,9 @@ services:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
annotations: {}
|
||||
doc:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
||||
@@ -3,7 +3,7 @@ name: graphql
|
||||
description: AFFiNE GraphQL server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.26.1"
|
||||
appVersion: "0.26.3"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -27,8 +27,11 @@ podSecurityContext:
|
||||
fsGroup: 2000
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: '1'
|
||||
memory: 4Gi
|
||||
requests:
|
||||
cpu: '2'
|
||||
cpu: '1'
|
||||
memory: 2Gi
|
||||
|
||||
probe:
|
||||
|
||||
10
.github/helm/affine/values.yaml
vendored
10
.github/helm/affine/values.yaml
vendored
@@ -47,12 +47,6 @@ graphql:
|
||||
annotations:
|
||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||
|
||||
doc:
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations:
|
||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||
|
||||
front:
|
||||
services:
|
||||
sync:
|
||||
@@ -71,3 +65,7 @@ front:
|
||||
name: affine-web
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
doc:
|
||||
type: ClusterIP
|
||||
annotations:
|
||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||
|
||||
10
.github/workflows/auto-labeler.yml
vendored
10
.github/workflows/auto-labeler.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
@@ -9,5 +13,5 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/labeler@v6
|
||||
|
||||
16
.github/workflows/build-images.yml
vendored
16
.github/workflows/build-images.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.build-type }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.build-type }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.build-type }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
build-server-native:
|
||||
name: Build Server native - ${{ matrix.targets.name }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
environment: ${{ inputs.build-type }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
file: server-native.armv7.node
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
needs:
|
||||
- build-server-native
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
- build-mobile
|
||||
- build-admin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download server dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
# setup node without cache configuration
|
||||
# Prisma cache is not compatible with docker build cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: https://npm.pkg.github.com
|
||||
|
||||
424
.github/workflows/build-test.yml
vendored
424
.github/workflows/build-test.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
@@ -67,10 +67,27 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Go (for actionlint)
|
||||
uses: actions/setup-go@v6
|
||||
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
|
||||
# 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
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -94,7 +111,7 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -108,20 +125,45 @@ jobs:
|
||||
run: |
|
||||
yarn affine bs-docs build
|
||||
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"
|
||||
exit 1
|
||||
} || {
|
||||
else
|
||||
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@v6
|
||||
|
||||
- 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:
|
||||
name: Lint Rust
|
||||
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- rust-test-filter
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
package: 'affine'
|
||||
no-build: 'true'
|
||||
- name: fmt check
|
||||
run: |
|
||||
@@ -140,7 +182,7 @@ jobs:
|
||||
needs:
|
||||
- build-server-native
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -159,21 +201,23 @@ jobs:
|
||||
yarn affine i18n build
|
||||
yarn affine server genconfig
|
||||
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"
|
||||
exit 1
|
||||
} || {
|
||||
else
|
||||
echo "All changes are submitted"
|
||||
}
|
||||
fi
|
||||
|
||||
check-yarn-binary:
|
||||
name: Check yarn binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Run check
|
||||
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
|
||||
|
||||
e2e-blocksuite-test:
|
||||
@@ -182,12 +226,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine-test/blocksuite @blocksuite/playground @blocksuite/integration-test
|
||||
playwright-install: true
|
||||
playwright-platform: 'chromium'
|
||||
electron-install: false
|
||||
@@ -210,18 +255,14 @@ jobs:
|
||||
e2e-blocksuite-cross-browser-test:
|
||||
name: E2E BlockSuite Cross Browser Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1]
|
||||
browser: ['chromium', 'firefox', 'webkit']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine-test/blocksuite @blocksuite/playground @blocksuite/integration-test
|
||||
playwright-install: true
|
||||
playwright-platform: ${{ matrix.browser }}
|
||||
playwright-platform: 'chromium,firefox,webkit'
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
@@ -229,15 +270,15 @@ jobs:
|
||||
run: yarn workspace @blocksuite/playground build
|
||||
|
||||
- name: Run playwright tests
|
||||
env:
|
||||
BROWSER: ${{ matrix.browser }}
|
||||
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
run: |
|
||||
yarn workspace @blocksuite/integration-test test:unit
|
||||
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-e2e-bs-cross-browser-${{ matrix.browser }}-${{ matrix.shard }}
|
||||
name: test-results-e2e-bs-cross-browser
|
||||
path: ./test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
@@ -253,10 +294,11 @@ jobs:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-local @affine/web @affine/server
|
||||
playwright-install: true
|
||||
playwright-platform: 'chromium'
|
||||
electron-install: false
|
||||
@@ -284,10 +326,11 @@ jobs:
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-mobile @affine/mobile
|
||||
playwright-install: true
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
@@ -307,20 +350,21 @@ jobs:
|
||||
name: Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-native
|
||||
- build-native-linux
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3]
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: true
|
||||
playwright-install: true
|
||||
playwright-platform: 'chromium,firefox,webkit'
|
||||
full-cache: true
|
||||
|
||||
- name: Download affine.linux-x64-gnu.node
|
||||
@@ -341,21 +385,13 @@ jobs:
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
build-native:
|
||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
build-native-linux:
|
||||
name: Build AFFiNE native (x86_64-unknown-linux-gnu)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
spec:
|
||||
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||
- { os: macos-latest, target: x86_64-apple-darwin }
|
||||
- { os: macos-latest, target: aarch64-apple-darwin }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -366,7 +402,46 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}
|
||||
shell: bash
|
||||
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('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 }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
spec:
|
||||
- { os: macos-latest, target: x86_64-apple-darwin }
|
||||
- { os: macos-latest, target: aarch64-apple-darwin }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- 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('${{ matrix.spec.target }}').platformArchABI)")"
|
||||
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
@@ -383,7 +458,7 @@ jobs:
|
||||
|
||||
# Split Windows build because it's too slow
|
||||
# and other ci jobs required linux native
|
||||
build-windows-native:
|
||||
build-native-windows:
|
||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
env:
|
||||
@@ -396,7 +471,7 @@ jobs:
|
||||
- { os: windows-latest, target: aarch64-pc-windows-msvc }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: samypr100/setup-dev-drive@v3
|
||||
with:
|
||||
workspace-copy: true
|
||||
@@ -415,7 +490,7 @@ jobs:
|
||||
working-directory: ${{ env.DEV_DRIVE_WORKSPACE }}
|
||||
shell: bash
|
||||
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"
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
@@ -436,7 +511,7 @@ jobs:
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -459,10 +534,11 @@ jobs:
|
||||
name: Build @affine/electron renderer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine/electron-renderer @affine/nbstore @toeverything/infra
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
- name: Build Electron renderer
|
||||
@@ -483,9 +559,9 @@ jobs:
|
||||
name: Native Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-native
|
||||
- build-native-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -539,11 +615,12 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
@@ -577,8 +654,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-native
|
||||
strategy:
|
||||
fail-fast: false
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
@@ -621,11 +696,12 @@ jobs:
|
||||
stack-version: 9.0.1
|
||||
security-enabled: false
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
@@ -642,8 +718,6 @@ jobs:
|
||||
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
CI_NODE_INDEX: ${{ matrix.node_index }}
|
||||
CI_NODE_TOTAL: ${{ matrix.total_nodes }}
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v5
|
||||
@@ -685,11 +759,12 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
@@ -716,13 +791,16 @@ jobs:
|
||||
|
||||
miri:
|
||||
name: miri code check
|
||||
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- rust-test-filter
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_TERM_COLOR: always
|
||||
MIRIFLAGS: -Zmiri-backtrace=full -Zmiri-tree-borrows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -741,13 +819,16 @@ jobs:
|
||||
|
||||
loom:
|
||||
name: loom thread test
|
||||
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- rust-test-filter
|
||||
env:
|
||||
RUSTFLAGS: --cfg loom
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -764,11 +845,14 @@ jobs:
|
||||
|
||||
fuzzing:
|
||||
name: fuzzing
|
||||
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- rust-test-filter
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -800,14 +884,18 @@ jobs:
|
||||
|
||||
rust-test:
|
||||
name: Run native tests
|
||||
if: ${{ needs.rust-test-filter.outputs.run-rust == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- rust-test-filter
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
package: 'affine'
|
||||
no-build: 'true'
|
||||
|
||||
@@ -819,11 +907,51 @@ jobs:
|
||||
- name: Run tests
|
||||
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@v6
|
||||
|
||||
- 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:
|
||||
name: Server Copilot Api Test
|
||||
if: ${{ needs.copilot-test-filter.outputs.run-api == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-native
|
||||
- copilot-test-filter
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DISTRIBUTION: web
|
||||
@@ -855,55 +983,32 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- 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.*'
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine/server
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
- name: Download server-native.node
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/native
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
env:
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- 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
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
|
||||
- name: Upload server test coverage results
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -914,6 +1019,7 @@ jobs:
|
||||
|
||||
copilot-e2e-test:
|
||||
name: Frontend Copilot E2E Test
|
||||
if: ${{ needs.copilot-test-filter.outputs.run-e2e == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
@@ -928,6 +1034,7 @@ jobs:
|
||||
shardTotal: [5]
|
||||
needs:
|
||||
- build-server-native
|
||||
- copilot-test-filter
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -949,54 +1056,29 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- 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/**'
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/monorepo @affine-test/affine-cloud-copilot @affine/web @affine/server
|
||||
playwright-install: true
|
||||
playwright-platform: 'chromium'
|
||||
electron-install: false
|
||||
hard-link-nm: false
|
||||
|
||||
- name: Download server-native.node
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/native
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
env:
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- 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
|
||||
with:
|
||||
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
@@ -1006,7 +1088,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-native
|
||||
- build-native
|
||||
- build-native-linux
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
@@ -1057,12 +1139,15 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
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-platform: 'chromium'
|
||||
electron-install: ${{ matrix.tests.shard == 'desktop' && 'true' || 'false' }}
|
||||
hard-link-nm: false
|
||||
|
||||
- name: Download server-native.node
|
||||
@@ -1099,7 +1184,9 @@ jobs:
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs:
|
||||
- build-electron-renderer
|
||||
- build-native
|
||||
- build-native-linux
|
||||
- build-native-macos
|
||||
- build-native-windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -1133,13 +1220,14 @@ jobs:
|
||||
test: true,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- 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
|
||||
playwright-install: ${{ matrix.spec.test && 'true' || 'false' }}
|
||||
playwright-platform: 'chromium'
|
||||
hard-link-nm: false
|
||||
enableScripts: false
|
||||
|
||||
@@ -1147,7 +1235,7 @@ jobs:
|
||||
id: filename
|
||||
shell: bash
|
||||
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"
|
||||
|
||||
- name: Download ${{ steps.filename.outputs.filename }}
|
||||
@@ -1182,84 +1270,6 @@ jobs:
|
||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||
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)
|
||||
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
|
||||
env:
|
||||
@@ -1299,6 +1309,14 @@ jobs:
|
||||
run: |
|
||||
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:
|
||||
needs:
|
||||
- analyze
|
||||
@@ -1312,8 +1330,9 @@ jobs:
|
||||
- e2e-blocksuite-cross-browser-test
|
||||
- e2e-mobile-test
|
||||
- unit-test
|
||||
- build-native
|
||||
- build-windows-native
|
||||
- build-native-linux
|
||||
- build-native-macos
|
||||
- build-native-windows
|
||||
- build-server-native
|
||||
- build-electron-renderer
|
||||
- native-unit-test
|
||||
@@ -1323,10 +1342,11 @@ jobs:
|
||||
- server-test
|
||||
- server-e2e-test
|
||||
- rust-test
|
||||
- rust-test-filter
|
||||
- copilot-test-filter
|
||||
- copilot-api-test
|
||||
- copilot-e2e-test
|
||||
- desktop-test
|
||||
- desktop-bundle-check
|
||||
- cloud-e2e-test
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
8
.github/workflows/copilot-test.yml
vendored
8
.github/workflows/copilot-test.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Post test result message
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
|
||||
5
.github/workflows/pr-title-lint.yml
vendored
5
.github/workflows/pr-title-lint.yml
vendored
@@ -16,10 +16,11 @@ jobs:
|
||||
check-pull-request-title:
|
||||
name: Check pull request title
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
2
.github/workflows/release-cloud.yml
vendored
2
.github/workflows/release-cloud.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- build-images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Deploy to ${{ inputs.build-type }}
|
||||
uses: ./.github/actions/deploy
|
||||
with:
|
||||
|
||||
10
.github/workflows/release-desktop-platform.yml
vendored
10
.github/workflows/release-desktop-platform.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ inputs.app_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Signing By Apple Developer ID
|
||||
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
@@ -174,18 +174,18 @@ jobs:
|
||||
run: |
|
||||
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/*.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/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
- uses: actions/attest-build-provenance@v4
|
||||
if: ${{ inputs.platform == 'darwin' }}
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
- uses: actions/attest-build-provenance@v4
|
||||
if: ${{ inputs.platform == 'linux' }}
|
||||
with:
|
||||
subject-path: |
|
||||
|
||||
88
.github/workflows/release-desktop.yml
vendored
88
.github/workflows/release-desktop.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.build-type }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -201,13 +201,44 @@ jobs:
|
||||
nmHoistingLimits: workspaces
|
||||
env:
|
||||
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
|
||||
with:
|
||||
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
path: .
|
||||
- name: unzip file
|
||||
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out
|
||||
path: signed-packaged-diff
|
||||
- name: Apply signed packaged file diff
|
||||
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
|
||||
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||
@@ -267,13 +298,44 @@ jobs:
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
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
|
||||
with:
|
||||
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
path: .
|
||||
- name: unzip file
|
||||
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
|
||||
path: signed-installer-diff
|
||||
- name: Apply signed installer file diff
|
||||
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
|
||||
run: |
|
||||
@@ -282,7 +344,7 @@ jobs:
|
||||
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
|
||||
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
- uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
|
||||
@@ -307,7 +369,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download Artifacts (macos-x64)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -333,7 +395,7 @@ jobs:
|
||||
with:
|
||||
name: affine-linux-x64-builds
|
||||
path: ./release
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Copy Selfhost Release Files
|
||||
|
||||
24
.github/workflows/release-mobile.yml
vendored
24
.github/workflows/release-mobile.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.build-type }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
build-android-web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
needs:
|
||||
- build-ios-web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
- name: Cap sync
|
||||
run: yarn workspace @affine/ios sync
|
||||
- name: Signing By Apple Developer ID
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
id: import-codesign-certs
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12_MOBILE }}
|
||||
@@ -128,9 +128,9 @@ jobs:
|
||||
- name: Testflight
|
||||
working-directory: packages/frontend/apps/ios/App
|
||||
run: |
|
||||
echo -n "${{ env.BUILD_PROVISION_PROFILE }}" | base64 --decode -o $PP_PATH
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
printf '%s' "$BUILD_PROVISION_PROFILE" | base64 --decode -o "$PP_PATH"
|
||||
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
cp "$PP_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
fastlane beta
|
||||
env:
|
||||
BUILD_TARGET: distribution
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
needs:
|
||||
- build-android-web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
@@ -160,7 +160,9 @@ jobs:
|
||||
- name: Load Google Service file
|
||||
env:
|
||||
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
|
||||
uses: ./.github/actions/setup-node
|
||||
timeout-minutes: 10
|
||||
@@ -178,7 +180,7 @@ jobs:
|
||||
no-build: 'true'
|
||||
- name: Cap sync
|
||||
run: yarn workspace @affine/android cap sync
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- name: Auth gcloud
|
||||
@@ -190,7 +192,7 @@ jobs:
|
||||
token_format: 'access_token'
|
||||
project_id: '${{ secrets.GCP_PROJECT_ID }}'
|
||||
access_token_scopes: 'https://www.googleapis.com/auth/androidpublisher'
|
||||
- uses: actions/setup-java@v4
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
GIT_SHORT_HASH: ${{ steps.prepare.outputs.GIT_SHORT_HASH }}
|
||||
BUILD_TYPE: ${{ steps.prepare.outputs.BUILD_TYPE }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Prepare Release
|
||||
id: prepare
|
||||
uses: ./.github/actions/prepare-release
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
steps:
|
||||
- name: Decide whether to release
|
||||
id: decide
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const buildType = '${{ needs.prepare.outputs.BUILD_TYPE }}'
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
name: Wait for approval
|
||||
with:
|
||||
secret: ${{ secrets.GITHUB_TOKEN }}
|
||||
approvers: darkskygit,pengx17,L-Sun,EYHN
|
||||
approvers: darkskygit
|
||||
minimum-approvals: 1
|
||||
fail-on-denial: true
|
||||
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: |
|
||||
cd ${{ env.ARCHIVE_DIR }}/out
|
||||
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||
- name: zip file
|
||||
shell: cmd
|
||||
- name: collect signed file diff
|
||||
shell: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {0}
|
||||
run: |
|
||||
cd ${{ env.ARCHIVE_DIR }}
|
||||
7za a signed.zip .\out\*
|
||||
$OutDir = Join-Path '${{ env.ARCHIVE_DIR }}' '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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signed-${{ inputs.artifact-name }}
|
||||
path: ${{ env.ARCHIVE_DIR }}/signed.zip
|
||||
path: ${{ env.ARCHIVE_DIR }}/signed-diff
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -48,6 +48,7 @@ testem.log
|
||||
/typings
|
||||
tsconfig.tsbuildinfo
|
||||
.context
|
||||
/*.md
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"correctness": "error",
|
||||
"perf": "error"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"es2026": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/node_modules",
|
||||
".yarn",
|
||||
@@ -44,6 +48,34 @@
|
||||
"**/test-blocks.json"
|
||||
],
|
||||
"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-redeclare": "allow",
|
||||
"promise/no-callback-in-promise": "allow",
|
||||
@@ -70,6 +102,14 @@
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-unused-vars": "error",
|
||||
"no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowShortCircuit": true,
|
||||
"allowTernary": true,
|
||||
"allowTaggedTemplates": true
|
||||
}
|
||||
],
|
||||
"no-ex-assign": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-fallthrough": "error",
|
||||
@@ -126,6 +166,7 @@
|
||||
"react/no-render-return-value": "error",
|
||||
"react/jsx-no-target-blank": "error",
|
||||
"react/jsx-no-comment-textnodes": "error",
|
||||
"react/no-array-index-key": "off",
|
||||
"typescript/consistent-type-imports": "error",
|
||||
"typescript/no-non-null-assertion": "error",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
@@ -241,6 +282,42 @@
|
||||
"typescript/consistent-type-imports": "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.patterns": {
|
||||
"*.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",
|
||||
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*",
|
||||
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
|
||||
|
||||
338
Cargo.lock
generated
338
Cargo.lock
generated
@@ -111,10 +111,12 @@ dependencies = [
|
||||
"base64-simd",
|
||||
"chrono",
|
||||
"homedir",
|
||||
"lru",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"sqlx",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
@@ -176,9 +178,15 @@ name = "affine_server_native"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"affine_common",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"file-format",
|
||||
"image",
|
||||
"infer",
|
||||
"libwebp-sys",
|
||||
"little_exif",
|
||||
"llm_adapter",
|
||||
"matroska",
|
||||
"mimalloc",
|
||||
"mp4parse",
|
||||
"napi",
|
||||
@@ -186,6 +194,8 @@ dependencies = [
|
||||
"napi-derive",
|
||||
"rand 0.9.2",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha3",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
@@ -230,6 +240,21 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
@@ -243,7 +268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
@@ -531,7 +556,7 @@ version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
@@ -581,9 +606,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -597,6 +622,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680575de65ce8b916b82a447458b94a48776707d9c2681a9d8da351c06886a1f"
|
||||
dependencies = [
|
||||
"core2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -639,6 +673,27 @@ version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
@@ -674,6 +729,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -902,6 +963,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -981,7 +1048,7 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
@@ -994,7 +1061,7 @@ version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
@@ -1007,7 +1074,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"libc",
|
||||
]
|
||||
@@ -1377,7 +1444,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -1560,6 +1627,15 @@ dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-format"
|
||||
version = "0.28.0"
|
||||
@@ -1831,6 +1907,16 @@ dependencies = [
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -1878,7 +1964,7 @@ version = "1.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -2001,6 +2087,22 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
@@ -2173,6 +2275,34 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate"
|
||||
version = "0.3.1"
|
||||
@@ -2374,9 +2504,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
@@ -2488,7 +2618,7 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"redox_syscall 0.7.0",
|
||||
]
|
||||
@@ -2504,6 +2634,17 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libwebp-sys"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "375ca3fbd6d89769361c5d505c9da676eb4128ee471b9fd763144d377a2d30e6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"glob",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
@@ -2516,6 +2657,33 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "little_exif"
|
||||
version = "0.6.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21eeb58b22d31be8dc5c625004fcd4b9b385cd3c05df575f523bcca382c51122"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"crc",
|
||||
"log",
|
||||
"miniz_oxide",
|
||||
"paste",
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llm_adapter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e98485dda5180cc89b993a001688bed93307be6bd8fedcde445b69bbca4f554d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -2553,7 +2721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59fa2559e99ba0f26a12458aabc754432c805bbb8cba516c427825a997af1fb7"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"cbc",
|
||||
"ecb",
|
||||
"encoding_rs",
|
||||
@@ -2572,6 +2740,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -2631,6 +2808,16 @@ dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matroska"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde85cd7fb5cf875c4a46fac0cbd6567d413bea2538cef6788e3a0e52a902b45"
|
||||
dependencies = [
|
||||
"bitstream-io",
|
||||
"phf 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@@ -2700,6 +2887,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mp4parse"
|
||||
version = "0.17.0"
|
||||
@@ -2730,7 +2927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "000f205daae6646003fdc38517be6232af2b150bad4b67bdaf4c5aadb119d738"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"chrono",
|
||||
"ctor",
|
||||
"futures",
|
||||
@@ -2790,7 +2987,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
@@ -2825,7 +3022,7 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2989,7 +3186,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
@@ -3006,7 +3203,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -3112,6 +3309,12 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "path-ext"
|
||||
version = "0.1.2"
|
||||
@@ -3358,6 +3561,19 @@ dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pom"
|
||||
version = "1.1.0"
|
||||
@@ -3458,7 +3674,7 @@ checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
|
||||
dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"bit-vec 0.8.0",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"num-traits",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
@@ -3486,7 +3702,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"pulldown-cmark-escape",
|
||||
@@ -3499,12 +3715,33 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.43"
|
||||
@@ -3652,7 +3889,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3661,7 +3898,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3820,7 +4057,7 @@ version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -3833,6 +4070,7 @@ version = "0.23.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -3874,7 +4112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
@@ -4258,7 +4496,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -4301,7 +4539,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -4901,9 +5139,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.9+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
@@ -5334,6 +5572,35 @@ version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"flate2",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
"webpki-roots 1.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -6299,3 +6566,18 @@ dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -40,12 +40,24 @@ resolver = "3"
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.28", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
image = { version = "0.25.9", default-features = false, features = [
|
||||
"bmp",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
] }
|
||||
infer = { version = "0.19.0" }
|
||||
lasso = { version = "0.7", features = ["multi-threaded"] }
|
||||
lib0 = { version = "0.16", features = ["lib0-serde"] }
|
||||
libc = "0.2"
|
||||
libwebp-sys = "0.14.2"
|
||||
little_exif = "0.6.23"
|
||||
llm_adapter = { version = "0.1.3", default-features = false }
|
||||
log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
lru = "0.16"
|
||||
matroska = "0.30"
|
||||
memory-indexer = "0.3.0"
|
||||
mimalloc = "0.1"
|
||||
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
|
||||
|
||||
| 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) |
|
||||
| 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 |
|
||||
| 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'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 |
|
||||
|
||||
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 **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 [AFFiNE Community](https://community.affine.pro) 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 [Discord](https://affine.pro/redirect/discord) where you can engage with other like-minded individuals.
|
||||
|
||||
## 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)
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
AFFiNE repo cloned, built, and ready to go.
|
||||
AFFiNE repo cloned, built, and ready to go).
|
||||
|
||||
### Local
|
||||
|
||||
|
||||
@@ -23,4 +23,6 @@ We welcome you to provide us with bug reports via and email at [security@toevery
|
||||
|
||||
Since we are an open source project, we also welcome you to provide corresponding fix PRs, we will determine specific rewards based on the evaluation results.
|
||||
|
||||
Due to limited resources, we do not accept and will not review any AI-generated security reports.
|
||||
|
||||
If the vulnerability is caused by a library we depend on, we encourage you to submit a security report to the corresponding dependent library at the same time to benefit more users.
|
||||
|
||||
@@ -296,10 +296,10 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1",
|
||||
"version": "0.26.3",
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"msw": "^2.12.4",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2101,6 +2101,157 @@ describe('html to snapshot', () => {
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('paragraph with br should split into multiple blocks', async () => {
|
||||
const html = template(`<p>aaa<br>bbb<br>ccc</p>`);
|
||||
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[0]',
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DefaultTheme.noteBackgrounColor,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[1]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [{ insert: 'aaa' }],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[2]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [{ insert: 'bbb' }],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[3]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [{ insert: 'ccc' }],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const htmlAdapter = new HtmlAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
|
||||
file: html,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('paragraph with br should keep inline styles in each split line', async () => {
|
||||
const html = template(
|
||||
`<p><strong>aaa</strong><br><a href="https://www.google.com/">bbb</a><br><em>ccc</em></p>`
|
||||
);
|
||||
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[0]',
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DefaultTheme.noteBackgrounColor,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[1]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[2]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
link: 'https://www.google.com/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[3]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'ccc',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const htmlAdapter = new HtmlAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
|
||||
file: html,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('nested list', async () => {
|
||||
const html = template(`<ul><li>111<ul><li>222</li></ul></li></ul>`);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||
testTimeout: 1000,
|
||||
coverage: {
|
||||
provider: 'istanbul', // or 'c8'
|
||||
provider: 'istanbul',
|
||||
reporter: ['lcov'],
|
||||
reportsDirectory: '../../../.coverage/blocksuite-affine',
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -41,5 +40,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -108,7 +108,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
}
|
||||
|
||||
open = () => {
|
||||
window.open(this.link, '_blank');
|
||||
const link = this.link;
|
||||
if (!link) return;
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -8,10 +9,9 @@ export default defineConfig({
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
name: 'chromium',
|
||||
provider: 'playwright',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
provider: playwright(),
|
||||
isolate: false,
|
||||
providerOptions: {},
|
||||
},
|
||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||
testTimeout: 500,
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -45,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -216,9 +216,13 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
override renderBlock() {
|
||||
const icon = this.model.props.icon$.value;
|
||||
const backgroundColorName = this.model.props.backgroundColorName$.value;
|
||||
const normalizedBackgroundName =
|
||||
backgroundColorName === 'default' || backgroundColorName === ''
|
||||
? 'grey'
|
||||
: backgroundColorName;
|
||||
const backgroundColor = (
|
||||
cssVarV2.block.callout.background as Record<string, string>
|
||||
)[backgroundColorName ?? ''];
|
||||
)[normalizedBackgroundName ?? 'grey'];
|
||||
|
||||
const iconContent = getIcon(icon);
|
||||
|
||||
|
||||
@@ -68,14 +68,14 @@ const backgroundColorAction = {
|
||||
${repeat(colors, color => {
|
||||
const isDefault = color === 'default';
|
||||
const value = isDefault
|
||||
? null
|
||||
? cssVarV2.block.callout.background.grey
|
||||
: `var(--affine-text-highlight-${color})`;
|
||||
const displayName = `${color} Background`;
|
||||
|
||||
return html`
|
||||
<editor-menu-action
|
||||
data-testid="background-${color}"
|
||||
@click=${() => updateBackground(color)}
|
||||
@click=${() => updateBackground(isDefault ? 'grey' : color)}
|
||||
>
|
||||
<affine-text-duotone-icon
|
||||
style=${styleMap({
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"shiki": "^3.19.0",
|
||||
"zod": "^3.25.76"
|
||||
@@ -48,5 +47,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ export class AffineCodeUnit extends ShadowlessElement {
|
||||
if (!codeBlock || !vElement) return plainContent;
|
||||
const tokens = codeBlock.highlightTokens$.value;
|
||||
if (tokens.length === 0) return plainContent;
|
||||
const line = tokens[vElement.lineIndex];
|
||||
if (!line) return plainContent;
|
||||
// copy the tokens to avoid modifying the original tokens
|
||||
const lineTokens = structuredClone(tokens[vElement.lineIndex]);
|
||||
const lineTokens = structuredClone(line);
|
||||
if (lineTokens.length === 0) return plainContent;
|
||||
|
||||
const startOffset = vElement.startOffset;
|
||||
|
||||
@@ -27,6 +27,16 @@ export const codeBlockStyles = css`
|
||||
|
||||
${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 {
|
||||
font-family: var(--affine-font-code-family);
|
||||
font-variant-ligatures: none;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -42,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
@@ -48,5 +47,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -39,5 +38,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -43,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -30,13 +30,12 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -49,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -30,13 +30,12 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -49,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
@@ -44,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -44,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ import {
|
||||
|
||||
@Peekable()
|
||||
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`
|
||||
affine-edgeless-image {
|
||||
position: relative;
|
||||
@@ -63,6 +68,11 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
affine-edgeless-image .resizable-img {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
resourceController = new ResourceController(
|
||||
@@ -70,6 +80,12 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
'Image'
|
||||
);
|
||||
|
||||
private _lodThumbnailUrl: string | null = null;
|
||||
private _lodSourceUrl: string | null = null;
|
||||
private _lodGeneratingSourceUrl: string | null = null;
|
||||
private _lodGenerationToken = 0;
|
||||
private _lastShouldUseLod = false;
|
||||
|
||||
get blobUrl() {
|
||||
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() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -108,14 +252,32 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
|
||||
this.disposables.add(
|
||||
this.model.props.sourceId$.subscribe(() => {
|
||||
this._resetLodSource(null);
|
||||
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() {
|
||||
const blobUrl = this.blobUrl;
|
||||
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
|
||||
this._resetLodSource(blobUrl);
|
||||
|
||||
const containerStyleMap = styleMap({
|
||||
display: 'flex',
|
||||
@@ -138,6 +300,13 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
});
|
||||
|
||||
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`
|
||||
<div class="affine-image-container" style=${containerStyleMap}>
|
||||
@@ -149,7 +318,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
class="drag-target"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
src=${blobUrl}
|
||||
src=${imageUrl ?? ''}
|
||||
alt=${caption}
|
||||
@error=${this._handleError}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"katex": "^0.16.27",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
@@ -46,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -27,12 +27,11 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -46,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -49,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -221,6 +221,12 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
}
|
||||
}
|
||||
|
||||
override getCSSScaleVal(): number {
|
||||
const baseScale = super.getCSSScaleVal();
|
||||
const extraScale = this.model.props.edgeless?.scale ?? 1;
|
||||
return baseScale * extraScale;
|
||||
}
|
||||
|
||||
override getRenderingRect() {
|
||||
const { xywh, edgeless } = this.model.props;
|
||||
const { collapse, scale = 1 } = edgeless;
|
||||
@@ -255,7 +261,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
|
||||
const style = {
|
||||
borderRadius: borderRadius + 'px',
|
||||
transform: `scale(${scale})`,
|
||||
};
|
||||
|
||||
const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
@@ -454,6 +459,28 @@ export const EdgelessNoteInteraction =
|
||||
return;
|
||||
}
|
||||
|
||||
let isClickOnTitle = false;
|
||||
const titleRect = view
|
||||
.querySelector('edgeless-page-block-title')
|
||||
?.getBoundingClientRect();
|
||||
|
||||
if (titleRect) {
|
||||
const titleBound = new Bound(
|
||||
titleRect.x,
|
||||
titleRect.y,
|
||||
titleRect.width,
|
||||
titleRect.height
|
||||
);
|
||||
if (titleBound.isPointInBound([e.clientX, e.clientY])) {
|
||||
isClickOnTitle = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isClickOnTitle) {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.children.length === 0) {
|
||||
const blockId = std.store.addBlock(
|
||||
'affine:paragraph',
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -42,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,126 @@ const tagsInAncestor = (o: NodeProps<HtmlAST>, tagNames: Array<string>) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const splitDeltaByNewline = (delta: DeltaInsert[]) => {
|
||||
const lines: DeltaInsert[][] = [[]];
|
||||
const pending = [...delta];
|
||||
|
||||
while (pending.length > 0) {
|
||||
const op = pending.shift();
|
||||
if (!op) continue;
|
||||
|
||||
const insert = op.insert;
|
||||
if (typeof insert !== 'string') {
|
||||
lines[lines.length - 1].push(op);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insert.includes('\n')) {
|
||||
if (insert.length === 0) {
|
||||
continue;
|
||||
}
|
||||
lines[lines.length - 1].push(op);
|
||||
continue;
|
||||
}
|
||||
|
||||
const splitIndex = insert.indexOf('\n');
|
||||
const linePart = insert.slice(0, splitIndex);
|
||||
const remainPart = insert.slice(splitIndex + 1);
|
||||
if (linePart.length > 0) {
|
||||
lines[lines.length - 1].push({ ...op, insert: linePart });
|
||||
}
|
||||
lines.push([]);
|
||||
if (remainPart) {
|
||||
pending.unshift({ ...op, insert: remainPart });
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
const hasBlockElementDescendant = (node: HtmlAST): boolean => {
|
||||
if (!HastUtils.isElement(node)) {
|
||||
return false;
|
||||
}
|
||||
return node.children.some(child => {
|
||||
if (!HastUtils.isElement(child)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(HastUtils.isTagBlock(child.tagName) && child.tagName !== 'br') ||
|
||||
hasBlockElementDescendant(child)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getParagraphDeltas = (
|
||||
node: HtmlAST,
|
||||
delta: DeltaInsert[]
|
||||
): DeltaInsert[][] => {
|
||||
if (!HastUtils.isElement(node)) return [delta];
|
||||
if (hasBlockElementDescendant(node)) return [delta];
|
||||
|
||||
const hasBr = !!HastUtils.querySelector(node, 'br');
|
||||
if (!hasBr) return [delta];
|
||||
|
||||
const hasNewline = delta.some(
|
||||
op => typeof op.insert === 'string' && op.insert.includes('\n')
|
||||
);
|
||||
if (!hasNewline) return [delta];
|
||||
|
||||
return splitDeltaByNewline(delta);
|
||||
};
|
||||
|
||||
const openParagraphBlocks = (
|
||||
deltas: DeltaInsert[][],
|
||||
type: string,
|
||||
// AST walker context from html adapter transform pipeline.
|
||||
walkerContext: any
|
||||
) => {
|
||||
for (const delta of deltas) {
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: { type, text: { '$blocksuite:internal:text$': true, delta } },
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
}
|
||||
};
|
||||
|
||||
const MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY =
|
||||
'affine:paragraph:multi-emitted-nodes';
|
||||
|
||||
const markMultiParagraphEmitted = (walkerContext: any, node: HtmlAST) => {
|
||||
const emittedNodes =
|
||||
(walkerContext.getGlobalContext(
|
||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
|
||||
) as WeakSet<object> | undefined) ?? new WeakSet<object>();
|
||||
emittedNodes.add(node as object);
|
||||
walkerContext.setGlobalContext(
|
||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY,
|
||||
emittedNodes
|
||||
);
|
||||
};
|
||||
|
||||
const consumeMultiParagraphEmittedMark = (
|
||||
walkerContext: any,
|
||||
node: HtmlAST
|
||||
) => {
|
||||
const emittedNodes = walkerContext.getGlobalContext(
|
||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
|
||||
) as WeakSet<object> | undefined;
|
||||
if (!emittedNodes) {
|
||||
return false;
|
||||
}
|
||||
return emittedNodes.delete(node as object);
|
||||
};
|
||||
|
||||
export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
flavour: ParagraphBlockSchema.model.flavour,
|
||||
toMatch: o =>
|
||||
@@ -88,41 +208,37 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
!tagsInAncestor(o, ['p', 'li']) &&
|
||||
HastUtils.isParagraphLike(o.node)
|
||||
) {
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(o.node),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
const delta = deltaConverter.astToDelta(o.node);
|
||||
const deltas = getParagraphDeltas(o.node, delta);
|
||||
openParagraphBlocks(deltas, 'text', walkerContext);
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
const type = walkerContext.getGlobalContext('hast:blockquote')
|
||||
? 'quote'
|
||||
: 'text';
|
||||
const delta = deltaConverter.astToDelta(o.node);
|
||||
const deltas = getParagraphDeltas(o.node, delta);
|
||||
|
||||
if (deltas.length > 1) {
|
||||
openParagraphBlocks(deltas, type, walkerContext);
|
||||
markMultiParagraphEmitted(walkerContext, o.node);
|
||||
walkerContext.skipAllChildren();
|
||||
break;
|
||||
}
|
||||
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: walkerContext.getGlobalContext('hast:blockquote')
|
||||
? 'quote'
|
||||
: 'text',
|
||||
type,
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(o.node),
|
||||
delta,
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
@@ -192,6 +308,9 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
if (consumeMultiParagraphEmittedMark(walkerContext, o.node)) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
o.next?.type === 'element' &&
|
||||
o.next.tagName === 'div' &&
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
@@ -67,5 +66,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
|
||||
if (this.std.store.readonly) return;
|
||||
this.std.store.captureSync();
|
||||
let hasPasteTarget = false;
|
||||
this.std.command
|
||||
.chain()
|
||||
.try<{}>(cmd => [
|
||||
@@ -144,18 +145,39 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
if (!ctx.parentBlock) {
|
||||
return;
|
||||
}
|
||||
hasPasteTarget = true;
|
||||
this.std.clipboard
|
||||
.paste(
|
||||
e,
|
||||
this.std.store,
|
||||
ctx.parentBlock.model.id,
|
||||
ctx.blockIndex ? ctx.blockIndex + 1 : 1
|
||||
ctx.blockIndex !== undefined ? ctx.blockIndex + 1 : 1
|
||||
)
|
||||
.catch(console.error);
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
if (hasPasteTarget) return;
|
||||
|
||||
// If no valid selection target exists (for example, stale block selection
|
||||
// right after cut), create/focus the default paragraph and paste after it.
|
||||
const firstParagraphId = document
|
||||
.querySelector('affine-page-root')
|
||||
?.focusFirstParagraph?.()?.id;
|
||||
const parentModel = firstParagraphId
|
||||
? this.std.store.getParent(firstParagraphId)
|
||||
: null;
|
||||
const paragraphIndex =
|
||||
firstParagraphId && parentModel
|
||||
? parentModel.children.findIndex(child => child.id === firstParagraphId)
|
||||
: -1;
|
||||
const insertIndex = paragraphIndex >= 0 ? paragraphIndex + 1 : undefined;
|
||||
|
||||
this.std.clipboard
|
||||
.paste(e, this.std.store, parentModel?.id, insertIndex)
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
override mounted() {
|
||||
|
||||
@@ -33,7 +33,11 @@ import {
|
||||
ReleaseFromGroupIcon,
|
||||
UnlockIcon,
|
||||
} 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 { renderAlignmentMenu } from './alignment';
|
||||
@@ -61,14 +65,13 @@ export const builtinMiscToolbarConfig = {
|
||||
|
||||
const group = firstModel.group;
|
||||
|
||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||
group.removeChild(firstModel);
|
||||
batchRemoveChildren(group, [firstModel]);
|
||||
|
||||
firstModel.index = ctx.gfx.layer.generateIndex();
|
||||
|
||||
const parent = group.group;
|
||||
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
|
||||
otherElements.forEach(element => {
|
||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||
element.group?.removeChild(element);
|
||||
topElement.group?.addChild(element);
|
||||
if (element.group) {
|
||||
batchRemoveChildren(element.group, [element]);
|
||||
}
|
||||
if (topElement.group) {
|
||||
batchAddChildren(topElement.group, [element]);
|
||||
}
|
||||
});
|
||||
|
||||
if (otherElements.length === 0) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
FrameBlockModel,
|
||||
ImageBlockModel,
|
||||
isExternalEmbedModel,
|
||||
MindmapElementModel,
|
||||
NoteBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
@@ -401,7 +402,17 @@ function reorderElements(
|
||||
) {
|
||||
if (!models.length) return;
|
||||
|
||||
for (const model of models) {
|
||||
const normalizedModels = Array.from(
|
||||
new Map(
|
||||
models.map(model => {
|
||||
const reorderTarget =
|
||||
model.group instanceof MindmapElementModel ? model.group : model;
|
||||
return [reorderTarget.id, reorderTarget];
|
||||
})
|
||||
).values()
|
||||
);
|
||||
|
||||
for (const model of normalizedModels) {
|
||||
const index = ctx.gfx.layer.getReorderedIndex(model, type);
|
||||
|
||||
// block should be updated in transaction
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -2,16 +2,24 @@ import { type Color, ColorScheme } from '@blocksuite/affine-model';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { IBound } from '@blocksuite/global/gfx';
|
||||
import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
Bound,
|
||||
getBoundWithRotation,
|
||||
type IBound,
|
||||
intersects,
|
||||
} from '@blocksuite/global/gfx';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type {
|
||||
GfxCompatibleInterface,
|
||||
GfxController,
|
||||
GfxLocalElementModel,
|
||||
GridManager,
|
||||
LayerManager,
|
||||
SurfaceBlockModel,
|
||||
Viewport,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import last from 'lodash-es/last';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@@ -40,11 +48,82 @@ type RendererOptions = {
|
||||
surfaceModel: SurfaceBlockModel;
|
||||
};
|
||||
|
||||
export type CanvasRenderPassMetrics = {
|
||||
overlayCount: number;
|
||||
placeholderElementCount: number;
|
||||
renderByBoundCallCount: number;
|
||||
renderedElementCount: number;
|
||||
visibleElementCount: number;
|
||||
};
|
||||
|
||||
export type CanvasMemorySnapshot = {
|
||||
bytes: number;
|
||||
datasetLayerId: string | null;
|
||||
height: number;
|
||||
kind: 'main' | 'stacking';
|
||||
width: number;
|
||||
zIndex: string;
|
||||
};
|
||||
|
||||
export type CanvasRendererDebugMetrics = {
|
||||
canvasLayerCount: number;
|
||||
canvasMemoryBytes: number;
|
||||
canvasMemorySnapshots: CanvasMemorySnapshot[];
|
||||
canvasMemoryMegabytes: number;
|
||||
canvasPixelCount: number;
|
||||
coalescedRefreshCount: number;
|
||||
dirtyLayerRenderCount: number;
|
||||
fallbackElementCount: number;
|
||||
lastRenderDurationMs: number;
|
||||
lastRenderMetrics: CanvasRenderPassMetrics;
|
||||
maxRenderDurationMs: number;
|
||||
pooledStackingCanvasCount: number;
|
||||
refreshCount: number;
|
||||
renderCount: number;
|
||||
stackingCanvasCount: number;
|
||||
totalLayerCount: number;
|
||||
totalRenderDurationMs: number;
|
||||
visibleStackingCanvasCount: number;
|
||||
};
|
||||
|
||||
type MutableCanvasRendererDebugMetrics = Omit<
|
||||
CanvasRendererDebugMetrics,
|
||||
| 'canvasLayerCount'
|
||||
| 'canvasMemoryBytes'
|
||||
| 'canvasMemoryMegabytes'
|
||||
| 'canvasPixelCount'
|
||||
| 'canvasMemorySnapshots'
|
||||
| 'pooledStackingCanvasCount'
|
||||
| 'stackingCanvasCount'
|
||||
| 'totalLayerCount'
|
||||
| 'visibleStackingCanvasCount'
|
||||
>;
|
||||
|
||||
type RenderPassStats = CanvasRenderPassMetrics;
|
||||
|
||||
type StackingCanvasState = {
|
||||
bound: Bound | null;
|
||||
layerId: string | null;
|
||||
};
|
||||
|
||||
type RefreshTarget =
|
||||
| { type: 'all' }
|
||||
| { type: 'main' }
|
||||
| { type: 'element'; element: SurfaceElementModel | GfxLocalElementModel }
|
||||
| {
|
||||
type: 'elements';
|
||||
elements: Array<SurfaceElementModel | GfxLocalElementModel>;
|
||||
};
|
||||
|
||||
const STACKING_CANVAS_PADDING = 32;
|
||||
|
||||
export class CanvasRenderer {
|
||||
private _container!: HTMLElement;
|
||||
|
||||
private readonly _disposables = new DisposableGroup();
|
||||
|
||||
private readonly _gfx: GfxController;
|
||||
|
||||
private readonly _turboEnabled: () => boolean;
|
||||
|
||||
private readonly _overlays = new Set<Overlay>();
|
||||
@@ -53,6 +132,37 @@ export class CanvasRenderer {
|
||||
|
||||
private _stackingCanvas: HTMLCanvasElement[] = [];
|
||||
|
||||
private readonly _stackingCanvasPool: HTMLCanvasElement[] = [];
|
||||
|
||||
private readonly _stackingCanvasState = new WeakMap<
|
||||
HTMLCanvasElement,
|
||||
StackingCanvasState
|
||||
>();
|
||||
|
||||
private readonly _dirtyStackingCanvasIndexes = new Set<number>();
|
||||
|
||||
private _mainCanvasDirty = true;
|
||||
|
||||
private _needsFullRender = true;
|
||||
|
||||
private _debugMetrics: MutableCanvasRendererDebugMetrics = {
|
||||
refreshCount: 0,
|
||||
coalescedRefreshCount: 0,
|
||||
renderCount: 0,
|
||||
totalRenderDurationMs: 0,
|
||||
lastRenderDurationMs: 0,
|
||||
maxRenderDurationMs: 0,
|
||||
lastRenderMetrics: {
|
||||
renderByBoundCallCount: 0,
|
||||
visibleElementCount: 0,
|
||||
renderedElementCount: 0,
|
||||
placeholderElementCount: 0,
|
||||
overlayCount: 0,
|
||||
},
|
||||
dirtyLayerRenderCount: 0,
|
||||
fallbackElementCount: 0,
|
||||
};
|
||||
|
||||
canvas: HTMLCanvasElement;
|
||||
|
||||
ctx: CanvasRenderingContext2D;
|
||||
@@ -89,6 +199,7 @@ export class CanvasRenderer {
|
||||
this.layerManager = options.layerManager;
|
||||
this.grid = options.gridManager;
|
||||
this.provider = options.provider ?? {};
|
||||
this._gfx = this.std.get(GfxControllerIdentifier);
|
||||
|
||||
this._turboEnabled = () => {
|
||||
const featureFlagService = options.std.get(FeatureFlagService);
|
||||
@@ -132,15 +243,199 @@ export class CanvasRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
private _applyStackingCanvasLayout(
|
||||
canvas: HTMLCanvasElement,
|
||||
bound: Bound | null,
|
||||
dpr = window.devicePixelRatio
|
||||
) {
|
||||
const state =
|
||||
this._stackingCanvasState.get(canvas) ??
|
||||
({
|
||||
bound: null,
|
||||
layerId: canvas.dataset.layerId ?? null,
|
||||
} satisfies StackingCanvasState);
|
||||
|
||||
if (!bound || bound.w <= 0 || bound.h <= 0) {
|
||||
canvas.style.display = 'none';
|
||||
canvas.style.left = '0px';
|
||||
canvas.style.top = '0px';
|
||||
canvas.style.width = '0px';
|
||||
canvas.style.height = '0px';
|
||||
canvas.style.transform = '';
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
state.bound = null;
|
||||
state.layerId = canvas.dataset.layerId ?? null;
|
||||
this._stackingCanvasState.set(canvas, state);
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewportBounds, zoom, viewScale } = this.viewport;
|
||||
const width = bound.w * zoom;
|
||||
const height = bound.h * zoom;
|
||||
const left = (bound.x - viewportBounds.x) * zoom;
|
||||
const top = (bound.y - viewportBounds.y) * zoom;
|
||||
const actualWidth = Math.max(1, Math.ceil(width * dpr));
|
||||
const actualHeight = Math.max(1, Math.ceil(height * dpr));
|
||||
const transform = `translate(${left}px, ${top}px) scale(${1 / viewScale})`;
|
||||
|
||||
if (canvas.style.display !== 'block') {
|
||||
canvas.style.display = 'block';
|
||||
}
|
||||
if (canvas.style.left !== '0px') {
|
||||
canvas.style.left = '0px';
|
||||
}
|
||||
if (canvas.style.top !== '0px') {
|
||||
canvas.style.top = '0px';
|
||||
}
|
||||
if (canvas.style.width !== `${width}px`) {
|
||||
canvas.style.width = `${width}px`;
|
||||
}
|
||||
if (canvas.style.height !== `${height}px`) {
|
||||
canvas.style.height = `${height}px`;
|
||||
}
|
||||
if (canvas.style.transform !== transform) {
|
||||
canvas.style.transform = transform;
|
||||
}
|
||||
if (canvas.style.transformOrigin !== 'top left') {
|
||||
canvas.style.transformOrigin = 'top left';
|
||||
}
|
||||
|
||||
if (canvas.width !== actualWidth) {
|
||||
canvas.width = actualWidth;
|
||||
}
|
||||
|
||||
if (canvas.height !== actualHeight) {
|
||||
canvas.height = actualHeight;
|
||||
}
|
||||
|
||||
state.bound = bound;
|
||||
state.layerId = canvas.dataset.layerId ?? null;
|
||||
this._stackingCanvasState.set(canvas, state);
|
||||
}
|
||||
|
||||
private _clampBoundToViewport(bound: Bound, viewportBounds: Bound) {
|
||||
const minX = Math.max(bound.x, viewportBounds.x);
|
||||
const minY = Math.max(bound.y, viewportBounds.y);
|
||||
const maxX = Math.min(bound.maxX, viewportBounds.maxX);
|
||||
const maxY = Math.min(bound.maxY, viewportBounds.maxY);
|
||||
|
||||
if (maxX <= minX || maxY <= minY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Bound(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private _createCanvasForLayer(
|
||||
onCreated?: (canvas: HTMLCanvasElement) => void
|
||||
) {
|
||||
const reused = this._stackingCanvasPool.pop();
|
||||
|
||||
if (reused) {
|
||||
return reused;
|
||||
}
|
||||
|
||||
const created = document.createElement('canvas');
|
||||
onCreated?.(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private _findLayerIndexByElement(
|
||||
element: SurfaceElementModel | GfxLocalElementModel
|
||||
) {
|
||||
const canvasLayers = this.layerManager.getCanvasLayers();
|
||||
const index = canvasLayers.findIndex(layer =>
|
||||
layer.elements.some(layerElement => layerElement.id === element.id)
|
||||
);
|
||||
|
||||
return index === -1 ? null : index;
|
||||
}
|
||||
|
||||
private _getLayerRenderBound(
|
||||
elements: SurfaceElementModel[],
|
||||
viewportBounds: Bound
|
||||
) {
|
||||
let layerBound: Bound | null = null;
|
||||
|
||||
for (const element of elements) {
|
||||
const display = (element.display ?? true) && !element.hidden;
|
||||
|
||||
if (!display) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const elementBound = Bound.from(getBoundWithRotation(element));
|
||||
|
||||
if (!intersects(elementBound, viewportBounds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
layerBound = layerBound ? layerBound.unite(elementBound) : elementBound;
|
||||
}
|
||||
|
||||
if (!layerBound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._clampBoundToViewport(
|
||||
layerBound.expand(STACKING_CANVAS_PADDING),
|
||||
viewportBounds
|
||||
);
|
||||
}
|
||||
|
||||
private _getResolvedStackingCanvasBound(
|
||||
canvas: HTMLCanvasElement,
|
||||
bound: Bound | null
|
||||
) {
|
||||
if (!bound || !this._gfx.tool.dragging$.peek()) {
|
||||
return bound;
|
||||
}
|
||||
|
||||
const previousBound = this._stackingCanvasState.get(canvas)?.bound;
|
||||
|
||||
return previousBound ? previousBound.unite(bound) : bound;
|
||||
}
|
||||
|
||||
private _invalidate(target: RefreshTarget = { type: 'all' }) {
|
||||
if (target.type === 'all') {
|
||||
this._needsFullRender = true;
|
||||
this._mainCanvasDirty = true;
|
||||
this._dirtyStackingCanvasIndexes.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._needsFullRender) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.type === 'main') {
|
||||
this._mainCanvasDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const elements =
|
||||
target.type === 'element' ? [target.element] : target.elements;
|
||||
|
||||
for (const element of elements) {
|
||||
const layerIndex = this._findLayerIndexByElement(element);
|
||||
|
||||
if (layerIndex === null || layerIndex >= this._stackingCanvas.length) {
|
||||
this._mainCanvasDirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
this._dirtyStackingCanvasIndexes.add(layerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private _resetPooledCanvas(canvas: HTMLCanvasElement) {
|
||||
canvas.dataset.layerId = '';
|
||||
this._applyStackingCanvasLayout(canvas, null);
|
||||
}
|
||||
|
||||
private _initStackingCanvas(onCreated?: (canvas: HTMLCanvasElement) => void) {
|
||||
const layer = this.layerManager;
|
||||
const updateStackingCanvasSize = (canvases: HTMLCanvasElement[]) => {
|
||||
this._stackingCanvas = canvases;
|
||||
|
||||
const sizeUpdater = this._canvasSizeUpdater();
|
||||
|
||||
canvases.filter(sizeUpdater.filter).forEach(sizeUpdater.update);
|
||||
};
|
||||
const updateStackingCanvas = () => {
|
||||
/**
|
||||
* we already have a main canvas, so the last layer should be skipped
|
||||
@@ -159,11 +454,7 @@ export class CanvasRenderer {
|
||||
const created = i < currentCanvases.length;
|
||||
const canvas = created
|
||||
? currentCanvases[i]
|
||||
: document.createElement('canvas');
|
||||
|
||||
if (!created) {
|
||||
onCreated?.(canvas);
|
||||
}
|
||||
: this._createCanvasForLayer(onCreated);
|
||||
|
||||
canvas.dataset.layerId = `[${layer.indexes[0]}--${layer.indexes[1]}]`;
|
||||
canvas.style.zIndex = layer.zIndex.toString();
|
||||
@@ -171,7 +462,6 @@ export class CanvasRenderer {
|
||||
}
|
||||
|
||||
this._stackingCanvas = canvases;
|
||||
updateStackingCanvasSize(canvases);
|
||||
|
||||
if (currentCanvases.length !== canvases.length) {
|
||||
const diff = canvases.length - currentCanvases.length;
|
||||
@@ -189,12 +479,16 @@ export class CanvasRenderer {
|
||||
payload.added = canvases.slice(-diff);
|
||||
} else {
|
||||
payload.removed = currentCanvases.slice(diff);
|
||||
payload.removed.forEach(canvas => {
|
||||
this._resetPooledCanvas(canvas);
|
||||
this._stackingCanvasPool.push(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
this.stackingCanvasUpdated.next(payload);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
this.refresh({ type: 'all' });
|
||||
};
|
||||
|
||||
this._disposables.add(
|
||||
@@ -211,7 +505,7 @@ export class CanvasRenderer {
|
||||
|
||||
this._disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this.refresh();
|
||||
this.refresh({ type: 'all' });
|
||||
})
|
||||
);
|
||||
|
||||
@@ -222,7 +516,6 @@ export class CanvasRenderer {
|
||||
sizeUpdatedRafId = null;
|
||||
this._resetSize();
|
||||
this._render();
|
||||
this.refresh();
|
||||
}, this._container);
|
||||
})
|
||||
);
|
||||
@@ -233,69 +526,212 @@ export class CanvasRenderer {
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this.refresh();
|
||||
this.refresh({ type: 'all' });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let wasDragging = false;
|
||||
this._disposables.add(
|
||||
effect(() => {
|
||||
const isDragging = this._gfx.tool.dragging$.value;
|
||||
|
||||
if (wasDragging && !isDragging) {
|
||||
this.refresh({ type: 'all' });
|
||||
}
|
||||
|
||||
wasDragging = isDragging;
|
||||
})
|
||||
);
|
||||
|
||||
this.usePlaceholder = false;
|
||||
}
|
||||
|
||||
private _createRenderPassStats(): RenderPassStats {
|
||||
return {
|
||||
renderByBoundCallCount: 0,
|
||||
visibleElementCount: 0,
|
||||
renderedElementCount: 0,
|
||||
placeholderElementCount: 0,
|
||||
overlayCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private _getCanvasMemorySnapshots(): CanvasMemorySnapshot[] {
|
||||
return [this.canvas, ...this._stackingCanvas].map((canvas, index) => {
|
||||
return {
|
||||
kind: index === 0 ? 'main' : 'stacking',
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
bytes: canvas.width * canvas.height * 4,
|
||||
zIndex: canvas.style.zIndex,
|
||||
datasetLayerId: canvas.dataset.layerId ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _render() {
|
||||
const renderStart = performance.now();
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const { ctx } = this;
|
||||
const dpr = window.devicePixelRatio;
|
||||
const scale = zoom * dpr;
|
||||
const matrix = new DOMMatrix().scaleSelf(scale);
|
||||
const renderStats = this._createRenderPassStats();
|
||||
const fullRender = this._needsFullRender;
|
||||
const stackingIndexesToRender = fullRender
|
||||
? this._stackingCanvas.map((_, idx) => idx)
|
||||
: [...this._dirtyStackingCanvasIndexes];
|
||||
/**
|
||||
* if a layer does not have a corresponding canvas
|
||||
* its element will be add to this array and drawing on the
|
||||
* main canvas
|
||||
*/
|
||||
let fallbackElement: SurfaceElementModel[] = [];
|
||||
const allCanvasLayers = this.layerManager.getCanvasLayers();
|
||||
const viewportBound = Bound.from(viewportBounds);
|
||||
|
||||
this.layerManager.getCanvasLayers().forEach((layer, idx) => {
|
||||
if (!this._stackingCanvas[idx]) {
|
||||
fallbackElement = fallbackElement.concat(layer.elements);
|
||||
return;
|
||||
for (const idx of stackingIndexesToRender) {
|
||||
const layer = allCanvasLayers[idx];
|
||||
const canvas = this._stackingCanvas[idx];
|
||||
|
||||
if (!layer || !canvas) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const canvas = this._stackingCanvas[idx];
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
const rc = new RoughCanvas(ctx.canvas);
|
||||
const layerRenderBound = this._getLayerRenderBound(
|
||||
layer.elements,
|
||||
viewportBound
|
||||
);
|
||||
const resolvedLayerRenderBound = this._getResolvedStackingCanvasBound(
|
||||
canvas,
|
||||
layerRenderBound
|
||||
);
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this._applyStackingCanvasLayout(canvas, resolvedLayerRenderBound);
|
||||
|
||||
if (
|
||||
!resolvedLayerRenderBound ||
|
||||
canvas.width === 0 ||
|
||||
canvas.height === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const layerCtx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
const layerRc = new RoughCanvas(layerCtx.canvas);
|
||||
|
||||
layerCtx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
layerCtx.save();
|
||||
layerCtx.setTransform(matrix);
|
||||
|
||||
this._renderByBound(
|
||||
layerCtx,
|
||||
matrix,
|
||||
layerRc,
|
||||
resolvedLayerRenderBound,
|
||||
layer.elements,
|
||||
false,
|
||||
renderStats
|
||||
);
|
||||
}
|
||||
|
||||
if (fullRender || this._mainCanvasDirty) {
|
||||
allCanvasLayers.forEach((layer, idx) => {
|
||||
if (!this._stackingCanvas[idx]) {
|
||||
fallbackElement = fallbackElement.concat(layer.elements);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
ctx.save();
|
||||
ctx.setTransform(matrix);
|
||||
|
||||
this._renderByBound(ctx, matrix, rc, viewportBounds, layer.elements);
|
||||
});
|
||||
this._renderByBound(
|
||||
ctx,
|
||||
matrix,
|
||||
new RoughCanvas(ctx.canvas),
|
||||
viewportBounds,
|
||||
fallbackElement,
|
||||
true,
|
||||
renderStats
|
||||
);
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
ctx.save();
|
||||
|
||||
ctx.setTransform(matrix);
|
||||
|
||||
this._renderByBound(
|
||||
ctx,
|
||||
matrix,
|
||||
new RoughCanvas(ctx.canvas),
|
||||
viewportBounds,
|
||||
fallbackElement,
|
||||
true
|
||||
const canvasMemorySnapshots = this._getCanvasMemorySnapshots();
|
||||
const canvasMemoryBytes = canvasMemorySnapshots.reduce(
|
||||
(sum, snapshot) => sum + snapshot.bytes,
|
||||
0
|
||||
);
|
||||
const layerTypes = this.layerManager.layers.map(layer => layer.type);
|
||||
const renderDurationMs = performance.now() - renderStart;
|
||||
|
||||
this._debugMetrics.renderCount += 1;
|
||||
this._debugMetrics.totalRenderDurationMs += renderDurationMs;
|
||||
this._debugMetrics.lastRenderDurationMs = renderDurationMs;
|
||||
this._debugMetrics.maxRenderDurationMs = Math.max(
|
||||
this._debugMetrics.maxRenderDurationMs,
|
||||
renderDurationMs
|
||||
);
|
||||
this._debugMetrics.lastRenderMetrics = renderStats;
|
||||
this._debugMetrics.fallbackElementCount = fallbackElement.length;
|
||||
this._debugMetrics.dirtyLayerRenderCount = stackingIndexesToRender.length;
|
||||
|
||||
this._lastDebugSnapshot = {
|
||||
canvasMemorySnapshots,
|
||||
canvasMemoryBytes,
|
||||
canvasPixelCount: canvasMemorySnapshots.reduce(
|
||||
(sum, snapshot) => sum + snapshot.width * snapshot.height,
|
||||
0
|
||||
),
|
||||
stackingCanvasCount: this._stackingCanvas.length,
|
||||
canvasLayerCount: layerTypes.filter(type => type === 'canvas').length,
|
||||
totalLayerCount: layerTypes.length,
|
||||
pooledStackingCanvasCount: this._stackingCanvasPool.length,
|
||||
visibleStackingCanvasCount: this._stackingCanvas.filter(
|
||||
canvas => canvas.width > 0 && canvas.height > 0
|
||||
).length,
|
||||
};
|
||||
|
||||
this._needsFullRender = false;
|
||||
this._mainCanvasDirty = false;
|
||||
this._dirtyStackingCanvasIndexes.clear();
|
||||
}
|
||||
|
||||
private _lastDebugSnapshot: Pick<
|
||||
CanvasRendererDebugMetrics,
|
||||
| 'canvasMemoryBytes'
|
||||
| 'canvasMemorySnapshots'
|
||||
| 'canvasPixelCount'
|
||||
| 'canvasLayerCount'
|
||||
| 'pooledStackingCanvasCount'
|
||||
| 'stackingCanvasCount'
|
||||
| 'totalLayerCount'
|
||||
| 'visibleStackingCanvasCount'
|
||||
> = {
|
||||
canvasMemoryBytes: 0,
|
||||
canvasMemorySnapshots: [],
|
||||
canvasPixelCount: 0,
|
||||
canvasLayerCount: 0,
|
||||
pooledStackingCanvasCount: 0,
|
||||
stackingCanvasCount: 0,
|
||||
totalLayerCount: 0,
|
||||
visibleStackingCanvasCount: 0,
|
||||
};
|
||||
|
||||
private _renderByBound(
|
||||
ctx: CanvasRenderingContext2D | null,
|
||||
matrix: DOMMatrix,
|
||||
rc: RoughCanvas,
|
||||
bound: IBound,
|
||||
surfaceElements?: SurfaceElementModel[],
|
||||
overLay: boolean = false
|
||||
overLay: boolean = false,
|
||||
renderStats?: RenderPassStats
|
||||
) {
|
||||
if (!ctx) return;
|
||||
|
||||
renderStats && (renderStats.renderByBoundCallCount += 1);
|
||||
|
||||
const elements =
|
||||
surfaceElements ??
|
||||
(this.grid.search(bound, {
|
||||
@@ -305,10 +741,12 @@ export class CanvasRenderer {
|
||||
for (const element of elements) {
|
||||
const display = (element.display ?? true) && !element.hidden;
|
||||
if (display && intersects(getBoundWithRotation(element), bound)) {
|
||||
renderStats && (renderStats.visibleElementCount += 1);
|
||||
if (
|
||||
this.usePlaceholder &&
|
||||
!(element as GfxCompatibleInterface).forceFullRender
|
||||
) {
|
||||
renderStats && (renderStats.placeholderElementCount += 1);
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
|
||||
const drawX = element.x - bound.x;
|
||||
@@ -316,6 +754,7 @@ export class CanvasRenderer {
|
||||
ctx.fillRect(drawX, drawY, element.w, element.h);
|
||||
ctx.restore();
|
||||
} else {
|
||||
renderStats && (renderStats.renderedElementCount += 1);
|
||||
ctx.save();
|
||||
const renderFn = this.std.getOptional<ElementRenderer>(
|
||||
ElementRendererIdentifier(element.type)
|
||||
@@ -333,6 +772,7 @@ export class CanvasRenderer {
|
||||
}
|
||||
|
||||
if (overLay) {
|
||||
renderStats && (renderStats.overlayCount += this._overlays.size);
|
||||
for (const overlay of this._overlays) {
|
||||
ctx.save();
|
||||
ctx.translate(-bound.x, -bound.y);
|
||||
@@ -348,33 +788,38 @@ export class CanvasRenderer {
|
||||
const sizeUpdater = this._canvasSizeUpdater();
|
||||
|
||||
sizeUpdater.update(this.canvas);
|
||||
|
||||
this._stackingCanvas.forEach(sizeUpdater.update);
|
||||
this.refresh();
|
||||
this._invalidate({ type: 'all' });
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(() => this.refresh())
|
||||
surfaceModel.elementAdded.subscribe(() => this.refresh({ type: 'all' }))
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(() => this.refresh())
|
||||
surfaceModel.elementRemoved.subscribe(() => this.refresh({ type: 'all' }))
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(() => this.refresh())
|
||||
surfaceModel.localElementAdded.subscribe(() =>
|
||||
this.refresh({ type: 'all' })
|
||||
)
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
|
||||
surfaceModel.localElementDeleted.subscribe(() =>
|
||||
this.refresh({ type: 'all' })
|
||||
)
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
|
||||
surfaceModel.localElementUpdated.subscribe(({ model }) => {
|
||||
this.refresh({ type: 'element', element: model });
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this.refresh();
|
||||
const element = surfaceModel.getElementById(payload.id);
|
||||
this.refresh(element ? { type: 'element', element } : { type: 'all' });
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -382,7 +827,7 @@ export class CanvasRenderer {
|
||||
addOverlay(overlay: Overlay) {
|
||||
overlay.setRenderer(this);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
this.refresh({ type: 'main' });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,7 +839,7 @@ export class CanvasRenderer {
|
||||
container.append(this.canvas);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
this.refresh({ type: 'all' });
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -453,8 +898,46 @@ export class CanvasRenderer {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._refreshRafId !== null) return;
|
||||
getDebugMetrics(): CanvasRendererDebugMetrics {
|
||||
return {
|
||||
...this._debugMetrics,
|
||||
...this._lastDebugSnapshot,
|
||||
canvasMemoryMegabytes:
|
||||
this._lastDebugSnapshot.canvasMemoryBytes / 1024 / 1024,
|
||||
};
|
||||
}
|
||||
|
||||
resetDebugMetrics() {
|
||||
this._debugMetrics = {
|
||||
refreshCount: 0,
|
||||
coalescedRefreshCount: 0,
|
||||
renderCount: 0,
|
||||
totalRenderDurationMs: 0,
|
||||
lastRenderDurationMs: 0,
|
||||
maxRenderDurationMs: 0,
|
||||
lastRenderMetrics: this._createRenderPassStats(),
|
||||
dirtyLayerRenderCount: 0,
|
||||
fallbackElementCount: 0,
|
||||
};
|
||||
this._lastDebugSnapshot = {
|
||||
canvasMemoryBytes: 0,
|
||||
canvasMemorySnapshots: [],
|
||||
canvasPixelCount: 0,
|
||||
canvasLayerCount: 0,
|
||||
pooledStackingCanvasCount: 0,
|
||||
stackingCanvasCount: 0,
|
||||
totalLayerCount: 0,
|
||||
visibleStackingCanvasCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
refresh(target: RefreshTarget = { type: 'all' }) {
|
||||
this._debugMetrics.refreshCount += 1;
|
||||
this._invalidate(target);
|
||||
if (this._refreshRafId !== null) {
|
||||
this._debugMetrics.coalescedRefreshCount += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
@@ -469,6 +952,6 @@ export class CanvasRenderer {
|
||||
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
this.refresh({ type: 'main' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,30 +354,37 @@ export class DomRenderer {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(payload => {
|
||||
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
|
||||
if (payload.props['index'] || payload.props['groupId']) {
|
||||
this._markViewportDirty();
|
||||
}
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
@@ -387,6 +394,9 @@ export class DomRenderer {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
|
||||
if (payload.props['index'] || payload.props['childIds']) {
|
||||
this._markViewportDirty();
|
||||
}
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -40,10 +40,146 @@ export const SurfaceBlockSchemaExtension =
|
||||
|
||||
export class SurfaceBlockModel extends BaseSurfaceModel {
|
||||
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() {
|
||||
this._extendElement(elementsCtorMap);
|
||||
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
|
||||
.getAll(surfaceMiddlewareIdentifier)
|
||||
.forEach(({ middleware }) => {
|
||||
@@ -52,13 +188,31 @@ export class SurfaceBlockModel extends BaseSurfaceModel {
|
||||
}
|
||||
|
||||
getConnectors(id: string) {
|
||||
const connectors = this.getElementsByType(
|
||||
'connector'
|
||||
) as unknown[] as ConnectorElementModel[];
|
||||
const connectorIds = this._connectorIdsByEndpoint.get(id);
|
||||
|
||||
return connectors.filter(
|
||||
connector => connector.source?.id === id || connector.target?.id === id
|
||||
);
|
||||
if (!connectorIds?.size) {
|
||||
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>(
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@blocksuite/sync": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
||||
"@lottiefiles/dotlottie-wc": "^0.9.4",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/hast": "^3.0.4",
|
||||
@@ -82,5 +82,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
getHostName,
|
||||
isValidUrl,
|
||||
normalizeUrl,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { PropTypes, requiredProperties } from '@blocksuite/std';
|
||||
import { css, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
@@ -44,15 +48,27 @@ export class LinkPreview extends LitElement {
|
||||
|
||||
override render() {
|
||||
const { url } = this;
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
const safeUrl =
|
||||
normalizedUrl && isValidUrl(normalizedUrl) ? normalizedUrl : null;
|
||||
const hostName = getHostName(safeUrl ?? url);
|
||||
|
||||
if (!safeUrl) {
|
||||
return html`
|
||||
<span class="affine-link-preview">
|
||||
<span>${hostName}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<a
|
||||
class="affine-link-preview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href=${url}
|
||||
href=${safeUrl}
|
||||
>
|
||||
<span>${getHostName(url)}</span>
|
||||
<span>${hostName}</span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -48,5 +48,5 @@
|
||||
"!src/__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 { DataSource } from '../core/data-source/base.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 { t } from '../core/logical/type-presets.js';
|
||||
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
||||
@@ -17,7 +18,10 @@ import {
|
||||
pickKanbanGroupColumn,
|
||||
resolveKanbanGroupBy,
|
||||
} 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 { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.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', () => {
|
||||
it('shows drop preview when insert position exists', () => {
|
||||
const controller = createDragController();
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { FilterGroup } from '../core/filter/types.js';
|
||||
import { numberFormats } from '../property-presets/number/utils/formats.js';
|
||||
import {
|
||||
formatNumber,
|
||||
NumberFormatSchema,
|
||||
parseNumber,
|
||||
} 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 type { MobileTableGroup } from '../view-presets/table/mobile/group.js';
|
||||
import { pcEffects } from '../view-presets/table/pc/effect.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 */
|
||||
|
||||
@@ -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', () => {
|
||||
test('number format menu should expose all schema formats', () => {
|
||||
const menuFormats = numberFormats.map(format => format.type);
|
||||
|
||||
@@ -67,7 +67,7 @@ export const autoScrollOnBoundary = (
|
||||
};
|
||||
|
||||
const cancelBoxListen = effect(() => {
|
||||
box.value;
|
||||
void box.value;
|
||||
startUpdate();
|
||||
});
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ export class KanbanSingleView extends SingleViewBase<KanbanViewData> {
|
||||
isShow(rowId: string): boolean {
|
||||
if (this.filter$.value?.conditions.length) {
|
||||
const rowMap = Object.fromEntries(
|
||||
this.properties$.value.map(column => [
|
||||
this.propertiesRaw$.value.map(column => [
|
||||
column.id,
|
||||
column.cellGetOrCreate(rowId).jsonValue$.value,
|
||||
])
|
||||
|
||||
@@ -54,7 +54,9 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
const selectionView = this.selectionView;
|
||||
if (selectionView) {
|
||||
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({
|
||||
groupKey: this.groupKey,
|
||||
focus: {
|
||||
|
||||
@@ -24,12 +24,12 @@ import {
|
||||
DataViewUIBase,
|
||||
DataViewUILogicBase,
|
||||
} from '../../../core/view/data-view-base.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import {
|
||||
type TableSingleView,
|
||||
TableViewRowSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../index.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
} from '../selection.js';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { TableClipboardController } from './controller/clipboard.js';
|
||||
import { TableDragController } from './controller/drag.js';
|
||||
import { TableHotkeysController } from './controller/hotkeys.js';
|
||||
|
||||
@@ -57,7 +57,9 @@ export class TableViewCellContainer extends SignalWatcher(
|
||||
const selectionView = this.selectionController;
|
||||
if (selectionView) {
|
||||
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({
|
||||
groupKey: this.groupKey,
|
||||
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 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> {
|
||||
propertiesRaw$ = computed(() => {
|
||||
const needShow = new Set(this.dataSource.properties$.value);
|
||||
@@ -220,14 +266,10 @@ export class TableSingleView extends SingleViewBase<TableViewData> {
|
||||
return this.data$.value?.mode ?? 'table';
|
||||
}
|
||||
|
||||
constructor(viewManager: ViewManager, viewId: string) {
|
||||
super(viewManager, viewId);
|
||||
}
|
||||
|
||||
isShow(rowId: string): boolean {
|
||||
if (this.filter$.value?.conditions.length) {
|
||||
const rowMap = Object.fromEntries(
|
||||
this.properties$.value.map(column => [
|
||||
this.propertiesRaw$.value.map(column => [
|
||||
column.id,
|
||||
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];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
@@ -26,5 +26,5 @@
|
||||
"!src/__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 option - Optional configuration options for the provider
|
||||
*/
|
||||
setup(context: Context<Scope>, option?: Options) {
|
||||
setup(_context: Context<Scope>, option?: Options) {
|
||||
if (option) {
|
||||
this.schema.parse(option);
|
||||
}
|
||||
context;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||
testTimeout: 500,
|
||||
coverage: {
|
||||
provider: 'istanbul', // or 'c8'
|
||||
provider: 'istanbul',
|
||||
reporter: ['lcov'],
|
||||
reportsDirectory: '../../../.coverage/ext-loader',
|
||||
},
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -40,5 +39,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -42,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -41,5 +40,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
@@ -43,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.26.1"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user