mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-05 00:54:56 +00:00
Compare commits
50 Commits
graphite-b
...
v0.22.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f54bc0c047 | ||
|
|
1f0cc51462 | ||
|
|
160e4c2a38 | ||
|
|
99198e246b | ||
|
|
44e1eb503f | ||
|
|
2288cbe54d | ||
|
|
23ff398994 | ||
|
|
ee931d546e | ||
|
|
a02eed382d | ||
|
|
ab78b8e3ab | ||
|
|
3fe2ac4e46 | ||
|
|
d02aa8c7e0 | ||
|
|
cce756365a | ||
|
|
a88dcc0951 | ||
|
|
57208a3de4 | ||
|
|
d8cbeb1bb1 | ||
|
|
418b38e8de | ||
|
|
00ff373c01 | ||
|
|
39830a410a | ||
|
|
ef3be4a816 | ||
|
|
658393159b | ||
|
|
ac3f247f01 | ||
|
|
065d9c3b73 | ||
|
|
2e58c11799 | ||
|
|
10da3ad28e | ||
|
|
887a496f8b | ||
|
|
ada69c80f6 | ||
|
|
7b82dd656b | ||
|
|
5c96566dd8 | ||
|
|
a35e1b1882 | ||
|
|
756847d3cb | ||
|
|
3c3a8bb107 | ||
|
|
88eec2cdfb | ||
|
|
52777b0064 | ||
|
|
00ccd2d865 | ||
|
|
5d94bd41a4 | ||
|
|
20d8d6131a | ||
|
|
94539ac0d0 | ||
|
|
e1ce42a6fc | ||
|
|
2a7f0162cf | ||
|
|
34a5d9dec3 | ||
|
|
c68598c0e0 | ||
|
|
9c81c24fbe | ||
|
|
517aec79ba | ||
|
|
31a1841e25 | ||
|
|
625e8392a6 | ||
|
|
f616bd29d3 | ||
|
|
d6b9e9c60a | ||
|
|
bc67766bb9 | ||
|
|
9a96cfded0 |
5
.github/workflows/build-test.yml
vendored
5
.github/workflows/build-test.yml
vendored
@@ -151,7 +151,8 @@ jobs:
|
||||
- name: Clippy
|
||||
run: |
|
||||
rustup component add clippy
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
|
||||
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
|
||||
|
||||
check-git-status:
|
||||
name: Check Git Status
|
||||
@@ -923,7 +924,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --release --no-fail-fast
|
||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||
|
||||
copilot-api-test:
|
||||
name: Server Copilot Api Test
|
||||
|
||||
25
.github/workflows/release-mobile.yml
vendored
25
.github/workflows/release-mobile.yml
vendored
@@ -117,31 +117,10 @@ jobs:
|
||||
name: android
|
||||
path: packages/frontend/apps/android/dist
|
||||
|
||||
determine-ios-runner:
|
||||
runs-on: ubuntu-latest
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
|
||||
needs:
|
||||
- build-ios-web
|
||||
outputs:
|
||||
RUNNER: ${{ steps.runner.outputs.RUNNER }}
|
||||
steps:
|
||||
- name: Determine Runner
|
||||
id: runner
|
||||
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
|
||||
# blaze/macos-14 is free but has limited concurrency
|
||||
run: |
|
||||
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
|
||||
if [ $RANDOM_NUMBER -le 20 ]; then
|
||||
echo "Selected namespace-profile-macos (20% probability)"
|
||||
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Selected blaze/macos-14 (80% probability)"
|
||||
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
|
||||
needs:
|
||||
- determine-ios-runner
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download mobile artifact
|
||||
|
||||
384
Cargo.lock
generated
384
Cargo.lock
generated
@@ -281,12 +281,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -496,7 +496,7 @@ version = "0.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
@@ -546,9 +546,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -603,9 +603,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bpaf"
|
||||
version = "0.9.19"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4848ed5727d39a7573551c205bcb1ccd88c8cad4ed2c80f62e2316f208196b8d"
|
||||
checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
@@ -697,9 +697,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.22"
|
||||
version = "1.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
|
||||
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -815,9 +815,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -825,9 +825,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.38"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -901,9 +901,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -922,7 +922,7 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
@@ -935,7 +935,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation",
|
||||
"libc",
|
||||
]
|
||||
@@ -1241,7 +1241,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -1272,9 +1272,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "docx-rust"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6a8e9647d314f66d467a705715111d045955949b5dbcf2dc7aee89e078af83d"
|
||||
checksum = "75463cd42bd579fdf76cf10ff1c4fb5988568c726f8ed4c2ed3921ddb6ef5c89"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"hard-xml",
|
||||
@@ -1339,9 +1339,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.11"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
|
||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -1595,15 +1595,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
|
||||
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.58.0",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1689,9 +1690,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hard-xml"
|
||||
version = "1.39.0"
|
||||
version = "1.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18b4f798dc293307dc5445b47403a317adc8131782963f3ad4700a836bd26c93"
|
||||
checksum = "3b07b8ba970e18a03dbb79f6786b6e4d6f198a0ac839aa5182017001bb8dee17"
|
||||
dependencies = [
|
||||
"hard-xml-derive",
|
||||
"jetscii",
|
||||
@@ -1702,11 +1703,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hard-xml-derive"
|
||||
version = "1.39.0"
|
||||
version = "1.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef0ad18601a1563729b311c9d49e5d46b1c987dc0353742cb8219f61f4339a6"
|
||||
checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -1825,7 +1826,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.0",
|
||||
"windows-core 0.57.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1910,9 +1911,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
|
||||
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections 2.0.0",
|
||||
@@ -1926,9 +1927,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
|
||||
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
@@ -2049,15 +2050,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "io-surface"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e"
|
||||
checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e"
|
||||
dependencies = [
|
||||
"cgl",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"leaky-cow",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2188,12 +2188,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.7"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.53.0",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2279,7 +2279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59fa2559e99ba0f26a12458aabc754432c805bbb8cba516c427825a997af1fb7"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"cbc",
|
||||
"ecb",
|
||||
"encoding_rs",
|
||||
@@ -2390,13 +2390,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2410,12 +2410,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.0.0-alpha.33"
|
||||
version = "3.0.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c857a2b38c994db8bec785554ab4216d45ad63469832070c86a992be0b5491ad"
|
||||
checksum = "2a5c343e6e1fb57bf3ea3386638c4affb394ee932708128840a56aaac3d6a8ab"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"chrono",
|
||||
"ctor",
|
||||
"napi-build",
|
||||
@@ -2426,17 +2426,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.1.6"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28acfa557c083f6e254a786e01ba253fc56f18ee000afcd4f79af735f73a6da"
|
||||
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.0.0-alpha.29"
|
||||
version = "3.0.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7165d931d54f68115e651330d5fe0ae0081133d3f4ee3ab55b0b808f0c23f71"
|
||||
checksum = "08d23065ee795a4b1a8755fdf4a39c2a229679f01f923a8feea33f045d6d96cb"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2445,9 +2446,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "2.0.0-alpha.28"
|
||||
version = "2.0.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce3f36354262054df8e1c3a73bdcd36ea13f130feb1e4d86b67cab9e10d6ef6d"
|
||||
checksum = "348aaac2c51b5d11cf90cf7670b470c7f4d1607d15c338efd4d3db361003e4f5"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
@@ -2458,9 +2459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.0.0-alpha.1"
|
||||
version = "3.0.0-alpha.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cc061b99c514ad4b7abc99d4db1ca24b9542b7ff48b4760bd9f82b24611534d"
|
||||
checksum = "b443b980b2258dbaa31b99115e74da6c0866e537278309d566b4672a2f8df516"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
@@ -2477,7 +2478,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2625,7 +2626,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
@@ -2642,7 +2643,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -2673,6 +2674,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
@@ -3009,7 +3016,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
|
||||
dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"bit-vec 0.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
@@ -3038,7 +3045,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
@@ -3243,7 +3250,7 @@ version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3418,7 +3425,7 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -3461,9 +3468,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
@@ -3717,9 +3724,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.9"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -3746,9 +3753,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
|
||||
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -3759,9 +3766,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
|
||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -3796,9 +3803,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
|
||||
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3809,9 +3816,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
|
||||
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -3828,20 +3835,19 @@ dependencies = [
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 2.0.101",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
|
||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -3878,13 +3884,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
|
||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -3916,9 +3922,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
|
||||
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -4432,9 +4438,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -4570,9 +4576,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.4"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69aff09fea9a41fb061ae6b206cb87cac1b8db07df31be3ba271fbc26760f213"
|
||||
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -4935,11 +4941,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5115,9 +5123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
@@ -5157,7 +5165,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5178,12 +5186,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
version = "0.61.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5200,28 +5220,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.4.0",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5235,17 +5253,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
@@ -5268,17 +5275,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
@@ -5296,6 +5292,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
@@ -5307,37 +5313,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -5393,7 +5380,7 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
@@ -5401,19 +5388,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
"windows_i686_gnullvm 0.53.0",
|
||||
"windows_i686_msvc 0.53.0",
|
||||
"windows_x86_64_gnu 0.53.0",
|
||||
"windows_x86_64_gnullvm 0.53.0",
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5428,12 +5408,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -5446,12 +5420,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -5464,24 +5432,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -5494,12 +5450,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -5512,12 +5462,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -5530,12 +5474,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -5548,12 +5486,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
@@ -5569,7 +5501,7 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5638,7 +5570,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol_str",
|
||||
"thiserror 2.0.12",
|
||||
"yrs 0.23.1",
|
||||
"yrs 0.23.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5669,7 +5601,7 @@ dependencies = [
|
||||
"regex",
|
||||
"y-octo",
|
||||
"y-sync",
|
||||
"yrs 0.23.1",
|
||||
"yrs 0.23.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5747,9 +5679,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yrs"
|
||||
version = "0.23.1"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a7cab84724ae7f361a8c92465f5160922cbb941a499e1a8cacd103351ab9c78"
|
||||
checksum = "bb1d740a98b12112352f05ddfc06c1505b66fca116601f9388b84be45d21f84d"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-lock",
|
||||
@@ -5760,7 +5692,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smallstr",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -47,9 +47,9 @@ log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.28" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
@@ -80,7 +80,7 @@ symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.25"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
tokio = "1.45"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter-c = { version = "0.23" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
|
||||
@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style = styles[0];
|
||||
@@ -482,24 +482,26 @@ const builtinSurfaceToolbarConfig = {
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'b.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => BookmarkStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -259,18 +259,18 @@ const builtinToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
@@ -368,26 +368,26 @@ const builtinSurfaceToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (options?.viewType === 'card') {
|
||||
@@ -227,7 +227,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style =
|
||||
@@ -441,7 +441,11 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
let { style } = model.props;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (!BookmarkStyles.includes(style)) {
|
||||
if (
|
||||
!BookmarkStyles.includes(
|
||||
style as (typeof BookmarkStyles)[number]
|
||||
)
|
||||
) {
|
||||
style = BookmarkStyles[0];
|
||||
}
|
||||
|
||||
@@ -517,26 +521,26 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedGithubStyles.includes(action.id)),
|
||||
when(ctx) {
|
||||
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import {
|
||||
@@ -87,6 +88,12 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
this.gfx.tool.currentToolName$.value === 'frameNavigator';
|
||||
const frameIndex = this.gfx.layer.getZIndex(model);
|
||||
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="affine-frame-container"
|
||||
@@ -102,6 +109,7 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
|
||||
})}
|
||||
></div>
|
||||
${widgets}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -178,11 +186,22 @@ export const FrameBlockInteraction =
|
||||
selectable(context) {
|
||||
const { model } = context;
|
||||
|
||||
const onTitle =
|
||||
model.externalBound?.containsPoint([
|
||||
context.position.x,
|
||||
context.position.y,
|
||||
]) ?? false;
|
||||
|
||||
return (
|
||||
context.default(context) &&
|
||||
(model.isLocked() || !isTransparent(model.props.background))
|
||||
(model.isLocked() ||
|
||||
!isTransparent(model.props.background) ||
|
||||
onTitle)
|
||||
);
|
||||
},
|
||||
onSelect(context) {
|
||||
return context.default(context);
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
@@ -241,20 +241,35 @@ export class EdgelessFrameManager extends GfxExtension {
|
||||
surfaceModel.elementAdded.subscribe(({ id, local }) => {
|
||||
const element = surfaceModel.getElementById(id);
|
||||
if (element && local) {
|
||||
const frame = this.getFrameFromPoint(element.elementBound.center);
|
||||
|
||||
// if the container created with a frame, skip it.
|
||||
if (
|
||||
isGfxGroupCompatibleModel(element) &&
|
||||
frame &&
|
||||
element.hasChild(frame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// new element may intended to be added to other group
|
||||
// so we need to wait for the next microtask to check if the element can be added to the frame
|
||||
// The entire frame detection logic must be in microtask for timing reasons:
|
||||
//
|
||||
// 1. For connectors: When elementAdded fires, connectors have invalid bounds [0,0,0,0]
|
||||
// because their path/bounds are calculated in a separate microtask of updateConnectorPath by connector-watcher.
|
||||
// We need to wait for that calculation to complete before frame detection.
|
||||
//
|
||||
// 2. For shapes: Although they have valid bounds immediately, processing them in microtask
|
||||
// ensures consistent timing and allows other initialization to complete first.
|
||||
//
|
||||
// 3. Group compatibility: Some elements may need to establish their group relationships
|
||||
// before being considered for frame membership.
|
||||
//
|
||||
// By embedding the entire logic in microtask, we ensure:
|
||||
// - Connectors have proper bounds calculated (not [0,0,0,0])
|
||||
// - getFrameFromPoint() works correctly with valid element centers
|
||||
// - All element initialization is complete before frame detection
|
||||
queueMicrotask(() => {
|
||||
const frame = this.getFrameFromPoint(element.elementBound.center);
|
||||
|
||||
// if the container created with a frame, skip it.
|
||||
if (
|
||||
isGfxGroupCompatibleModel(element) &&
|
||||
frame &&
|
||||
element.hasChild(frame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add elements that aren't already grouped and have a valid frame
|
||||
if (!element.group && frame) {
|
||||
this.addElementsToFrame(frame, [element]);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import type {
|
||||
EmbedCardStyle,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BookmarkStyles,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
@@ -236,7 +239,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
const options: Record<string, unknown> = {};
|
||||
|
||||
let flavour = 'affine:bookmark';
|
||||
let style = BookmarkStyles[0];
|
||||
let style: EmbedCardStyle = BookmarkStyles[0];
|
||||
let isInternalLink = false;
|
||||
let isLinkedBlock = false;
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
) as SurfaceBlockModel;
|
||||
}
|
||||
|
||||
private get _viewportElement(): HTMLElement {
|
||||
get viewportElement(): HTMLElement {
|
||||
return this.std.get(ViewportElementProvider).viewportElement;
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
this.gfx.viewport.onResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(this._viewportElement);
|
||||
resizeObserver.observe(this.viewportElement);
|
||||
this._resizeObserver = resizeObserver;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,25 @@ type RendererOptions = {
|
||||
surfaceModel: SurfaceBlockModel;
|
||||
};
|
||||
|
||||
const UpdateType = {
|
||||
ELEMENT_ADDED: 'element-added',
|
||||
ELEMENT_REMOVED: 'element-removed',
|
||||
ELEMENT_UPDATED: 'element-updated',
|
||||
VIEWPORT_CHANGED: 'viewport-changed',
|
||||
SIZE_CHANGED: 'size-changed',
|
||||
ZOOM_STATE_CHANGED: 'zoom-state-changed',
|
||||
} as const;
|
||||
|
||||
type UpdateType = (typeof UpdateType)[keyof typeof UpdateType];
|
||||
|
||||
interface IncrementalUpdateState {
|
||||
dirtyElementIds: Set<string>;
|
||||
viewportDirty: boolean;
|
||||
sizeDirty: boolean;
|
||||
usePlaceholderDirty: boolean;
|
||||
pendingUpdates: Map<string, UpdateType[]>;
|
||||
}
|
||||
|
||||
const PLACEHOLDER_RESET_STYLES = {
|
||||
border: 'none',
|
||||
borderRadius: '0',
|
||||
@@ -141,6 +160,18 @@ export class DomRenderer {
|
||||
|
||||
private _sizeUpdatedRafId: number | null = null;
|
||||
|
||||
private readonly _updateState: IncrementalUpdateState = {
|
||||
dirtyElementIds: new Set(),
|
||||
viewportDirty: false,
|
||||
sizeDirty: false,
|
||||
usePlaceholderDirty: false,
|
||||
pendingUpdates: new Map(),
|
||||
};
|
||||
|
||||
private _lastViewportBounds: Bound | null = null;
|
||||
private _lastZoom: number | null = null;
|
||||
private _lastUsePlaceholder: boolean = false;
|
||||
|
||||
rootElement: HTMLElement;
|
||||
|
||||
private readonly _elementsMap = new Map<string, HTMLElement>();
|
||||
@@ -186,6 +217,7 @@ export class DomRenderer {
|
||||
private _initViewport() {
|
||||
this._disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
@@ -195,6 +227,7 @@ export class DomRenderer {
|
||||
if (this._sizeUpdatedRafId) return;
|
||||
this._sizeUpdatedRafId = requestConnectedFrame(() => {
|
||||
this._sizeUpdatedRafId = null;
|
||||
this._markSizeDirty();
|
||||
this._resetSize();
|
||||
this._render();
|
||||
this.refresh();
|
||||
@@ -208,6 +241,7 @@ export class DomRenderer {
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this._markUsePlaceholderDirty();
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
@@ -307,6 +341,292 @@ export class DomRenderer {
|
||||
}
|
||||
|
||||
private _render() {
|
||||
this._renderIncremental();
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(payload => {
|
||||
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = (overlay: Overlay) => {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
attach = (container: HTMLElement) => {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
};
|
||||
|
||||
generateColorProperty = (color: Color, fallback?: Color) => {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
};
|
||||
|
||||
getColorScheme = () => {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
};
|
||||
|
||||
getColorValue = (color: Color, fallback?: Color, real?: boolean) => {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
};
|
||||
|
||||
getPropertyValue = (property: string) => {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
};
|
||||
|
||||
refresh = () => {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
};
|
||||
|
||||
removeOverlay = (overlay: Overlay) => {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark a specific element as dirty for incremental updates
|
||||
* @param elementId - The ID of the element to mark as dirty
|
||||
* @param updateType - The type of update (optional, defaults to ELEMENT_UPDATED)
|
||||
*/
|
||||
markElementDirty = (
|
||||
elementId: string,
|
||||
updateType: UpdateType = UpdateType.ELEMENT_UPDATED
|
||||
) => {
|
||||
this._markElementDirty(elementId, updateType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a full re-render of all elements
|
||||
*/
|
||||
forceFullRender = () => {
|
||||
this._updateState.viewportDirty = true;
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
private _markElementDirty(elementId: string, updateType: UpdateType) {
|
||||
this._updateState.dirtyElementIds.add(elementId);
|
||||
const currentUpdates =
|
||||
this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (!currentUpdates.includes(updateType)) {
|
||||
currentUpdates.push(updateType);
|
||||
this._updateState.pendingUpdates.set(elementId, currentUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private _markViewportDirty() {
|
||||
this._updateState.viewportDirty = true;
|
||||
}
|
||||
|
||||
private _markSizeDirty() {
|
||||
this._updateState.sizeDirty = true;
|
||||
}
|
||||
|
||||
private _markUsePlaceholderDirty() {
|
||||
this._updateState.usePlaceholderDirty = true;
|
||||
}
|
||||
|
||||
private _clearUpdateState() {
|
||||
this._updateState.dirtyElementIds.clear();
|
||||
this._updateState.viewportDirty = false;
|
||||
this._updateState.sizeDirty = false;
|
||||
this._updateState.usePlaceholderDirty = false;
|
||||
this._updateState.pendingUpdates.clear();
|
||||
}
|
||||
|
||||
private _isViewportChanged(): boolean {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
|
||||
if (!this._lastViewportBounds || !this._lastZoom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
this._lastViewportBounds.x !== viewportBounds.x ||
|
||||
this._lastViewportBounds.y !== viewportBounds.y ||
|
||||
this._lastViewportBounds.w !== viewportBounds.w ||
|
||||
this._lastViewportBounds.h !== viewportBounds.h ||
|
||||
this._lastZoom !== zoom
|
||||
);
|
||||
}
|
||||
|
||||
private _isUsePlaceholderChanged(): boolean {
|
||||
return this._lastUsePlaceholder !== this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _updateLastState() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
this._lastViewportBounds = {
|
||||
x: viewportBounds.x,
|
||||
y: viewportBounds.y,
|
||||
w: viewportBounds.w,
|
||||
h: viewportBounds.h,
|
||||
} as Bound;
|
||||
this._lastZoom = zoom;
|
||||
this._lastUsePlaceholder = this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _renderIncremental() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
|
||||
const needsFullRender =
|
||||
this._isViewportChanged() ||
|
||||
this._isUsePlaceholderChanged() ||
|
||||
this._updateState.sizeDirty ||
|
||||
this._updateState.viewportDirty ||
|
||||
this._updateState.usePlaceholderDirty;
|
||||
|
||||
if (needsFullRender) {
|
||||
this._renderFull();
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update dirty elements
|
||||
const elementsFromGrid = this.grid.search(viewportBounds, {
|
||||
filter: ['canvas', 'local'],
|
||||
}) as SurfaceElementModel[];
|
||||
|
||||
const visibleElementIds = new Set<string>();
|
||||
|
||||
// 1. Update dirty elements
|
||||
for (const elementModel of elementsFromGrid) {
|
||||
const display = (elementModel.display ?? true) && !elementModel.hidden;
|
||||
if (
|
||||
display &&
|
||||
intersects(getBoundWithRotation(elementModel), viewportBounds)
|
||||
) {
|
||||
visibleElementIds.add(elementModel.id);
|
||||
|
||||
// Only update dirty elements
|
||||
if (this._updateState.dirtyElementIds.has(elementModel.id)) {
|
||||
if (
|
||||
this.usePlaceholder &&
|
||||
!(elementModel as GfxCompatibleInterface).forceFullRender
|
||||
) {
|
||||
this._renderOrUpdatePlaceholder(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
} else {
|
||||
this._renderOrUpdateFullElement(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove elements that are no longer in the grid
|
||||
for (const elementId of this._updateState.dirtyElementIds) {
|
||||
const updateTypes = this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (
|
||||
updateTypes.includes(UpdateType.ELEMENT_REMOVED) ||
|
||||
!visibleElementIds.has(elementId)
|
||||
) {
|
||||
const domElem = this._elementsMap.get(elementId);
|
||||
if (domElem) {
|
||||
domElem.remove();
|
||||
this._elementsMap.delete(elementId);
|
||||
elementsToRemove.push(domElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Notify changes
|
||||
if (addedElements.length > 0 || elementsToRemove.length > 0) {
|
||||
this.elementsUpdated.next({
|
||||
elements: Array.from(this._elementsMap.values()),
|
||||
added: addedElements,
|
||||
removed: elementsToRemove,
|
||||
});
|
||||
}
|
||||
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
}
|
||||
|
||||
private _renderFull() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
@@ -387,100 +707,4 @@ export class DomRenderer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay(overlay: Overlay) {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement) {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
}
|
||||
|
||||
generateColorProperty(color: Color, fallback?: Color) {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
}
|
||||
|
||||
getColorValue(color: Color, fallback?: Color, real?: boolean) {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getPropertyValue(property: string) {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
}
|
||||
|
||||
removeOverlay(overlay: Overlay) {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@ export class OpenDocDropdownMenu extends SignalWatcher(
|
||||
gap: unset !important;
|
||||
}
|
||||
|
||||
editor-icon-button {
|
||||
.label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-orientation] {
|
||||
width: 264px;
|
||||
gap: 4px;
|
||||
|
||||
@@ -9,6 +9,7 @@ const toolbarColorKeys: Array<keyof AffineCssVariables> = [
|
||||
'--affine-background-overlay-panel-color',
|
||||
'--affine-v2-layer-background-overlayPanel' as never,
|
||||
'--affine-v2-layer-insideBorder-blackBorder' as never,
|
||||
'--affine-v2-icon-primary' as never,
|
||||
'--affine-background-error-color',
|
||||
'--affine-background-primary-color',
|
||||
'--affine-background-tertiary-color',
|
||||
|
||||
@@ -16,5 +16,6 @@ export const renderFilterBar = (props: DataViewWidgetProps) => {
|
||||
.vars="${filterTrait.view.vars$}"
|
||||
.filterGroup="${filterTrait.filter$}"
|
||||
.onChange="${filterTrait.filterSet}"
|
||||
.dataViewLogic="${props.dataViewLogic}"
|
||||
></filter-bar>`;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { property } from 'lit/decorators.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import type { DataViewUILogicBase } from '../../../core/view/data-view-base.js';
|
||||
import { popFilterGroup } from './group-panel-view.js';
|
||||
|
||||
export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
@@ -99,6 +100,7 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
requestAnimationFrame(() => {
|
||||
this.expandGroup(element, index);
|
||||
});
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -206,6 +208,9 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewLogic!: DataViewUILogicBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -26,7 +26,10 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { FilterTrait } from '../../../core/filter/trait.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import {
|
||||
type DataViewUILogicBase,
|
||||
popCreateFilter,
|
||||
} from '../../../core/index.js';
|
||||
import {
|
||||
type FilterGroupView,
|
||||
getDepth,
|
||||
@@ -375,6 +378,7 @@ export const popFilterRoot = (
|
||||
props: {
|
||||
filterTrait: FilterTrait;
|
||||
onBack: () => void;
|
||||
dataViewLogic: DataViewUILogicBase;
|
||||
}
|
||||
) => {
|
||||
const filterTrait = props.filterTrait;
|
||||
@@ -414,6 +418,10 @@ export const popFilterRoot = (
|
||||
...value,
|
||||
conditions: [...value.conditions, filter],
|
||||
});
|
||||
props.dataViewLogic.eventTrace(
|
||||
'CreateDatabaseFilter',
|
||||
{}
|
||||
);
|
||||
},
|
||||
},
|
||||
{ middleware: subMenuMiddleware }
|
||||
|
||||
@@ -75,6 +75,7 @@ export class DataViewHeaderToolsFilter extends WidgetBase {
|
||||
conditions: [filter],
|
||||
};
|
||||
this.toggleShowFilter(true);
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -145,13 +145,16 @@ const createSettingMenus = (
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -192,10 +192,14 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
||||
this._updateTitleInMeta();
|
||||
this.requestUpdate();
|
||||
};
|
||||
this._rootModel?.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
this._rootModel?.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
|
||||
if (this._rootModel) {
|
||||
const rootModel = this._rootModel;
|
||||
rootModel.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
rootModel.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { type Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
type DragExtensionInitializeContext,
|
||||
type ExtensionDragMoveContext,
|
||||
@@ -74,47 +74,63 @@ export class SnapExtension extends InteractivityExtension {
|
||||
return {};
|
||||
}
|
||||
|
||||
let alignBound: Bound | null = null;
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
alignBound = snapOverlay.setMovingElements(context.elements);
|
||||
snapOverlay.setMovingElements(context.elements);
|
||||
},
|
||||
onResizeMove(context) {
|
||||
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
|
||||
return;
|
||||
const {
|
||||
handle,
|
||||
originalBound,
|
||||
scaleX,
|
||||
scaleY,
|
||||
handleSign,
|
||||
currentHandlePos,
|
||||
elements,
|
||||
} = context;
|
||||
const rotate = elements.length > 1 ? 0 : elements[0].rotate;
|
||||
const alignDirection: ('vertical' | 'horizontal')[] = [];
|
||||
let switchDirection = false;
|
||||
let nx = handleSign.x;
|
||||
let ny = handleSign.y;
|
||||
|
||||
if (handle.length > 6) {
|
||||
alignDirection.push('vertical', 'horizontal');
|
||||
} else if (rotate % 90 === 0) {
|
||||
nx =
|
||||
handleSign.x * Math.cos((rotate / 180) * Math.PI) -
|
||||
handleSign.y * Math.sin((rotate / 180) * Math.PI);
|
||||
ny =
|
||||
handleSign.x * Math.sin((rotate / 180) * Math.PI) +
|
||||
handleSign.y * Math.cos((rotate / 180) * Math.PI);
|
||||
|
||||
if (Math.abs(nx) > Math.abs(ny)) {
|
||||
alignDirection.push('horizontal');
|
||||
} else {
|
||||
alignDirection.push('vertical');
|
||||
}
|
||||
|
||||
if (rotate % 180 !== 0) {
|
||||
switchDirection = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { handle, handleSign, lockRatio } = context;
|
||||
let { dx, dy } = context;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(
|
||||
Math.abs(dx / alignBound.w),
|
||||
Math.abs(dy / alignBound.h)
|
||||
if (alignDirection.length > 0) {
|
||||
const rst = snapOverlay.alignResize(
|
||||
currentHandlePos,
|
||||
alignDirection
|
||||
);
|
||||
|
||||
dx = min * Math.sign(dx) * alignBound.w;
|
||||
dy = min * Math.sign(dy) * alignBound.h;
|
||||
const dx = switchDirection ? ny * rst.dy : nx * rst.dx;
|
||||
const dy = switchDirection ? nx * rst.dx : ny * rst.dy;
|
||||
|
||||
context.suggest({
|
||||
scaleX: scaleX + dx / originalBound.w,
|
||||
scaleY: scaleY + dy / originalBound.h,
|
||||
});
|
||||
}
|
||||
|
||||
const currentBound = new Bound(
|
||||
alignBound.x +
|
||||
(handle.includes('left') ? -dx * handleSign.xSign : 0),
|
||||
alignBound.y +
|
||||
(handle.includes('top') ? -dy * handleSign.ySign : 0),
|
||||
Math.abs(alignBound.w + dx * handleSign.xSign),
|
||||
Math.abs(alignBound.h + dy * handleSign.ySign)
|
||||
);
|
||||
const alignRst = snapOverlay.align(currentBound);
|
||||
|
||||
context.suggest({
|
||||
dx: alignRst.dx + context.dx,
|
||||
dy: alignRst.dy + context.dy,
|
||||
});
|
||||
},
|
||||
onResizeEnd() {
|
||||
alignBound = null;
|
||||
snapOverlay.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ConnectorElementModel,
|
||||
MindmapElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
|
||||
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
|
||||
interface Distance {
|
||||
@@ -586,6 +586,60 @@ export class SnapOverlay extends Overlay {
|
||||
);
|
||||
}
|
||||
|
||||
alignResize(position: IVec, direction: ('vertical' | 'horizontal')[]) {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
|
||||
const { viewport } = this.gfx;
|
||||
const threshold = ALIGN_THRESHOLD / viewport.zoom;
|
||||
const searchBound = new Bound(
|
||||
position[0] - threshold / 2,
|
||||
position[1] - threshold / 2,
|
||||
threshold,
|
||||
threshold
|
||||
);
|
||||
const alignBound = new Bound(position[0], position[1], 0, 0);
|
||||
|
||||
this._intraGraphicAlignLines = {
|
||||
horizontal: [],
|
||||
vertical: [],
|
||||
};
|
||||
this._distributedAlignLines = [];
|
||||
this._updateAlignCandidates(searchBound);
|
||||
|
||||
for (const other of this._referenceBounds.all) {
|
||||
const closestDistances = this._calculateClosestDistances(
|
||||
alignBound,
|
||||
other
|
||||
);
|
||||
|
||||
if (
|
||||
direction.includes('horizontal') &&
|
||||
closestDistances.horiz &&
|
||||
(!this._intraGraphicAlignLines.horizontal.length ||
|
||||
Math.abs(closestDistances.horiz.distance) < Math.abs(rst.dx))
|
||||
) {
|
||||
this._updateXAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
|
||||
if (
|
||||
direction.includes('vertical') &&
|
||||
closestDistances.vert &&
|
||||
(!this._intraGraphicAlignLines.vertical.length ||
|
||||
Math.abs(closestDistances.vert.distance) < Math.abs(rst.dy))
|
||||
) {
|
||||
this._updateYAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
}
|
||||
|
||||
this._intraGraphicAlignLines.horizontal =
|
||||
this._intraGraphicAlignLines.horizontal.slice(0, 1);
|
||||
this._intraGraphicAlignLines.vertical =
|
||||
this._intraGraphicAlignLines.vertical.slice(0, 1);
|
||||
this._renderer?.refresh();
|
||||
|
||||
return rst;
|
||||
}
|
||||
|
||||
align(bound: Bound): { dx: number; dy: number } {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom;
|
||||
|
||||
@@ -9,18 +9,35 @@ function applyShapeSpecificStyles(
|
||||
element: HTMLElement,
|
||||
zoom: number
|
||||
) {
|
||||
if (model.shapeType === 'rect') {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
} else if (model.shapeType === 'ellipse') {
|
||||
element.style.borderRadius = '50%';
|
||||
} else {
|
||||
element.style.borderRadius = '';
|
||||
// Reset properties that might be set by different shape types
|
||||
element.style.removeProperty('clip-path');
|
||||
element.style.removeProperty('border-radius');
|
||||
// Clear DOM for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based
|
||||
if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') {
|
||||
while (element.firstChild) element.firstChild.remove();
|
||||
}
|
||||
|
||||
switch (model.shapeType) {
|
||||
case 'rect': {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
break;
|
||||
}
|
||||
case 'ellipse':
|
||||
element.style.borderRadius = '50%';
|
||||
break;
|
||||
case 'diamond':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
|
||||
break;
|
||||
case 'triangle':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)';
|
||||
break;
|
||||
}
|
||||
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
|
||||
}
|
||||
|
||||
function applyBorderStyles(
|
||||
@@ -78,6 +95,9 @@ export const shapeDomRenderer = (
|
||||
renderer: DomRenderer
|
||||
): void => {
|
||||
const { zoom } = renderer.viewport;
|
||||
const unscaledWidth = model.w;
|
||||
const unscaledHeight = model.h;
|
||||
|
||||
const fillColor = renderer.getColorValue(
|
||||
model.fillColor,
|
||||
DefaultTheme.shapeFillColor,
|
||||
@@ -89,17 +109,80 @@ export const shapeDomRenderer = (
|
||||
true
|
||||
);
|
||||
|
||||
element.style.width = `${model.w * zoom}px`;
|
||||
element.style.height = `${model.h * zoom}px`;
|
||||
element.style.width = `${unscaledWidth * zoom}px`;
|
||||
element.style.height = `${unscaledHeight * zoom}px`;
|
||||
element.style.boxSizing = 'border-box';
|
||||
|
||||
// Apply shape-specific clipping, border-radius, and potentially clear innerHTML
|
||||
applyShapeSpecificStyles(model, element, zoom);
|
||||
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
if (model.shapeType === 'diamond' || model.shapeType === 'triangle') {
|
||||
// For diamond and triangle, fill and border are handled by inline SVG
|
||||
element.style.border = 'none'; // Ensure no standard CSS border interferes
|
||||
element.style.backgroundColor = 'transparent'; // Host element is transparent
|
||||
|
||||
const strokeW = model.strokeWidth;
|
||||
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
|
||||
|
||||
let svgPoints = '';
|
||||
if (model.shapeType === 'diamond') {
|
||||
// Adjusted points for diamond
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
|
||||
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight / 2}`,
|
||||
].join(' ');
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
const finalStrokeColor =
|
||||
model.strokeStyle !== 'none' && strokeW > 0 ? strokeColor : 'transparent';
|
||||
// Determine dash array, only if stroke is visible and style is 'dash'
|
||||
const finalStrokeDasharray =
|
||||
model.strokeStyle === 'dash' && finalStrokeColor !== 'transparent'
|
||||
? '12, 12'
|
||||
: 'none';
|
||||
// Determine fill color
|
||||
const finalFillColor = model.filled ? fillColor : 'transparent';
|
||||
|
||||
// Build SVG safely with DOM-API
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
|
||||
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||
polygon.setAttribute('points', svgPoints);
|
||||
polygon.setAttribute('fill', finalFillColor);
|
||||
polygon.setAttribute('stroke', finalStrokeColor);
|
||||
polygon.setAttribute('stroke-width', String(strokeW));
|
||||
if (finalStrokeDasharray !== 'none') {
|
||||
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
|
||||
}
|
||||
svg.append(polygon);
|
||||
|
||||
// Replace existing children to avoid memory leaks
|
||||
element.replaceChildren(svg);
|
||||
} else {
|
||||
// Standard rendering for other shapes (e.g., rect, ellipse)
|
||||
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
|
||||
}
|
||||
|
||||
applyBorderStyles(model, element, strokeColor, zoom);
|
||||
applyTransformStyles(model, element);
|
||||
|
||||
element.style.boxSizing = 'border-box';
|
||||
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
|
||||
|
||||
manageClassNames(model, element);
|
||||
|
||||
@@ -188,6 +188,8 @@ export class AffineLatexNode extends SignalWatcher(
|
||||
this._editorAbortController?.abort();
|
||||
this._editorAbortController = new AbortController();
|
||||
|
||||
blockComponent.selection.setGroup('note', []);
|
||||
|
||||
const portal = createLitPortal({
|
||||
template: html`<latex-editor-menu
|
||||
.std=${this.std}
|
||||
|
||||
@@ -30,11 +30,12 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
|
||||
*/
|
||||
type BackwardCompatibleUndefined = undefined;
|
||||
|
||||
export const AttachmentBlockStyles: EmbedCardStyle[] = [
|
||||
export const AttachmentBlockStyles = [
|
||||
'cubeThick',
|
||||
'horizontalThin',
|
||||
'pdf',
|
||||
] as const;
|
||||
'citation',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type AttachmentBlockProps = {
|
||||
name: string;
|
||||
|
||||
@@ -15,13 +15,13 @@ import type {
|
||||
LinkPreviewData,
|
||||
} from '../../utils/index.js';
|
||||
|
||||
export const BookmarkStyles: EmbedCardStyle[] = [
|
||||
export const BookmarkStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'citation',
|
||||
] as const;
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type BookmarkBlockProps = {
|
||||
style: (typeof BookmarkStyles)[number];
|
||||
|
||||
@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedFigmaBlockProps = {
|
||||
style: (typeof EmbedFigmaStyles)[number];
|
||||
|
||||
@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
|
||||
assignees: string[] | null;
|
||||
};
|
||||
|
||||
export const EmbedGithubStyles: EmbedCardStyle[] = [
|
||||
export const EmbedGithubStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
] as const;
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedGithubBlockProps = {
|
||||
style: (typeof EmbedGithubStyles)[number];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BlockModel } from '@blocksuite/store';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
|
||||
export const EmbedHtmlStyles = ['html'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedHtmlBlockProps = {
|
||||
style: (typeof EmbedHtmlStyles)[number];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { type EmbedCardStyle } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
export const EmbedIframeStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedIframeBlockProps = {
|
||||
url: string; // the original url that user input
|
||||
|
||||
@@ -4,17 +4,17 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
|
||||
export const EmbedLinkedDocStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'horizontalThin',
|
||||
'citation',
|
||||
];
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedLinkedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
style: (typeof EmbedLinkedDocStyles)[number];
|
||||
caption: string | null;
|
||||
footnoteIdentifier: string | null;
|
||||
} & ReferenceInfo;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedLoomBlockProps = {
|
||||
style: (typeof EmbedLoomStyles)[number];
|
||||
|
||||
@@ -5,7 +5,9 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
|
||||
export const EmbedSyncedDocStyles = [
|
||||
'syncedDoc',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedSyncedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { BookmarkBlockModel } from '../bookmark';
|
||||
import { EmbedFigmaModel } from './figma';
|
||||
import { EmbedGithubModel } from './github';
|
||||
import type { EmbedHtmlModel } from './html';
|
||||
@@ -30,7 +31,10 @@ export type EmbedCardModel = InstanceType<
|
||||
ExternalEmbedModel | InternalEmbedModel
|
||||
>;
|
||||
|
||||
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
|
||||
export type LinkableEmbedModel =
|
||||
| EmbedCardModel
|
||||
| EmbedIframeBlockModel
|
||||
| BookmarkBlockModel;
|
||||
|
||||
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
|
||||
creatorImage: string | null;
|
||||
};
|
||||
|
||||
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedYoutubeBlockProps = {
|
||||
style: (typeof EmbedYoutubeStyles)[number];
|
||||
|
||||
@@ -80,4 +80,8 @@ describe('isValidUrl: determining whether a URL is valid is very complicated', (
|
||||
// See also https://stackoverflow.com/questions/9238640/how-long-can-a-tld-possibly-be#:~:text=Longest%20TLD%20up%20to%20date,17%20when%20decoded%20%5Bverm%C3%B6gensberatung%5D.
|
||||
expect(isValidUrl('example.xn--vermgensberatung-pwb')).toEqual(false);
|
||||
});
|
||||
|
||||
test('should allow ip address url when origin is same', () => {
|
||||
expect(isValidUrl('http://127.0.0.1', 'http://127.0.0.1')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ const handlePoint = (
|
||||
};
|
||||
|
||||
const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
|
||||
slots.afterExport.subscribe(payload => {
|
||||
const afterExportSubscription = slots.afterExport.subscribe(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const snapshot = payload.snapshot;
|
||||
|
||||
@@ -53,10 +53,14 @@ const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
afterExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
export const copyMiddleware = (std: BlockStdScope): TransformerMiddleware => {
|
||||
return ({ slots }) => {
|
||||
sliceText(slots, std);
|
||||
return sliceText(slots, std);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
export const fileNameMiddleware =
|
||||
(fileName?: string): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type !== 'page') {
|
||||
return;
|
||||
}
|
||||
@@ -20,4 +20,8 @@ export const fileNameMiddleware =
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -528,7 +528,7 @@ export const pasteMiddleware = (
|
||||
): TransformerMiddleware => {
|
||||
return ({ slots }) => {
|
||||
let tr: PasteTr | undefined;
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
const { snapshot } = payload;
|
||||
flatNote(snapshot);
|
||||
@@ -543,13 +543,18 @@ export const pasteMiddleware = (
|
||||
}
|
||||
}
|
||||
});
|
||||
slots.afterImport.subscribe(payload => {
|
||||
const afterImportSubscription = slots.afterImport.subscribe(payload => {
|
||||
if (tr && payload.type === 'slice') {
|
||||
tr.pasted();
|
||||
tr.focusPasted();
|
||||
tr.convertToLinkedDoc();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSubscription.unsubscribe();
|
||||
afterImportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
|
||||
map(({ model }) => model)
|
||||
);
|
||||
|
||||
afterImportBlock$
|
||||
const afterImportBlockSubscription = afterImportBlock$
|
||||
.pipe(filter(model => matchModels(model, [DatabaseBlockModel])))
|
||||
.subscribe(model => {
|
||||
Object.keys(model.props.cells).forEach(cellId => {
|
||||
@@ -44,7 +44,7 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
|
||||
// replace LinkedPage pageId with new id in paragraph blocks
|
||||
afterImportBlock$
|
||||
const replaceLinkedPageIdSubscription = afterImportBlock$
|
||||
.pipe(
|
||||
filter(model =>
|
||||
matchModels(model, [ParagraphBlockModel, ListBlockModel])
|
||||
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
|
||||
}
|
||||
});
|
||||
|
||||
afterImportBlock$
|
||||
const replaceSurfaceRefIdSubscription = afterImportBlock$
|
||||
.pipe(filter(model => matchModels(model, [SurfaceRefBlockModel])))
|
||||
.subscribe(model => {
|
||||
const original = model.props.reference;
|
||||
@@ -105,7 +105,7 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
|
||||
// TODO(@fundon): process linked block/element
|
||||
afterImportBlock$
|
||||
const replaceLinkedDocIdSubscription = afterImportBlock$
|
||||
.pipe(
|
||||
filter(model =>
|
||||
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
|
||||
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
|
||||
|
||||
// Before Import
|
||||
|
||||
slots.beforeImport
|
||||
const beforeImportPageSubscription = slots.beforeImport
|
||||
.pipe(filter(payload => payload.type === 'page'))
|
||||
.subscribe(payload => {
|
||||
if (idMap.has(payload.snapshot.meta.id)) {
|
||||
@@ -140,7 +140,7 @@ export const replaceIdMiddleware =
|
||||
payload.snapshot.meta.id = newId;
|
||||
});
|
||||
|
||||
slots.beforeImport
|
||||
const beforeImportBlockSubscription = slots.beforeImport
|
||||
.pipe(
|
||||
filter(
|
||||
(payload): payload is BeforeImportBlockPayload =>
|
||||
@@ -244,4 +244,13 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
replaceLinkedPageIdSubscription.unsubscribe();
|
||||
replaceSurfaceRefIdSubscription.unsubscribe();
|
||||
replaceLinkedDocIdSubscription.unsubscribe();
|
||||
beforeImportPageSubscription.unsubscribe();
|
||||
beforeImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,33 +5,42 @@ export const surfaceRefToEmbed =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
let pageId: string | null = null;
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
}
|
||||
}
|
||||
});
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
// only handle surface-ref block snapshot
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.snapshot.flavour !== 'affine:surface-ref'
|
||||
)
|
||||
return;
|
||||
);
|
||||
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
// only handle surface-ref block snapshot
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.snapshot.flavour !== 'affine:surface-ref'
|
||||
)
|
||||
return;
|
||||
|
||||
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
|
||||
const isNotSameDoc = pageId !== std.store.doc.id;
|
||||
if (pageId && isNotSameDoc) {
|
||||
// The blockId of the original surface-ref block
|
||||
const blockId = payload.snapshot.id;
|
||||
payload.snapshot.id = std.workspace.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
pageId,
|
||||
params: {
|
||||
mode: 'page',
|
||||
blockIds: [blockId],
|
||||
},
|
||||
};
|
||||
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
|
||||
const isNotSameDoc = pageId !== std.store.doc.id;
|
||||
if (pageId && isNotSameDoc) {
|
||||
// The blockId of the original surface-ref block
|
||||
const blockId = payload.snapshot.id;
|
||||
payload.snapshot.id = std.workspace.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
pageId,
|
||||
params: {
|
||||
mode: 'page',
|
||||
blockIds: [blockId],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
beforeImportSliceSubscription.unsubscribe();
|
||||
beforeImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
|
||||
export const titleMiddleware =
|
||||
(metas: DocMeta[]): TransformerMiddleware =>
|
||||
({ slots, adapterConfigs }) => {
|
||||
slots.beforeExport.subscribe(() => {
|
||||
const beforeExportSubscription = slots.beforeExport.subscribe(() => {
|
||||
for (const meta of metas) {
|
||||
adapterConfigs.set('title:' + meta.id, meta.title);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export const uploadMiddleware = (
|
||||
}
|
||||
}
|
||||
|
||||
blockView$
|
||||
const blockViewSubscription = blockView$
|
||||
.pipe(
|
||||
map(payload => {
|
||||
if (assetsManager.uploadingAssetsMap.size === 0) return null;
|
||||
@@ -110,5 +110,9 @@ export const uploadMiddleware = (
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
blockViewSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,6 +11,9 @@ const ALLOWED_SCHEMES = new Set([
|
||||
// https://publicsuffix.org/
|
||||
const TLD_REGEXP = /(?:\.[a-zA-Z]+)?(\.[a-zA-Z]{2,})$/;
|
||||
|
||||
const IPV4_ADDR_REGEXP =
|
||||
/^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
|
||||
|
||||
const toURL = (str: string) => {
|
||||
try {
|
||||
if (!URL.canParse(str)) return null;
|
||||
@@ -21,16 +24,20 @@ const toURL = (str: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
function resolveURL(str: string) {
|
||||
function resolveURL(str: string, baseUrl: string, padded = false) {
|
||||
const url = toURL(str);
|
||||
if (!url) return null;
|
||||
|
||||
const protocol = url.protocol.substring(0, url.protocol.length - 1);
|
||||
const hostname = url.hostname;
|
||||
const origin = url.origin;
|
||||
|
||||
let allowed = ALLOWED_SCHEMES.has(protocol);
|
||||
if (allowed && hostname.includes('.')) {
|
||||
allowed = TLD_REGEXP.test(hostname);
|
||||
allowed =
|
||||
origin === baseUrl ||
|
||||
TLD_REGEXP.test(hostname) ||
|
||||
(padded ? false : IPV4_ADDR_REGEXP.test(hostname));
|
||||
}
|
||||
|
||||
return { url, allowed };
|
||||
@@ -68,10 +75,10 @@ export function normalizeUrl(str: string) {
|
||||
*
|
||||
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
|
||||
*/
|
||||
export function isValidUrl(str: string) {
|
||||
export function isValidUrl(str: string, baseUrl = location.origin) {
|
||||
str = str.trim();
|
||||
|
||||
let result = resolveURL(str);
|
||||
let result = resolveURL(str, baseUrl);
|
||||
|
||||
if (result && !result.allowed) return false;
|
||||
|
||||
@@ -80,7 +87,7 @@ export function isValidUrl(str: string) {
|
||||
if (!hasScheme) {
|
||||
const dotIdx = str.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx < str.length - 1) {
|
||||
result = resolveURL(`https://${str}`);
|
||||
result = resolveURL(`https://${str}`, baseUrl, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const gfxBlocksFilter = (
|
||||
}
|
||||
|
||||
return ({ slots, transformerConfigs }) => {
|
||||
slots.beforeExport.subscribe(payload => {
|
||||
const beforeExportSubscription = slots.beforeExport.subscribe(payload => {
|
||||
if (payload.type !== 'block') {
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const gfxBlocksFilter = (
|
||||
}
|
||||
});
|
||||
|
||||
slots.afterExport.subscribe(payload => {
|
||||
const afterExportSubscription = slots.afterExport.subscribe(payload => {
|
||||
if (payload.type !== 'block') {
|
||||
return;
|
||||
}
|
||||
@@ -110,5 +110,10 @@ export const gfxBlocksFilter = (
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeExportSubscription.unsubscribe();
|
||||
afterExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
EmbedGithubModel,
|
||||
EmbedLinkedDocModel,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const cardStyleUpdater =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type !== 'block' || !payload.parent) return;
|
||||
const parentModel = std.store.getModelById(payload.parent);
|
||||
if (!matchModels(parentModel, [NoteBlockModel])) return;
|
||||
|
||||
// TODO(@L-Sun): Refactor this after refactor `store.moveBlocks`
|
||||
// Currently, drag a block will use store.moveBlocks to update the tree of blocks
|
||||
// but the instance of it is not changed.
|
||||
// So change the style of snapshot.props in the middleware is not working.
|
||||
// Instead, we can change the style of the model instance in the middleware,
|
||||
const model = std.store.getModelById(payload.snapshot.id);
|
||||
if (!model) return;
|
||||
|
||||
if (model instanceof AttachmentBlockModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontalThin',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof BookmarkBlockModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof EmbedGithubModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof EmbedLinkedDocModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,36 +9,45 @@ export const newIdCrossDoc =
|
||||
let samePage = false;
|
||||
const oldToNewIdMap = new Map<string, string>();
|
||||
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.store.id;
|
||||
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.store.id;
|
||||
}
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
const newId = std.workspace.idGenerator();
|
||||
|
||||
oldToNewIdMap.set(payload.snapshot.id, newId);
|
||||
payload.snapshot.id = newId;
|
||||
}
|
||||
}
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
const newId = std.workspace.idGenerator();
|
||||
);
|
||||
|
||||
oldToNewIdMap.set(payload.snapshot.id, newId);
|
||||
payload.snapshot.id = newId;
|
||||
const afterImportBlockSubscription = slots.afterImport.subscribe(
|
||||
payload => {
|
||||
if (
|
||||
!samePage &&
|
||||
payload.type === 'block' &&
|
||||
matchModels(payload.model, [DatabaseBlockModel])
|
||||
) {
|
||||
const originalCells = payload.model.props.cells;
|
||||
const newCells = {
|
||||
...originalCells,
|
||||
};
|
||||
|
||||
Object.keys(originalCells).forEach(cellId => {
|
||||
if (oldToNewIdMap.has(cellId)) {
|
||||
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
|
||||
}
|
||||
});
|
||||
|
||||
payload.model.props.cells$.value = newCells;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (
|
||||
!samePage &&
|
||||
payload.type === 'block' &&
|
||||
matchModels(payload.model, [DatabaseBlockModel])
|
||||
) {
|
||||
const originalCells = payload.model.props.cells;
|
||||
const newCells = {
|
||||
...originalCells,
|
||||
};
|
||||
|
||||
Object.keys(originalCells).forEach(cellId => {
|
||||
if (oldToNewIdMap.has(cellId)) {
|
||||
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
|
||||
}
|
||||
});
|
||||
|
||||
payload.model.props.cells$.value = newCells;
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
beforeImportSliceSubscription.unsubscribe();
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,19 +7,25 @@ import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
export const reorderList =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchModels(model, [ListBlockModel]) &&
|
||||
model.props.type === 'numbered'
|
||||
) {
|
||||
const next = std.store.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.store, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.store, next);
|
||||
const afterImportBlockSubscription = slots.afterImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchModels(model, [ListBlockModel]) &&
|
||||
model.props.type === 'numbered'
|
||||
) {
|
||||
const next = std.store.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.store, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.store, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,6 +76,7 @@ import last from 'lodash-es/last';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import { PreviewHelper } from '../helpers/preview-helper.js';
|
||||
import { gfxBlocksFilter } from '../middleware/blocks-filter.js';
|
||||
import { cardStyleUpdater } from '../middleware/card-style-updater.js';
|
||||
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
|
||||
import { reorderList } from '../middleware/reorder-list';
|
||||
import {
|
||||
@@ -1433,6 +1434,7 @@ export class DragEventWatcher {
|
||||
newIdCrossDoc(std),
|
||||
reorderList(std),
|
||||
surfaceRefToEmbed(std),
|
||||
cardStyleUpdater(std),
|
||||
];
|
||||
|
||||
if (selectedIds) {
|
||||
|
||||
@@ -374,6 +374,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize' | 'rotate';
|
||||
angle: number;
|
||||
handle: ResizeHandle;
|
||||
flipX?: boolean;
|
||||
flipY?: boolean;
|
||||
pure?: boolean;
|
||||
}) => {
|
||||
if (!options) {
|
||||
@@ -381,8 +383,25 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const { type, angle, handle } = options;
|
||||
const { type, angle, flipX, flipY } = options;
|
||||
let cursor: CursorType = 'default';
|
||||
let handle: ResizeHandle = options.handle;
|
||||
|
||||
if (flipX) {
|
||||
handle = (
|
||||
handle.includes('left')
|
||||
? handle.replace('left', 'right')
|
||||
: handle.replace('right', 'left')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (flipY) {
|
||||
handle = (
|
||||
handle.includes('top')
|
||||
? handle.replace('top', 'bottom')
|
||||
: handle.replace('bottom', 'top')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (type === 'rotate') {
|
||||
cursor = generateCursorUrl(angle, handle);
|
||||
@@ -626,7 +645,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
onResizeStart: () => {
|
||||
this._mode = 'resize';
|
||||
},
|
||||
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
|
||||
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
|
||||
if (lockRatio) {
|
||||
this._scaleDirection = handle;
|
||||
this._scalePercent = `${Math.round(scaleX * 100)}%`;
|
||||
@@ -642,6 +661,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize',
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
handle,
|
||||
flipX: scaleX < 0,
|
||||
flipY: scaleY < 0,
|
||||
});
|
||||
},
|
||||
onResizeEnd: () => {
|
||||
@@ -652,6 +673,14 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
}
|
||||
},
|
||||
option => {
|
||||
if (
|
||||
['resize', 'rotate'].includes(
|
||||
interaction.activeInteraction$.value?.type ?? ''
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this._updateCursor({
|
||||
...option,
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { type FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import type { AffineFrameTitle } from './frame-title.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
|
||||
private get _frames() {
|
||||
return Object.values(this.store.blocks.value)
|
||||
.map(({ model }) => model)
|
||||
.filter(model => model instanceof FrameBlockModel);
|
||||
}
|
||||
|
||||
getFrameTitle(frame: FrameBlockModel | string) {
|
||||
const id = typeof frame === 'string' ? frame : frame.id;
|
||||
const frameTitle = this.shadowRoot?.querySelector(
|
||||
`affine-frame-title[data-id="${id}"]`
|
||||
) as AffineFrameTitle | null;
|
||||
return frameTitle;
|
||||
}
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<FrameBlockModel> {
|
||||
override render() {
|
||||
return repeat(
|
||||
this._frames,
|
||||
({ id }) => id,
|
||||
frame =>
|
||||
html`<affine-frame-title
|
||||
.model=${frame}
|
||||
data-id=${frame.id}
|
||||
></affine-frame-title>`
|
||||
);
|
||||
return html`<affine-frame-title
|
||||
.model=${this.model}
|
||||
data-id=${this.model.id}
|
||||
></affine-frame-title>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const frameTitleWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
'affine:frame',
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
type AffineFrameTitleWidget,
|
||||
} from './affine-frame-title-widget';
|
||||
import type { AffineFrameTitle } from './frame-title';
|
||||
import { frameTitleStyleVars } from './styles';
|
||||
|
||||
export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
@@ -135,12 +136,13 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
|
||||
const frameTitleWidget = this.edgeless.std.view.getWidget(
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
rootBlockId
|
||||
this.frameModel.id
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
if (!frameTitleWidget) return nothing;
|
||||
|
||||
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
|
||||
const frameTitle =
|
||||
frameTitleWidget.querySelector<AffineFrameTitle>('affine-frame-title');
|
||||
|
||||
const colors = frameTitle?.colors ?? {
|
||||
background: cssVarV2('edgeless/frame/background/white'),
|
||||
|
||||
@@ -142,12 +142,10 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
}px)`,
|
||||
];
|
||||
|
||||
const anchor = this.gfx.viewport.toViewCoord(bound.x, bound.y);
|
||||
|
||||
this.style.display = '';
|
||||
this.style.setProperty('--bg-color', this.colors.background);
|
||||
this.style.left = `${anchor[0]}px`;
|
||||
this.style.top = `${anchor[1]}px`;
|
||||
this.style.left = '0px';
|
||||
this.style.top = '0px';
|
||||
this.style.display = hidden ? 'none' : 'flex';
|
||||
this.style.transform = transformOperation.join(' ');
|
||||
this.style.maxWidth = `${maxWidth}px`;
|
||||
@@ -205,18 +203,6 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'click', evt => {
|
||||
if (evt.shiftKey) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'dblclick', () => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');
|
||||
|
||||
@@ -566,23 +566,29 @@ Optional flag to insert before sibling
|
||||
|
||||
### updateBlock()
|
||||
|
||||
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
|
||||
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
|
||||
|
||||
Updates a block's properties or executes a callback in a transaction
|
||||
|
||||
#### Type Parameters
|
||||
|
||||
##### T
|
||||
|
||||
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### modelOrId
|
||||
|
||||
The block model or block ID to update
|
||||
|
||||
`string` | `BlockModel`\<`object`\>
|
||||
`string` | `T`
|
||||
|
||||
##### callBackOrProps
|
||||
|
||||
Either a callback function to execute or properties to update
|
||||
|
||||
`Partial`\<`BlockProps`\> | () => `void`
|
||||
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
|
||||
|
||||
#### Returns
|
||||
|
||||
|
||||
@@ -947,23 +947,34 @@ export class InteractivityManager extends GfxExtension {
|
||||
...options,
|
||||
lockRatio,
|
||||
elements,
|
||||
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
|
||||
onResizeMove: ({
|
||||
scaleX,
|
||||
scaleY,
|
||||
originalBound,
|
||||
handleSign,
|
||||
handlePos,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
}) => {
|
||||
const suggested: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
priority?: number;
|
||||
}[] = [];
|
||||
const suggest = (distance: { dx: number; dy: number }) => {
|
||||
const suggest = (distance: { scaleX: number; scaleY: number }) => {
|
||||
suggested.push(distance);
|
||||
};
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeMove?.({
|
||||
dx,
|
||||
dy,
|
||||
scaleX,
|
||||
scaleY,
|
||||
elements,
|
||||
handleSign,
|
||||
handle,
|
||||
handleSign,
|
||||
handlePos,
|
||||
originalBound,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
suggest,
|
||||
});
|
||||
@@ -973,9 +984,9 @@ export class InteractivityManager extends GfxExtension {
|
||||
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||
});
|
||||
|
||||
return last(suggested) ?? { dx, dy };
|
||||
return last(suggested) ?? { scaleX, scaleY };
|
||||
},
|
||||
onResizeStart: ({ data }) => {
|
||||
onResizeStart: ({ handleSign, handlePos, data }) => {
|
||||
this.activeInteraction$.value = {
|
||||
type: 'resize',
|
||||
elements,
|
||||
@@ -984,6 +995,8 @@ export class InteractivityManager extends GfxExtension {
|
||||
ext.onResizeStart?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1045,13 +1058,15 @@ export class InteractivityManager extends GfxExtension {
|
||||
|
||||
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
|
||||
},
|
||||
onResizeEnd: ({ data }) => {
|
||||
onResizeEnd: ({ handleSign, handlePos, data }) => {
|
||||
this.activeInteraction$.value = null;
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeEnd?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
options.onResizeEnd?.();
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Bound,
|
||||
getCommonBoundWithRotation,
|
||||
type IBound,
|
||||
type IPoint,
|
||||
type IVec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
|
||||
@@ -29,7 +30,7 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
|
||||
'bottom',
|
||||
];
|
||||
|
||||
type ElementInitialSnapshot = Readonly<Required<IBound>>;
|
||||
type ReadonlyIBound = Readonly<Required<IBound>>;
|
||||
|
||||
export interface OptionResize {
|
||||
elements: GfxModel[];
|
||||
@@ -37,16 +38,18 @@ export interface OptionResize {
|
||||
lockRatio: boolean;
|
||||
event: PointerEvent;
|
||||
onResizeMove: (payload: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
originalBound: IBound;
|
||||
|
||||
handleSign: IPoint;
|
||||
|
||||
handlePos: IVec;
|
||||
currentHandlePos: IVec;
|
||||
|
||||
lockRatio: boolean;
|
||||
}) => { dx: number; dy: number };
|
||||
}) => { scaleX: number; scaleY: number };
|
||||
onResizeUpdate: (payload: {
|
||||
lockRatio: boolean;
|
||||
scaleX: number;
|
||||
@@ -59,8 +62,16 @@ export interface OptionResize {
|
||||
matrix: DOMMatrix;
|
||||
}[];
|
||||
}) => void;
|
||||
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeStart?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
onResizeEnd?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export type RotateOption = {
|
||||
@@ -95,11 +106,102 @@ export class ResizeController {
|
||||
this.gfx = option.gfx;
|
||||
}
|
||||
|
||||
getCoordsTransform(originalBound: IBound, handle: ResizeHandle) {
|
||||
const { x: xSign, y: ySign } = this.getHandleSign(handle);
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(
|
||||
originalBound.w / 2 + originalBound.x,
|
||||
originalBound.h / 2 + originalBound.y
|
||||
)
|
||||
.rotate(-(originalBound.rotate ?? 0))
|
||||
.translate(
|
||||
-(originalBound.w / 2 + originalBound.x),
|
||||
-(originalBound.h / 2 + originalBound.y)
|
||||
);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean = false) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handlePos = toModel(
|
||||
new DOMPoint(originalBound.w * xSign, originalBound.h * ySign)
|
||||
);
|
||||
|
||||
return {
|
||||
xSign,
|
||||
ySign,
|
||||
originalBound,
|
||||
toLocalM,
|
||||
toLocalRotatedM,
|
||||
toLocal,
|
||||
toModel,
|
||||
handlePos: [handlePos.x, handlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleFromDelta(
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
delta: { dx: number; dy: number },
|
||||
handleStartPos: IVec,
|
||||
lockRatio: boolean
|
||||
) {
|
||||
const { originalBound, xSign, ySign, toModel, toLocal } = transform;
|
||||
const currentPos = toLocal(
|
||||
new DOMPoint(handleStartPos[0] + delta.dx, handleStartPos[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign ? currentPos.x / (originalBound.w * xSign) : 1;
|
||||
let scaleY = ySign ? currentPos.y / (originalBound.h * ySign) : 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const finalHandlePos = toModel(
|
||||
new DOMPoint(
|
||||
originalBound.w * xSign * scaleX,
|
||||
originalBound.h * ySign * scaleY
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: [finalHandlePos.x, finalHandlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleMatrix(
|
||||
{ scaleX, scaleY }: { scaleX: number; scaleY: number },
|
||||
lockRatio: boolean
|
||||
) {
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
scaleM: new DOMMatrix().scaleSelf(scaleX, scaleY),
|
||||
};
|
||||
}
|
||||
|
||||
startResize(options: OptionResize) {
|
||||
const {
|
||||
elements,
|
||||
handle,
|
||||
lockRatio,
|
||||
onResizeStart,
|
||||
onResizeMove,
|
||||
onResizeUpdate,
|
||||
@@ -107,19 +209,32 @@ export class ResizeController {
|
||||
event,
|
||||
} = options;
|
||||
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
h: el.h,
|
||||
rotate: el.rotate,
|
||||
}));
|
||||
const originalBound = getCommonBoundWithRotation(originals);
|
||||
const originalBound: IBound =
|
||||
originals.length > 1
|
||||
? getCommonBoundWithRotation(originals)
|
||||
: {
|
||||
x: originals[0].x,
|
||||
y: originals[0].y,
|
||||
w: originals[0].w,
|
||||
h: originals[0].h,
|
||||
rotate: originals[0].rotate,
|
||||
};
|
||||
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
]);
|
||||
const handleSign = this.getHandleSign(handle);
|
||||
const transform = this.getCoordsTransform(originalBound, handle);
|
||||
const handleSign = {
|
||||
x: transform.xSign,
|
||||
y: transform.ySign,
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
@@ -130,45 +245,69 @@ export class ResizeController {
|
||||
dx: currPt[0] - startPt[0],
|
||||
dy: currPt[1] - startPt[1],
|
||||
};
|
||||
const shouldLockRatio = lockRatio || e.shiftKey;
|
||||
const shouldLockRatio =
|
||||
options.lockRatio || e.shiftKey || elements.length > 1;
|
||||
const {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: currentHandlePos,
|
||||
} = this.getScaleFromDelta(
|
||||
transform,
|
||||
delta,
|
||||
transform.handlePos,
|
||||
shouldLockRatio
|
||||
);
|
||||
|
||||
const scale = onResizeMove({
|
||||
scaleX,
|
||||
scaleY,
|
||||
|
||||
originalBound,
|
||||
|
||||
delta = onResizeMove({
|
||||
dx: delta.dx,
|
||||
dy: delta.dy,
|
||||
handleSign,
|
||||
|
||||
handlePos: transform.handlePos,
|
||||
currentHandlePos,
|
||||
|
||||
lockRatio: shouldLockRatio,
|
||||
});
|
||||
const scaleInfo = this.getScaleMatrix(scale, shouldLockRatio);
|
||||
|
||||
if (elements.length === 1) {
|
||||
this.resizeSingle(
|
||||
originals[0],
|
||||
elements[0],
|
||||
shouldLockRatio,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
transform,
|
||||
scaleInfo,
|
||||
onResizeUpdate
|
||||
);
|
||||
} else {
|
||||
this.resizeMulti(
|
||||
originalBound,
|
||||
originals,
|
||||
elements,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
transform,
|
||||
scaleInfo,
|
||||
onResizeUpdate
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onResizeStart?.({ data: elements.map(model => ({ model })) });
|
||||
onResizeStart?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
|
||||
const onPointerUp = () => {
|
||||
this.host.removeEventListener('pointermove', onPointerMove);
|
||||
this.host.removeEventListener('pointerup', onPointerUp);
|
||||
|
||||
onResizeEnd?.({ data: elements.map(model => ({ model })) });
|
||||
onResizeEnd?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
};
|
||||
|
||||
this.host.addEventListener('pointermove', onPointerMove);
|
||||
@@ -176,55 +315,15 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeSingle(
|
||||
orig: ElementInitialSnapshot,
|
||||
orig: ReadonlyIBound,
|
||||
model: GfxModel,
|
||||
lockRatio: boolean,
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
transform: ReturnType<typeof ResizeController.prototype.getCoordsTransform>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { xSign, ySign } = handleSign;
|
||||
|
||||
const pivot = new DOMPoint(
|
||||
orig.x + (-xSign === 1 ? orig.w : 0),
|
||||
orig.y + (-ySign === 1 ? orig.h : 0)
|
||||
);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(orig.w / 2 + orig.x, orig.h / 2 + orig.y)
|
||||
.rotate(-orig.rotate)
|
||||
.translate(-(orig.w / 2 + orig.x), -(orig.h / 2 + orig.y));
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
|
||||
const currPtLocal = toLocal(
|
||||
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + orig.h) / orig.h
|
||||
: 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
const { toLocalM, toLocalRotatedM, toLocal, toModel } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
|
||||
const [visualTopLeft, visualBottomRight] = [
|
||||
new DOMPoint(orig.x, orig.y),
|
||||
@@ -282,45 +381,14 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeMulti(
|
||||
originalBound: Bound,
|
||||
originals: ElementInitialSnapshot[],
|
||||
originals: ReadonlyIBound[],
|
||||
elements: GfxModel[],
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { xSign, ySign } = handleSign;
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||
|
||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
|
||||
const currPtLocal = toLocal(
|
||||
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy)
|
||||
);
|
||||
|
||||
let scaleX = xSign
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + originalBound.w) /
|
||||
originalBound.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + originalBound.h) /
|
||||
originalBound.h
|
||||
: 1;
|
||||
|
||||
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
const { toLocalM } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
|
||||
const data = elements.map((model, i) => {
|
||||
const orig = originals[i];
|
||||
@@ -357,7 +425,7 @@ export class ResizeController {
|
||||
startRotate(option: RotateOption) {
|
||||
const { event, elements, onRotateUpdate } = option;
|
||||
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
@@ -429,7 +497,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateSingle(option: {
|
||||
orig: ElementInitialSnapshot;
|
||||
orig: ReadonlyIBound;
|
||||
model: GfxModel;
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -481,7 +549,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateMulti(option: {
|
||||
origs: ElementInitialSnapshot[];
|
||||
origs: ReadonlyIBound[];
|
||||
models: GfxModel[];
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -567,23 +635,23 @@ export class ResizeController {
|
||||
private getHandleSign(handle: ResizeHandle) {
|
||||
switch (handle) {
|
||||
case 'top-left':
|
||||
return { xSign: -1, ySign: -1 };
|
||||
return { x: -1, y: -1 };
|
||||
case 'top':
|
||||
return { xSign: 0, ySign: -1 };
|
||||
return { x: 0, y: -1 };
|
||||
case 'top-right':
|
||||
return { xSign: 1, ySign: -1 };
|
||||
return { x: 1, y: -1 };
|
||||
case 'right':
|
||||
return { xSign: 1, ySign: 0 };
|
||||
return { x: 1, y: 0 };
|
||||
case 'bottom-right':
|
||||
return { xSign: 1, ySign: 1 };
|
||||
return { x: 1, y: 1 };
|
||||
case 'bottom':
|
||||
return { xSign: 0, ySign: 1 };
|
||||
return { x: 0, y: 1 };
|
||||
case 'bottom-left':
|
||||
return { xSign: -1, ySign: 1 };
|
||||
return { x: -1, y: 1 };
|
||||
case 'left':
|
||||
return { xSign: -1, ySign: 0 };
|
||||
return { x: -1, y: 0 };
|
||||
default:
|
||||
return { xSign: 0, ySign: 0 };
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
|
||||
|
||||
import type { GfxModel } from '../../model/model';
|
||||
import type { ResizeHandle } from '../resize/manager';
|
||||
|
||||
@@ -8,6 +10,16 @@ export type ExtensionElementResizeContext = {
|
||||
export type ExtensionElementResizeStartContext = {
|
||||
elements: GfxModel[];
|
||||
|
||||
/**
|
||||
* The position of the handle in the browser coordinate space.
|
||||
*/
|
||||
handlePos: IVec;
|
||||
|
||||
/**
|
||||
* The sign (or normal vector) of the handle.
|
||||
*/
|
||||
handleSign: IPoint;
|
||||
|
||||
handle: ResizeHandle;
|
||||
};
|
||||
|
||||
@@ -16,15 +28,14 @@ export type ExtensionElementResizeEndContext =
|
||||
|
||||
export type ExtensionElementResizeMoveContext =
|
||||
ExtensionElementResizeStartContext & {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
originalBound: IBound;
|
||||
|
||||
currentHandlePos: IVec;
|
||||
|
||||
lockRatio: boolean;
|
||||
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
|
||||
suggest: (distance: { dx: number; dy: number }) => void;
|
||||
suggest: (distance: { scaleX: number; scaleY: number }) => void;
|
||||
};
|
||||
|
||||
@@ -825,7 +825,7 @@ export class LayerManager extends GfxExtension {
|
||||
const block = store.getModelById(payload.id);
|
||||
|
||||
if (block instanceof GfxBlockElementModel) {
|
||||
this.delete(block as GfxBlockElementModel);
|
||||
this.delete(block);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -834,20 +834,29 @@ export class LayerManager extends GfxExtension {
|
||||
const watchSurface = (surface: SurfaceBlockModel) => {
|
||||
let lastChildMap = new Map(surface.childMap.peek());
|
||||
this._disposable.add(
|
||||
surface.childMap.subscribe(val => {
|
||||
val.forEach((_, id) => {
|
||||
surface.childMap.subscribe(currentChildMap => {
|
||||
currentChildMap.forEach((_, id) => {
|
||||
if (lastChildMap.has(id)) {
|
||||
lastChildMap.delete(id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
lastChildMap.forEach((_, id) => {
|
||||
const block = this._doc.getBlock(id);
|
||||
if (block?.model) {
|
||||
this.delete(block.model as GfxBlockElementModel);
|
||||
const model = this._doc.getModelById(id);
|
||||
if (model instanceof GfxBlockElementModel) {
|
||||
this.delete(model);
|
||||
}
|
||||
});
|
||||
lastChildMap = new Map(val);
|
||||
currentChildMap.forEach((_, id) => {
|
||||
const model = store.getModelById(id);
|
||||
if (
|
||||
model instanceof GfxBlockElementModel &&
|
||||
!this.blocks.includes(model)
|
||||
) {
|
||||
this.add(model);
|
||||
}
|
||||
});
|
||||
lastChildMap = new Map(currentChildMap);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ export type BlockSysProps = {
|
||||
children?: BlockModel[];
|
||||
};
|
||||
export type BlockProps = BlockSysProps & Record<string, unknown>;
|
||||
|
||||
export type PropsOfModel<T> = T extends BlockModel<infer P> ? P : never;
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
type BlockModel,
|
||||
type BlockOptions,
|
||||
type BlockProps,
|
||||
type BlockSysProps,
|
||||
type PropsOfModel,
|
||||
type YBlock,
|
||||
} from '../block/index.js';
|
||||
import { DocCRUD } from './crud.js';
|
||||
@@ -852,9 +854,12 @@ export class Store {
|
||||
*
|
||||
* @category Block CRUD
|
||||
*/
|
||||
updateBlock(
|
||||
modelOrId: BlockModel | string,
|
||||
callBackOrProps: (() => void) | Partial<BlockProps>
|
||||
|
||||
updateBlock<T extends BlockModel = BlockModel>(
|
||||
modelOrId: T | string,
|
||||
callBackOrProps:
|
||||
| (() => void)
|
||||
| Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps>
|
||||
) {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
|
||||
@@ -113,6 +113,8 @@ type TransformerMiddlewareOptions = {
|
||||
transformerConfigs: Map<string, unknown>;
|
||||
};
|
||||
|
||||
type TransformerMiddlewareCleanup = () => void;
|
||||
|
||||
export type TransformerMiddleware = (
|
||||
options: TransformerMiddlewareOptions
|
||||
) => void;
|
||||
) => void | TransformerMiddlewareCleanup;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { nextTick } from '@blocksuite/global/utils';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -67,6 +68,8 @@ export class Transformer {
|
||||
|
||||
private readonly _docCRUD: DocCRUD;
|
||||
|
||||
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
||||
|
||||
private readonly _slots: TransformerSlots = {
|
||||
beforeImport: new Subject<BeforeImportPayload>(),
|
||||
afterImport: new Subject<AfterImportPayload>(),
|
||||
@@ -366,13 +369,16 @@ export class Transformer {
|
||||
this._docCRUD = docCRUD;
|
||||
|
||||
middlewares.forEach(middleware => {
|
||||
middleware({
|
||||
const cleanup = middleware({
|
||||
slots: this._slots,
|
||||
docCRUD: this._docCRUD,
|
||||
assetsManager: this._assetsManager,
|
||||
adapterConfigs: this._adapterConfigs,
|
||||
transformerConfigs: this._transformerConfigs,
|
||||
});
|
||||
if (cleanup) {
|
||||
this._disposables.add(cleanup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,4 +652,9 @@ export class Transformer {
|
||||
reset() {
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this._disposables.dispose();
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,12 +132,13 @@ export class DocEngine {
|
||||
this.logger
|
||||
);
|
||||
|
||||
cleanUp.push(
|
||||
state.mainPeer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
}).unsubscribe
|
||||
);
|
||||
const subscriber = state.mainPeer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
});
|
||||
cleanUp.push(() => {
|
||||
subscriber.unsubscribe();
|
||||
});
|
||||
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
|
||||
@@ -152,12 +153,15 @@ export class DocEngine {
|
||||
this.priorityTarget,
|
||||
this.logger
|
||||
);
|
||||
cleanUp.push(
|
||||
peer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
}).unsubscribe
|
||||
);
|
||||
|
||||
const subscriber = peer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
});
|
||||
cleanUp.push(() => {
|
||||
subscriber.unsubscribe();
|
||||
});
|
||||
|
||||
return peer;
|
||||
});
|
||||
|
||||
|
||||
@@ -31,12 +31,15 @@ describe('frame', () => {
|
||||
);
|
||||
await wait();
|
||||
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
doc.root!.id
|
||||
) as AffineFrameTitleWidget | null;
|
||||
const getFrameTitle = (frameId: string) => {
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
frameId
|
||||
) as AffineFrameTitleWidget | null;
|
||||
return frameTitleWidget?.shadowRoot?.querySelector('affine-frame-title');
|
||||
};
|
||||
|
||||
const frameTitle = frameTitleWidget?.getFrameTitle(frame);
|
||||
const frameTitle = getFrameTitle(frame);
|
||||
const rect = frameTitle?.getBoundingClientRect();
|
||||
|
||||
expect(frameTitle).toBeTruthy();
|
||||
@@ -58,7 +61,7 @@ describe('frame', () => {
|
||||
);
|
||||
await wait();
|
||||
|
||||
const nestedTitle = frameTitleWidget?.getFrameTitle(nestedFrame);
|
||||
const nestedTitle = getFrameTitle(nestedFrame);
|
||||
expect(nestedTitle).toBeTruthy();
|
||||
if (!nestedTitle) return;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
fill: '#ff0000',
|
||||
stroke: '#000000',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector(
|
||||
@@ -73,7 +73,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
subType: 'ellipse',
|
||||
xywh: '[200, 200, 50, 50]',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
@@ -91,4 +91,48 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
expect(shapeElement).toBeNull();
|
||||
});
|
||||
|
||||
test('should correctly render diamond shape', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
const shapeProps = {
|
||||
type: 'shape',
|
||||
subType: 'diamond',
|
||||
xywh: '[150, 150, 80, 60]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
|
||||
test('should correctly render triangle shape', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
const shapeProps = {
|
||||
type: 'shape',
|
||||
subType: 'triangle',
|
||||
xywh: '[150, 150, 80, 60]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('basic', () => {
|
||||
xywh: '[100, 0, 100, 100]',
|
||||
index: service.generateIndex(),
|
||||
})!;
|
||||
await wait(0); // wait next frame
|
||||
frameId = service.crud.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
|
||||
@@ -7,30 +7,40 @@ import type { InitFn } from './utils.js';
|
||||
const presetMarkdown = `Click the 🔁 button to switch between editors dynamically - they are fully compatible!`;
|
||||
|
||||
export const preset: InitFn = async (collection: Workspace, id: string) => {
|
||||
const doc = collection.createDoc(id).getStore({ id });
|
||||
doc.load();
|
||||
// Add root block and surface block at root level
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new Text('BlockSuite Playground'),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
let doc = collection.getDoc(id);
|
||||
const hasDoc = !!doc;
|
||||
if (!doc) {
|
||||
doc = collection.createDoc(id);
|
||||
}
|
||||
|
||||
// Add note block inside root block
|
||||
const noteId = doc.addBlock(
|
||||
'affine:note',
|
||||
{ xywh: '[0, 100, 800, 640]' },
|
||||
rootId
|
||||
);
|
||||
const store = doc.getStore({ id });
|
||||
store.load();
|
||||
|
||||
// Import preset markdown content inside note block
|
||||
await MarkdownTransformer.importMarkdownToBlock({
|
||||
doc,
|
||||
blockId: noteId,
|
||||
markdown: presetMarkdown,
|
||||
extensions: getTestStoreManager().get('store'),
|
||||
});
|
||||
// Run only once on all clients.
|
||||
let noteId: string;
|
||||
if (!hasDoc) {
|
||||
// Add root block and surface block at root level
|
||||
const rootId = store.addBlock('affine:page', {
|
||||
title: new Text('BlockSuite Playground'),
|
||||
});
|
||||
store.addBlock('affine:surface', {}, rootId);
|
||||
|
||||
doc.resetHistory();
|
||||
// Add note block inside root block
|
||||
noteId = store.addBlock(
|
||||
'affine:note',
|
||||
{ xywh: '[0, 100, 800, 640]' },
|
||||
rootId
|
||||
);
|
||||
// Import preset markdown content inside note block
|
||||
await MarkdownTransformer.importMarkdownToBlock({
|
||||
doc: store,
|
||||
blockId: noteId,
|
||||
markdown: presetMarkdown,
|
||||
extensions: getTestStoreManager().get('store'),
|
||||
});
|
||||
}
|
||||
|
||||
store.resetHistory();
|
||||
};
|
||||
|
||||
preset.id = 'preset';
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@types/katex": "^0.16.7",
|
||||
"browser-fs-access": "^0.37.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.11",
|
||||
"lit": "^3.2.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@import '@toeverything/theme/style.css';
|
||||
@import '@toeverything/theme/fonts.css';
|
||||
|
||||
@import 'katex/dist/katex.min.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'color-emoji';
|
||||
src:
|
||||
|
||||
12
packages/backend/native/index.d.ts
vendored
12
packages/backend/native/index.d.ts
vendored
@@ -8,6 +8,11 @@ export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
|
||||
|
||||
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||
|
||||
export interface Chunk {
|
||||
index: number
|
||||
content: string
|
||||
}
|
||||
|
||||
export declare function fromModelName(modelName: string): Tokenizer | null
|
||||
|
||||
export declare function getMime(input: Uint8Array): string
|
||||
@@ -22,6 +27,11 @@ export declare function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
|
||||
|
||||
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
|
||||
|
||||
export declare function parseDoc(filePath: string, doc: Buffer): Promise<{ name: string, chunks: Array<{index: number, content: string}> }>
|
||||
export interface ParsedDoc {
|
||||
name: string
|
||||
chunks: Array<Chunk>
|
||||
}
|
||||
|
||||
export declare function parseDoc(filePath: string, doc: Buffer): Promise<ParsedDoc>
|
||||
|
||||
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.78",
|
||||
"@napi-rs/cli": "3.0.0-alpha.81",
|
||||
"lib0": "^0.2.99",
|
||||
"tiktoken": "^1.0.17",
|
||||
"tinybench": "^4.0.0",
|
||||
|
||||
@@ -2,9 +2,21 @@ use affine_common::doc_loader::Doc;
|
||||
use napi::{
|
||||
anyhow::anyhow,
|
||||
bindgen_prelude::{AsyncTask, Buffer},
|
||||
Env, JsObject, Result, Task,
|
||||
Env, Result, Task,
|
||||
};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct Chunk {
|
||||
pub index: i64,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ParsedDoc {
|
||||
pub name: String,
|
||||
pub chunks: Vec<Chunk>,
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
inner: Doc,
|
||||
}
|
||||
@@ -14,24 +26,20 @@ impl Document {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
fn chunks(&self, env: Env) -> Result<JsObject> {
|
||||
let mut array = env.create_array_with_length(self.inner.chunks.len())?;
|
||||
for (i, chunk) in self.inner.chunks.iter().enumerate() {
|
||||
let content = crate::utils::clean_content(&chunk.content);
|
||||
|
||||
let mut obj = env.create_object()?;
|
||||
obj.set_named_property("index", i as i64)?;
|
||||
obj.set_named_property("content", content)?;
|
||||
array.set_element(i as u32, obj)?;
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn resolve(self, env: Env) -> Result<JsObject> {
|
||||
let mut obj = env.create_object()?;
|
||||
obj.set_named_property("name", self.name())?;
|
||||
obj.set_named_property("chunks", self.chunks(env)?)?;
|
||||
Ok(obj)
|
||||
fn chunks(&self) -> Vec<Chunk> {
|
||||
self
|
||||
.inner
|
||||
.chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let content = crate::utils::clean_content(&chunk.content);
|
||||
Chunk {
|
||||
index: i as i64,
|
||||
content,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Chunk>>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,21 +51,22 @@ pub struct AsyncParseDocResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncParseDocResponse {
|
||||
type Output = Document;
|
||||
type JsValue = JsObject;
|
||||
type JsValue = ParsedDoc;
|
||||
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
let doc = Doc::new(&self.file_path, &self.doc).map_err(|e| anyhow!(e))?;
|
||||
Ok(Document { inner: doc })
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, doc: Document) -> Result<Self::JsValue> {
|
||||
doc.resolve(env)
|
||||
fn resolve(&mut self, _: Env, doc: Document) -> Result<Self::JsValue> {
|
||||
Ok(ParsedDoc {
|
||||
name: doc.name(),
|
||||
chunks: doc.chunks(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(
|
||||
ts_return_type = "Promise<{ name: string, chunks: Array<{index: number, content: string}> }>"
|
||||
)]
|
||||
#[napi]
|
||||
pub fn parse_doc(file_path: String, doc: Buffer) -> AsyncTask<AsyncParseDocResponse> {
|
||||
AsyncTask::new(AsyncParseDocResponse {
|
||||
file_path,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
@@ -13,7 +13,7 @@ pub struct AsyncVerifyChallengeResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = JsBoolean;
|
||||
type JsValue = bool;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
@@ -23,8 +23,8 @@ impl Task for AsyncVerifyChallengeResponse {
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
fn resolve(&mut self, _: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,14 +49,14 @@ pub struct AsyncMintChallengeResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = JsString;
|
||||
type JsValue = String;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
fn resolve(&mut self, _: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_files" ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_files') THEN
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_files"
|
||||
ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@@ -5,16 +5,25 @@
|
||||
- The primary key for the `ai_workspace_file_embeddings` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_embeddings_workspace_id_doc_id_chunk_key";
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_embeddings') AND
|
||||
EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_file_embeddings') THEN
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_embeddings_workspace_id_doc_id_chunk_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_file_embeddings_workspace_id_file_id_chunk_key";
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_file_embeddings_workspace_id_file_id_chunk_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_embeddings" DROP CONSTRAINT "ai_workspace_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_embeddings_pkey" PRIMARY KEY ("workspace_id", "doc_id", "chunk");
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_embeddings"
|
||||
DROP CONSTRAINT "ai_workspace_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_embeddings_pkey" PRIMARY KEY ("workspace_id", "doc_id", "chunk");
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_file_embeddings" DROP CONSTRAINT "ai_workspace_file_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_file_embeddings_pkey" PRIMARY KEY ("workspace_id", "file_id", "chunk");
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_file_embeddings"
|
||||
DROP CONSTRAINT "ai_workspace_file_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_file_embeddings_pkey" PRIMARY KEY ("workspace_id", "file_id", "chunk");
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
"dependencies": {
|
||||
"@affine/reader": "workspace:*",
|
||||
"@affine/server-native": "workspace:*",
|
||||
"@ai-sdk/anthropic": "^1.2.10",
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/google": "^1.2.18",
|
||||
"@ai-sdk/google-vertex": "^2.2.22",
|
||||
"@ai-sdk/openai": "^1.3.21",
|
||||
"@ai-sdk/perplexity": "^1.1.6",
|
||||
"@ai-sdk/google-vertex": "^2.2.23",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@ai-sdk/perplexity": "^1.1.9",
|
||||
"@apollo/server": "^4.11.3",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.779.0",
|
||||
@@ -100,7 +100,7 @@
|
||||
"nanoid": "^5.0.9",
|
||||
"nest-commander": "^3.15.0",
|
||||
"nest-winston": "^1.9.7",
|
||||
"nestjs-cls": "^5.0.0",
|
||||
"nestjs-cls": "^6.0.0",
|
||||
"nodemailer": "^7.0.0",
|
||||
"on-headers": "^1.0.2",
|
||||
"piscina": "^5.0.0-alpha.0",
|
||||
|
||||
@@ -47,5 +47,33 @@ function runPredeployScript() {
|
||||
});
|
||||
}
|
||||
|
||||
function fixFailedMigrations() {
|
||||
console.log('fixing failed migrations.');
|
||||
const maybeFailedMigrations = [
|
||||
'20250521083048_fix_workspace_embedding_chunk_primary_key',
|
||||
];
|
||||
for (const migration of maybeFailedMigrations) {
|
||||
try {
|
||||
execSync(`yarn prisma migrate resolve --rolled-back ${migration}`, {
|
||||
encoding: 'utf-8',
|
||||
env: process.env,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
console.log(`migration [${migration}] has been rolled back.`);
|
||||
} catch (err) {
|
||||
if (
|
||||
err.message.includes(
|
||||
'cannot be rolled back because it is not in a failed state'
|
||||
)
|
||||
) {
|
||||
// migration has been rolled back, skip it
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepare();
|
||||
fixFailedMigrations();
|
||||
runPredeployScript();
|
||||
|
||||
@@ -272,13 +272,68 @@ test('should validate markdown list', t => {
|
||||
// ==================== action ====================
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: 'Should chat with histories',
|
||||
promptName: ['Chat With AFFiNE AI'],
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: `
|
||||
Hi! I’m going to send you a technical term related to real-time collaborative editing (e.g., CRDT, Operational Transformation, OT Composer, etc.). Whenever I send you a term:
|
||||
1. Translate it into Chinese (send me the Chinese version).
|
||||
2. Then translate that Chinese back into English (send me the retranslated English).
|
||||
3. Provide a brief, English-language introduction and context for this concept.
|
||||
4. In that English explanation, annotate any niche terms with their Chinese equivalents in parentheses (for example: “Conflict-Free Replicated Data Type (无冲突复制数据类型)”).
|
||||
5. Finally, give the origin or “term history” (e.g., who introduced it, in which paper or year).
|
||||
|
||||
If you understand, please proceed by explaining the term “CRDT.”
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: `
|
||||
1. **Chinese Translation:**
|
||||
“CRDT” → **无冲突复制数据类型**
|
||||
|
||||
2. **Back-Translation to English:**
|
||||
无冲突复制数据类型 → **Conflict-Free Replicated Data Type**
|
||||
|
||||
3. **English Introduction & Context:**
|
||||
A **Conflict-Free Replicated Data Type (无冲突复制数据类型)** is an abstract data type designed for distributed systems where replicas of shared state may be modified concurrently without requiring coordination. CRDTs allow multiple users or processes to update the same data structure (for example, a shared document in a collaborative editor) at the same time.
|
||||
- **Key Terms (with Chinese equivalents):**
|
||||
- **Replica (副本):** Each node or client maintains its own copy of the data.
|
||||
- **State-based (状态型) vs. Operation-based (操作型):** Two main CRDT classes; state-based CRDTs exchange entire state snapshots occasionally, whereas operation-based CRDTs broadcast only incremental operations.
|
||||
- **Merge Function (合并函数):** A deterministic function that resolves differences between two replicas without conflicts.
|
||||
|
||||
CRDTs enable **eventual consistency (最终一致性)** in real-time collaborative editors by ensuring that, after all updates propagate, every replica converges to the same state, even if operations arrive in different orders. This approach removes the need for a centralized server to resolve conflicts, making offline or peer-to-peer editing possible.
|
||||
|
||||
4. **Origin / Term History:**
|
||||
The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Carlos Baquero, and Marek Zawirski in their 2011 paper titled “Conflict-free Replicated Data Types” (published in the _Stabilization, Safety, and Security of Distributed Systems (SSS)_ conference). They formalized two families of CRDTs—state-based (“Convergent Replicated Data Types” or CvRDTs) and operation-based (“Commutative Replicated Data Types” or CmRDTs)—and proved their convergence properties under asynchronous, unreliable networks.
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: `Thanks! Now please just tell me the **Chinese translation** and the **back-translated English term** that you provided previously for “CRDT.” Do not reprint the full introduction—only those two lines.`,
|
||||
},
|
||||
],
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
assertNotWrappedInCodeBlock(t, result);
|
||||
const lower = result.toLowerCase();
|
||||
t.assert(
|
||||
lower.includes('无冲突复制数据类型') &&
|
||||
lower.includes('conflict-free replicated data type'),
|
||||
'The response should include “无冲突复制数据类型” and “Conflict-Free Replicated Data Type”'
|
||||
);
|
||||
},
|
||||
type: 'text' as const,
|
||||
},
|
||||
{
|
||||
name: 'Should not have citation',
|
||||
promptName: ['Chat With AFFiNE AI'],
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: 'what is ssot',
|
||||
content: 'what is AFFiNE AI?',
|
||||
params: {
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ export class JobQueue {
|
||||
|
||||
const removed = await queue.remove(jobId);
|
||||
if (removed) {
|
||||
this.logger.log(`Job ${jobName} removed from queue ${ns}`);
|
||||
this.logger.log(`Job ${jobName}(id=${jobId}) removed from queue ${ns}`);
|
||||
return job.data.payload;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Snapshot report for `src/base/utils/__tests__/duration.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `duration.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should parse duration strings correctly
|
||||
|
||||
> parser - 1ms
|
||||
|
||||
'{"ms":1}'
|
||||
|
||||
> ms - 1ms
|
||||
|
||||
1
|
||||
|
||||
> parser - 1s
|
||||
|
||||
'{"s":1}'
|
||||
|
||||
> ms - 1s
|
||||
|
||||
1000
|
||||
|
||||
> parser - 1m
|
||||
|
||||
'{"m":1}'
|
||||
|
||||
> ms - 1m
|
||||
|
||||
60000
|
||||
|
||||
> parser - 1h
|
||||
|
||||
'{"h":1}'
|
||||
|
||||
> ms - 1h
|
||||
|
||||
3600000
|
||||
|
||||
> parser - 1d
|
||||
|
||||
'{"d":1}'
|
||||
|
||||
> ms - 1d
|
||||
|
||||
86400000
|
||||
|
||||
> parser - 1w
|
||||
|
||||
'{"w":1}'
|
||||
|
||||
> ms - 1w
|
||||
|
||||
604800000
|
||||
|
||||
> parser - 1M
|
||||
|
||||
'{"M":1}'
|
||||
|
||||
> ms - 1M
|
||||
|
||||
2592000000
|
||||
|
||||
> parser - 1y
|
||||
|
||||
'{"y":1}'
|
||||
|
||||
> ms - 1y
|
||||
|
||||
31536000000
|
||||
|
||||
> parser - 1000ms
|
||||
|
||||
'{"ms":1000}'
|
||||
|
||||
> ms - 1000ms
|
||||
|
||||
1000
|
||||
|
||||
> parser - 60s
|
||||
|
||||
'{"s":60}'
|
||||
|
||||
> ms - 60s
|
||||
|
||||
60000
|
||||
|
||||
> parser - 30m
|
||||
|
||||
'{"m":30}'
|
||||
|
||||
> ms - 30m
|
||||
|
||||
1800000
|
||||
|
||||
> parser - 1h30m
|
||||
|
||||
'{"h":1,"m":30}'
|
||||
|
||||
> ms - 1h30m
|
||||
|
||||
5400000
|
||||
|
||||
> parser - 15d
|
||||
|
||||
'{"d":15}'
|
||||
|
||||
> ms - 15d
|
||||
|
||||
1296000000
|
||||
|
||||
> parser - 1y
|
||||
|
||||
'{"y":1}'
|
||||
|
||||
> ms - 1y
|
||||
|
||||
31536000000
|
||||
|
||||
> parser - 12M
|
||||
|
||||
'{"M":12}'
|
||||
|
||||
> ms - 12M
|
||||
|
||||
31104000000
|
||||
|
||||
> parser - 1y1M1d1h1m1s1ms
|
||||
|
||||
'{"y":1,"M":1,"d":1,"h":1,"m":1,"s":1,"ms":1}'
|
||||
|
||||
> ms - 1y1M1d1h1m1s1ms
|
||||
|
||||
34218061001
|
||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
import test from 'ava';
|
||||
|
||||
import { Due } from '../duration';
|
||||
|
||||
test('should parse duration strings correctly', t => {
|
||||
const testcases = [
|
||||
'1ms',
|
||||
'1s',
|
||||
'1m',
|
||||
'1h',
|
||||
'1d',
|
||||
'1w',
|
||||
'1M',
|
||||
'1y',
|
||||
'1000ms',
|
||||
'60s',
|
||||
'30m',
|
||||
'1h30m',
|
||||
'15d',
|
||||
'1y',
|
||||
'12M',
|
||||
'1y1M1d1h1m1s1ms',
|
||||
];
|
||||
|
||||
for (const str of testcases) {
|
||||
t.snapshot(JSON.stringify(Due.parse(str)), `parser - ${str}`);
|
||||
t.snapshot(Due.ms(str), `ms - ${str}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should calc relative time correctly', t => {
|
||||
const date = new Date();
|
||||
t.is(Due.before('1d', date).getTime(), date.getTime() - 1000 * 60 * 60 * 24);
|
||||
|
||||
const date2 = new Date();
|
||||
t.is(Due.after('1d', date2).getTime(), date2.getTime() + 1000 * 60 * 60 * 24);
|
||||
});
|
||||
78
packages/backend/server/src/base/utils/duration.ts
Normal file
78
packages/backend/server/src/base/utils/duration.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
type DurationUnit = 'd' | 'w' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms';
|
||||
type DurationInput = Partial<Record<DurationUnit, number>>;
|
||||
|
||||
const UnitToSecMap: Record<DurationUnit, number> = {
|
||||
ms: 0.001,
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 3600,
|
||||
d: 24 * 3600,
|
||||
w: 7 * 24 * 3600,
|
||||
M: 30 * 24 * 3600,
|
||||
y: 365 * 24 * 3600,
|
||||
};
|
||||
|
||||
const KnownCharCodeToCharMap: Record<number, DurationUnit> = {
|
||||
100: 'd',
|
||||
119: 'w',
|
||||
77: 'M',
|
||||
121: 'y',
|
||||
104: 'h',
|
||||
109: 'm',
|
||||
115: 's',
|
||||
};
|
||||
|
||||
function parse(str: string): DurationInput {
|
||||
let input: DurationInput = {};
|
||||
|
||||
let acc = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str[i];
|
||||
const code = ch.charCodeAt(0);
|
||||
|
||||
// number [0..9]
|
||||
if (code >= 48 && code <= 57) {
|
||||
acc = acc * 10 + code - 48;
|
||||
} else {
|
||||
let unit = KnownCharCodeToCharMap[code];
|
||||
if (!unit) {
|
||||
throw new Error(`Invalid duration string unit ${ch}`);
|
||||
}
|
||||
|
||||
// look ahead a char for 'ms' checking if unit met 'm'
|
||||
if (unit === 'm' && str[i + 1] === 's') {
|
||||
unit = 'ms';
|
||||
i++;
|
||||
}
|
||||
|
||||
input[unit] = acc;
|
||||
acc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export const Due = {
|
||||
ms: (dueStr: string | DurationInput) => {
|
||||
const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr;
|
||||
return Object.entries(input).reduce((duration, [unit, val]) => {
|
||||
return duration + UnitToSecMap[unit as DurationUnit] * (val || 0) * 1000;
|
||||
}, 0);
|
||||
},
|
||||
s: (dueStr: string | DurationInput) => {
|
||||
const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr;
|
||||
return Object.entries(input).reduce((duration, [unit, val]) => {
|
||||
return duration + UnitToSecMap[unit as DurationUnit] * (val || 0);
|
||||
}, 0);
|
||||
},
|
||||
parse,
|
||||
after: (dueStr: string | number | DurationInput, date?: Date) => {
|
||||
const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr);
|
||||
return new Date((date?.getTime() ?? Date.now()) + timestamp);
|
||||
},
|
||||
before: (dueStr: string | number | DurationInput, date?: Date) => {
|
||||
const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr);
|
||||
return new Date((date?.getTime() ?? Date.now()) - timestamp);
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './duration';
|
||||
export * from './promise';
|
||||
export * from './request';
|
||||
export * from './stream';
|
||||
|
||||
@@ -203,7 +203,9 @@ export class SpaceSyncGateway
|
||||
await client.join(room);
|
||||
}
|
||||
} else {
|
||||
this.event.emit('workspace.embedding', { workspaceId: spaceId });
|
||||
if (spaceType === SpaceType.Workspace) {
|
||||
this.event.emit('workspace.embedding', { workspaceId: spaceId });
|
||||
}
|
||||
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,75 @@
|
||||
import {
|
||||
createOpenAI,
|
||||
type OpenAIProvider as VercelOpenAIProvider,
|
||||
} from '@ai-sdk/openai';
|
||||
import { embedMany, generateObject } from 'ai';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { chunk } from 'lodash-es';
|
||||
|
||||
import { ChunkSimilarity, Embedding } from '../../../models';
|
||||
import { OpenAIConfig } from '../providers/openai';
|
||||
import {
|
||||
CopilotPromptNotFound,
|
||||
CopilotProviderNotSupported,
|
||||
} from '../../../base';
|
||||
import type { ChunkSimilarity, Embedding } from '../../../models';
|
||||
import type { PromptService } from '../prompt';
|
||||
import {
|
||||
type CopilotProvider,
|
||||
type CopilotProviderFactory,
|
||||
type ModelFullConditions,
|
||||
ModelInputType,
|
||||
ModelOutputType,
|
||||
} from '../providers';
|
||||
import {
|
||||
EMBEDDING_DIMENSIONS,
|
||||
EmbeddingClient,
|
||||
getReRankSchema,
|
||||
ReRankResult,
|
||||
type ReRankResult,
|
||||
} from './types';
|
||||
|
||||
const RERANK_MODEL = 'gpt-4.1-mini';
|
||||
const RERANK_PROMPT = 'Rerank results';
|
||||
|
||||
export class OpenAIEmbeddingClient extends EmbeddingClient {
|
||||
readonly #instance: VercelOpenAIProvider;
|
||||
export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
private readonly logger = new Logger(ProductionEmbeddingClient.name);
|
||||
|
||||
constructor(config: OpenAIConfig) {
|
||||
constructor(
|
||||
private readonly providerFactory: CopilotProviderFactory,
|
||||
private readonly prompt: PromptService
|
||||
) {
|
||||
super();
|
||||
this.#instance = createOpenAI({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseUrl,
|
||||
}
|
||||
|
||||
override async configured(): Promise<boolean> {
|
||||
const embedding = await this.providerFactory.getProvider({
|
||||
outputType: ModelOutputType.Embedding,
|
||||
});
|
||||
const result = Boolean(embedding);
|
||||
if (!result) {
|
||||
this.logger.warn(
|
||||
'Copilot embedding client is not configured properly, please check your configuration.'
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getProvider(
|
||||
cond: ModelFullConditions
|
||||
): Promise<CopilotProvider> {
|
||||
const provider = await this.providerFactory.getProvider(cond);
|
||||
if (!provider) {
|
||||
throw new CopilotProviderNotSupported({
|
||||
provider: 'embedding',
|
||||
kind: cond.outputType || 'embedding',
|
||||
});
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
async getEmbeddings(input: string[]): Promise<Embedding[]> {
|
||||
const modelInstance = this.#instance.embedding('text-embedding-3-large', {
|
||||
dimensions: EMBEDDING_DIMENSIONS,
|
||||
const provider = await this.getProvider({
|
||||
outputType: ModelOutputType.Embedding,
|
||||
});
|
||||
this.logger.verbose(`Using provider ${provider.type} for embedding`, input);
|
||||
|
||||
const { embeddings } = await embedMany({
|
||||
model: modelInstance,
|
||||
values: input,
|
||||
});
|
||||
const embeddings = await provider.embedding(
|
||||
{ inputTypes: [ModelInputType.Text] },
|
||||
input,
|
||||
{ dimensions: EMBEDDING_DIMENSIONS }
|
||||
);
|
||||
|
||||
return Array.from(embeddings.entries()).map(([index, embedding]) => ({
|
||||
index,
|
||||
@@ -44,27 +78,6 @@ export class OpenAIEmbeddingClient extends EmbeddingClient {
|
||||
}));
|
||||
}
|
||||
|
||||
private getRelevancePrompt<Chunk extends ChunkSimilarity = ChunkSimilarity>(
|
||||
query: string,
|
||||
embeddings: Chunk[]
|
||||
) {
|
||||
const results = embeddings
|
||||
.map(e => {
|
||||
const targetId = 'docId' in e ? e.docId : 'fileId' in e ? e.fileId : '';
|
||||
// NOTE: not xml, just for the sake of the prompt format
|
||||
return [
|
||||
'<result>',
|
||||
`<targetId>${targetId}</targetId>`,
|
||||
`<chunk>${e.chunk}</chunk>`,
|
||||
`<content>${e.content}</content>`,
|
||||
'</result>',
|
||||
];
|
||||
})
|
||||
.flat()
|
||||
.join('\n');
|
||||
return `Generate a score array based on the search results list to measure the likelihood that the information contained in the search results is useful for the report on the following topic: ${query}\n\nHere are the search results:\n<results>\n${results}\n</results>`;
|
||||
}
|
||||
|
||||
private async getEmbeddingRelevance<
|
||||
Chunk extends ChunkSimilarity = ChunkSimilarity,
|
||||
>(
|
||||
@@ -72,19 +85,36 @@ export class OpenAIEmbeddingClient extends EmbeddingClient {
|
||||
embeddings: Chunk[],
|
||||
signal?: AbortSignal
|
||||
): Promise<ReRankResult> {
|
||||
const prompt = this.getRelevancePrompt(query, embeddings);
|
||||
const modelInstance = this.#instance(RERANK_MODEL);
|
||||
if (!embeddings.length) return [];
|
||||
|
||||
const {
|
||||
object: { ranks },
|
||||
} = await generateObject({
|
||||
model: modelInstance,
|
||||
prompt,
|
||||
schema: getReRankSchema(embeddings.length),
|
||||
maxRetries: 3,
|
||||
abortSignal: signal,
|
||||
});
|
||||
return ranks;
|
||||
const prompt = await this.prompt.get(RERANK_PROMPT);
|
||||
if (!prompt) {
|
||||
throw new CopilotPromptNotFound({ name: RERANK_PROMPT });
|
||||
}
|
||||
const provider = await this.getProvider({ modelId: prompt.model });
|
||||
const schema = getReRankSchema(embeddings.length);
|
||||
|
||||
const ranks = await provider.structure(
|
||||
{ modelId: prompt.model },
|
||||
prompt.finish({
|
||||
query,
|
||||
results: embeddings.map(e => {
|
||||
const targetId =
|
||||
'docId' in e ? e.docId : 'fileId' in e ? e.fileId : '';
|
||||
return { targetId, chunk: e.chunk, content: e.content };
|
||||
}),
|
||||
schema,
|
||||
}),
|
||||
{ maxRetries: 3, signal }
|
||||
);
|
||||
|
||||
try {
|
||||
return schema.parse(JSON.parse(ranks)).ranks;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to parse rerank results', error);
|
||||
// silent error, will fallback to default sorting in parent method
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
override async reRank<Chunk extends ChunkSimilarity = ChunkSimilarity>(
|
||||
@@ -110,6 +140,10 @@ export class OpenAIEmbeddingClient extends EmbeddingClient {
|
||||
const ranks = [];
|
||||
for (const c of chunk(sortedEmbeddings, Math.min(topK, 10))) {
|
||||
const rank = await this.getEmbeddingRelevance(query, c, signal);
|
||||
if (c.length !== rank.length) {
|
||||
// llm return wrong result, fallback to default sorting
|
||||
return super.reRank(query, embeddings, topK, signal);
|
||||
}
|
||||
ranks.push(rank);
|
||||
}
|
||||
|
||||
@@ -124,6 +158,21 @@ export class OpenAIEmbeddingClient extends EmbeddingClient {
|
||||
}
|
||||
}
|
||||
|
||||
let EMBEDDING_CLIENT: EmbeddingClient | undefined;
|
||||
export async function getEmbeddingClient(
|
||||
providerFactory: CopilotProviderFactory,
|
||||
prompt: PromptService
|
||||
): Promise<EmbeddingClient | undefined> {
|
||||
if (EMBEDDING_CLIENT) {
|
||||
return EMBEDDING_CLIENT;
|
||||
}
|
||||
const client = new ProductionEmbeddingClient(providerFactory, prompt);
|
||||
if (await client.configured()) {
|
||||
EMBEDDING_CLIENT = client;
|
||||
}
|
||||
return EMBEDDING_CLIENT;
|
||||
}
|
||||
|
||||
export class MockEmbeddingClient extends EmbeddingClient {
|
||||
async getEmbeddings(input: string[]): Promise<Embedding[]> {
|
||||
return input.map((_, i) => ({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
AFFiNELogger,
|
||||
BlobNotFound,
|
||||
Config,
|
||||
CallMetric,
|
||||
CopilotContextFileNotSupported,
|
||||
DocNotFound,
|
||||
EventBus,
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
} from '../../../base';
|
||||
import { DocReader } from '../../../core/doc';
|
||||
import { Models } from '../../../models';
|
||||
import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { CopilotStorage } from '../storage';
|
||||
import { readStream } from '../utils';
|
||||
import { OpenAIEmbeddingClient } from './embedding';
|
||||
import { getEmbeddingClient } from './embedding';
|
||||
import type { Chunk, DocFragment } from './types';
|
||||
import { EMBEDDING_DIMENSIONS, EmbeddingClient } from './types';
|
||||
|
||||
@@ -29,11 +31,12 @@ export class CopilotContextDocJob {
|
||||
private client: EmbeddingClient | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly doc: DocReader,
|
||||
private readonly event: EventBus,
|
||||
private readonly logger: AFFiNELogger,
|
||||
private readonly models: Models,
|
||||
private readonly providerFactory: CopilotProviderFactory,
|
||||
private readonly prompt: PromptService,
|
||||
private readonly queue: JobQueue,
|
||||
private readonly storage: CopilotStorage
|
||||
) {
|
||||
@@ -53,10 +56,8 @@ export class CopilotContextDocJob {
|
||||
private async setup() {
|
||||
this.supportEmbedding =
|
||||
await this.models.copilotContext.checkEmbeddingAvailable();
|
||||
if (this.supportEmbedding && this.config.copilot.providers.openai.apiKey) {
|
||||
this.client = new OpenAIEmbeddingClient(
|
||||
this.config.copilot.providers.openai
|
||||
);
|
||||
if (this.supportEmbedding) {
|
||||
this.client = await getEmbeddingClient(this.providerFactory, this.prompt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ export class CopilotContextDocJob {
|
||||
return this.client as EmbeddingClient;
|
||||
}
|
||||
|
||||
@CallMetric('ai', 'addFileEmbeddingQueue')
|
||||
async addFileEmbeddingQueue(file: Jobs['copilot.embedding.files']) {
|
||||
if (!this.supportEmbedding) return;
|
||||
|
||||
@@ -87,6 +89,14 @@ export class CopilotContextDocJob {
|
||||
if (!this.supportEmbedding) return;
|
||||
|
||||
for (const { workspaceId, docId } of docs) {
|
||||
const jobId = `workspace:embedding:${workspaceId}:${docId}`;
|
||||
const job = await this.queue.get(jobId, 'copilot.embedding.docs');
|
||||
// if the job exists and is older than 5 minute, remove it
|
||||
if (job && job.timestamp + 5 * 60 * 1000 < Date.now()) {
|
||||
this.logger.verbose(`Removing old embedding job ${jobId}`);
|
||||
await this.queue.remove(jobId, 'copilot.embedding.docs');
|
||||
}
|
||||
|
||||
await this.queue.add(
|
||||
'copilot.embedding.docs',
|
||||
{
|
||||
@@ -97,6 +107,7 @@ export class CopilotContextDocJob {
|
||||
{
|
||||
jobId: `workspace:embedding:${workspaceId}:${docId}`,
|
||||
priority: options?.priority ?? 1,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -129,6 +140,10 @@ export class CopilotContextDocJob {
|
||||
if (enableDocEmbedding) {
|
||||
const toBeEmbedDocIds =
|
||||
await this.models.copilotWorkspace.findDocsToEmbed(workspaceId);
|
||||
this.logger.debug('Trigger embedding for docs', {
|
||||
workspaceId,
|
||||
toBeEmbedDocs: toBeEmbedDocIds.length,
|
||||
});
|
||||
for (const docId of toBeEmbedDocIds) {
|
||||
await this.queue.add(
|
||||
'copilot.embedding.docs',
|
||||
@@ -180,7 +195,7 @@ export class CopilotContextDocJob {
|
||||
);
|
||||
}
|
||||
|
||||
async readCopilotBlob(
|
||||
private async readCopilotBlob(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
blobId: string,
|
||||
@@ -330,6 +345,9 @@ export class CopilotContextDocJob {
|
||||
workspaceId,
|
||||
docId
|
||||
);
|
||||
this.logger.verbose(
|
||||
`Check if doc ${docId} in workspace ${workspaceId} needs embedding: ${needEmbedding}`
|
||||
);
|
||||
if (needEmbedding) {
|
||||
if (signal.aborted) return;
|
||||
const fragment = await this.getDocFragment(workspaceId, docId);
|
||||
@@ -371,6 +389,9 @@ export class CopilotContextDocJob {
|
||||
error instanceof CopilotContextFileNotSupported &&
|
||||
error.message.includes('no content found')
|
||||
) {
|
||||
this.logger.warn(
|
||||
`Doc ${docId} in workspace ${workspaceId} has no content, fulfilling empty embedding.`
|
||||
);
|
||||
// if the doc is empty, we still need to fulfill the embedding
|
||||
await this.fulfillEmptyEmbedding(workspaceId, docId);
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Cache,
|
||||
Config,
|
||||
CopilotInvalidContext,
|
||||
NoCopilotProviderAvailable,
|
||||
OnEvent,
|
||||
@@ -15,9 +14,11 @@ import {
|
||||
ContextFile,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { OpenAIEmbeddingClient } from './embedding';
|
||||
import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { getEmbeddingClient } from './embedding';
|
||||
import { ContextSession } from './session';
|
||||
import { EmbeddingClient } from './types';
|
||||
import type { EmbeddingClient } from './types';
|
||||
|
||||
const CONTEXT_SESSION_KEY = 'context-session';
|
||||
|
||||
@@ -27,26 +28,24 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
private client: EmbeddingClient | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly cache: Cache,
|
||||
private readonly models: Models
|
||||
private readonly models: Models,
|
||||
private readonly providerFactory: CopilotProviderFactory,
|
||||
private readonly prompt: PromptService
|
||||
) {}
|
||||
|
||||
@OnEvent('config.init')
|
||||
onConfigInit() {
|
||||
this.setup();
|
||||
async onConfigInit() {
|
||||
await this.setup();
|
||||
}
|
||||
|
||||
@OnEvent('config.changed')
|
||||
onConfigChanged() {
|
||||
this.setup();
|
||||
async onConfigChanged() {
|
||||
await this.setup();
|
||||
}
|
||||
|
||||
private setup() {
|
||||
const configure = this.config.copilot.providers.openai;
|
||||
if (configure.apiKey) {
|
||||
this.client = new OpenAIEmbeddingClient(configure);
|
||||
}
|
||||
private async setup() {
|
||||
this.client = await getEmbeddingClient(this.providerFactory, this.prompt);
|
||||
}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
|
||||
@@ -69,6 +69,10 @@ export type Chunk = {
|
||||
export const EMBEDDING_DIMENSIONS = 1024;
|
||||
|
||||
export abstract class EmbeddingClient {
|
||||
async configured() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getFileEmbeddings(
|
||||
file: File,
|
||||
chunkMapper: (chunk: Chunk[]) => Chunk[],
|
||||
|
||||
@@ -335,7 +335,66 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
|
||||
requireAttachment: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Rerank results',
|
||||
action: 'Rerank results',
|
||||
model: 'gpt-4.1-mini',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `Evaluate and rank search results based on their relevance and quality to the given query by assigning a score from 1 to 10, where 10 denotes the highest relevance.
|
||||
|
||||
Consider various factors such as content alignment with the query, source credibility, timeliness, and user intent.
|
||||
|
||||
# Steps
|
||||
|
||||
1. **Read the Query**: Understand the main intent and specific details of the search query.
|
||||
2. **Review Each Result**:
|
||||
- Analyze the content's relevance to the query.
|
||||
- Assess the credibility of the source or website.
|
||||
- Consider the timeliness of the information, ensuring it's current and relevant.
|
||||
- Evaluate the alignment with potential user intent based on the query.
|
||||
3. **Scoring**:
|
||||
- Assign a score from 1 to 10 based on the overall relevance and quality, with 10 being the most relevant.
|
||||
|
||||
# Output Format
|
||||
|
||||
Return a JSON object for each result in the following format in raw:
|
||||
{
|
||||
"scores": [
|
||||
{
|
||||
"reason": "[Reasoning behind the score in 20 words]",
|
||||
"chunk": "[chunk]",
|
||||
"targetId": "[targetId]",
|
||||
"score": [1-10]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Notes
|
||||
|
||||
- Be aware of the potential biases or inaccuracies in the sources.
|
||||
- Consider if the content is comprehensive and directly answers the query.
|
||||
- Pay attention to the nuances of user intent that might influence relevance.`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
<query>{{query}}</query>
|
||||
<results>
|
||||
{{#results}}
|
||||
<result>
|
||||
<targetId>{{targetId}}</targetId>
|
||||
<chunk>{{chunk}}</chunk>
|
||||
<content>
|
||||
{{content}}
|
||||
</content>
|
||||
</result>
|
||||
{{/results}}
|
||||
</results>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Generate a caption',
|
||||
action: 'Generate a caption',
|
||||
@@ -1558,7 +1617,7 @@ const imageActions: Prompt[] = [
|
||||
];
|
||||
|
||||
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
|
||||
model: 'gpt-4.1',
|
||||
model: 'claude-sonnet-4@20250514',
|
||||
optionalModels: [
|
||||
'gpt-4.1',
|
||||
'o3',
|
||||
|
||||
@@ -58,7 +58,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
try {
|
||||
metrics.ai.counter('chat_text_calls').add(1, { model: model.id });
|
||||
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
const [system, msgs] = await chatToGPTMessage(messages, true, true);
|
||||
|
||||
const modelInstance = this.instance(model.id);
|
||||
const { text, reasoning } = await generateText({
|
||||
@@ -142,7 +142,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
}
|
||||
|
||||
private isReasoningModel(model: string) {
|
||||
// only claude 3.7 sonnet supports reasoning config
|
||||
return model.startsWith('claude-3-7-sonnet');
|
||||
// claude 3.5 sonnet doesn't support reasoning config
|
||||
return model.includes('sonnet') && !model.startsWith('claude-3-5-sonnet');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
defaultForOutputType: true,
|
||||
},
|
||||
],
|
||||
@@ -113,7 +113,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -122,7 +122,16 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gpt-4.1-nano',
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -283,8 +292,8 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
temperature: options.temperature || 0,
|
||||
maxTokens: options.maxTokens || 4096,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
providerOptions: {
|
||||
openai: this.getOpenAIOptions(options, model.id),
|
||||
},
|
||||
@@ -322,10 +331,10 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
frequencyPenalty: options.frequencyPenalty || 0,
|
||||
presencePenalty: options.presencePenalty || 0,
|
||||
temperature: options.temperature || 0,
|
||||
maxTokens: options.maxTokens || 4096,
|
||||
frequencyPenalty: options.frequencyPenalty ?? 0,
|
||||
presencePenalty: options.presencePenalty ?? 0,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
providerOptions: {
|
||||
openai: this.getOpenAIOptions(options, model.id),
|
||||
},
|
||||
@@ -388,8 +397,9 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
temperature: ('temperature' in options && options.temperature) || 0,
|
||||
maxTokens: ('maxTokens' in options && options.maxTokens) || 4096,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
maxRetries: options.maxRetries ?? 3,
|
||||
schema,
|
||||
providerOptions: {
|
||||
openai: options.user ? { user: options.user } : {},
|
||||
|
||||
@@ -124,8 +124,8 @@ export class PerplexityProvider extends CopilotProvider<PerplexityConfig> {
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
temperature: options.temperature || 0,
|
||||
maxTokens: options.maxTokens || 4096,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
abortSignal: options.signal,
|
||||
});
|
||||
|
||||
@@ -164,8 +164,8 @@ export class PerplexityProvider extends CopilotProvider<PerplexityConfig> {
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
temperature: options.temperature || 0,
|
||||
maxTokens: options.maxTokens || 4096,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
abortSignal: options.signal,
|
||||
});
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ export abstract class CopilotProvider<C = any> {
|
||||
structure(
|
||||
_cond: ModelConditions,
|
||||
_messages: PromptMessage[],
|
||||
_options: CopilotStructuredOptions
|
||||
_options?: CopilotStructuredOptions
|
||||
): Promise<string> {
|
||||
throw new CopilotProviderNotSupported({
|
||||
provider: this.type,
|
||||
@@ -193,7 +193,7 @@ export abstract class CopilotProvider<C = any> {
|
||||
|
||||
embedding(
|
||||
_model: ModelConditions,
|
||||
_text: string,
|
||||
_text: string | string[],
|
||||
_options?: CopilotEmbeddingOptions
|
||||
): Promise<number[][]> {
|
||||
throw new CopilotProviderNotSupported({
|
||||
|
||||
@@ -61,6 +61,8 @@ export const PromptConfigStrictSchema = z.object({
|
||||
// params requirements
|
||||
requireContent: z.boolean().nullable().optional(),
|
||||
requireAttachment: z.boolean().nullable().optional(),
|
||||
// structure output
|
||||
maxRetries: z.number().nullable().optional(),
|
||||
// openai
|
||||
frequencyPenalty: z.number().nullable().optional(),
|
||||
presencePenalty: z.number().nullable().optional(),
|
||||
|
||||
@@ -64,7 +64,10 @@ export async function inferMimeType(url: string) {
|
||||
export async function chatToGPTMessage(
|
||||
messages: PromptMessage[],
|
||||
// TODO(@darkskygit): move this logic in interface refactoring
|
||||
withAttachment: boolean = true
|
||||
withAttachment: boolean = true,
|
||||
// NOTE: some providers in vercel ai sdk are not able to handle url attachments yet
|
||||
// so we need to use base64 encoded attachments instead
|
||||
useBase64Attachment: boolean = false
|
||||
): Promise<[string | undefined, ChatMessage[], ZodType?]> {
|
||||
const system = messages[0]?.role === 'system' ? messages.shift() : undefined;
|
||||
const schema =
|
||||
@@ -101,9 +104,10 @@ export async function chatToGPTMessage(
|
||||
if (mimeType.startsWith('image/')) {
|
||||
contents.push({ type: 'image', image: attachment, mimeType });
|
||||
} else {
|
||||
const data = attachment.startsWith('data:')
|
||||
? await fetch(attachment).then(r => r.arrayBuffer())
|
||||
: new URL(attachment);
|
||||
const data =
|
||||
attachment.startsWith('data:') || useBase64Attachment
|
||||
? await fetch(attachment).then(r => r.arrayBuffer())
|
||||
: new URL(attachment);
|
||||
contents.push({ type: 'file' as const, data, mimeType });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,17 +142,17 @@ export class ChatSession implements AsyncDisposable {
|
||||
}
|
||||
|
||||
private mergeUserContent(params: PromptParams) {
|
||||
const messages = this.stashMessages;
|
||||
const firstMessage = messages.at(0);
|
||||
const messages = this.takeMessages();
|
||||
const lastMessage = messages.pop();
|
||||
if (
|
||||
this.state.prompt.paramKeys.includes('content') &&
|
||||
!messages.some(m => m.role === AiPromptRole.assistant) &&
|
||||
firstMessage
|
||||
lastMessage?.role === AiPromptRole.user
|
||||
) {
|
||||
const normalizedParams = {
|
||||
...params,
|
||||
...firstMessage.params,
|
||||
content: firstMessage.content,
|
||||
...lastMessage.params,
|
||||
content: lastMessage.content,
|
||||
};
|
||||
const finished = this.state.prompt.finish(
|
||||
normalizedParams,
|
||||
@@ -160,11 +160,16 @@ export class ChatSession implements AsyncDisposable {
|
||||
);
|
||||
|
||||
// attachments should be combined with the first user message
|
||||
const firstUserMessage =
|
||||
finished.find(m => m.role === 'user') || finished[0];
|
||||
const firstUserMessageIndex = finished.findIndex(
|
||||
m => m.role === AiPromptRole.user
|
||||
);
|
||||
// if prompt not contains user message, skip merge content
|
||||
if (firstUserMessageIndex < 0) return null;
|
||||
const firstUserMessage = finished[firstUserMessageIndex];
|
||||
|
||||
firstUserMessage.attachments = [
|
||||
finished[0].attachments || [],
|
||||
firstMessage.attachments || [],
|
||||
lastMessage.attachments || [],
|
||||
]
|
||||
.flat()
|
||||
.filter(v =>
|
||||
@@ -172,6 +177,8 @@ export class ChatSession implements AsyncDisposable {
|
||||
? !!v.trim()
|
||||
: v && v.attachment.trim() && v.mimeType
|
||||
);
|
||||
//insert all previous user message content before first user message
|
||||
finished.splice(firstUserMessageIndex, 0, ...messages);
|
||||
|
||||
return finished;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import { ConnectedAccount } from '@prisma/client';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import {
|
||||
Config,
|
||||
InvalidAuthState,
|
||||
InvalidOauthCallbackState,
|
||||
MissingOauthQueryParameter,
|
||||
OauthAccountAlreadyConnected,
|
||||
OauthStateExpired,
|
||||
SignUpForbidden,
|
||||
UnknownOauthProvider,
|
||||
URLHelper,
|
||||
UseNamedGuard,
|
||||
@@ -38,7 +40,8 @@ export class OAuthController {
|
||||
private readonly oauth: OAuthService,
|
||||
private readonly models: Models,
|
||||
private readonly providerFactory: OAuthProviderFactory,
|
||||
private readonly url: URLHelper
|
||||
private readonly url: URLHelper,
|
||||
private readonly config: Config
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@@ -80,8 +83,10 @@ export class OAuthController {
|
||||
};
|
||||
}
|
||||
|
||||
// the prerequest `/oauth/prelight` request already checked client version,
|
||||
// let's simply ignore it for callback which will block apple oauth post_form mode
|
||||
// @UseNamedGuard('version')
|
||||
@Public()
|
||||
@UseNamedGuard('version')
|
||||
@Post('/callback')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async callback(
|
||||
@@ -182,7 +187,7 @@ export class OAuthController {
|
||||
}
|
||||
|
||||
const externAccount = await provider.getUser(tokens, state);
|
||||
const user = await this.loginFromOauth(
|
||||
const user = await this.getOrCreateUserFromOauth(
|
||||
state.provider,
|
||||
externAccount,
|
||||
tokens
|
||||
@@ -203,7 +208,7 @@ export class OAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
private async loginFromOauth(
|
||||
private async getOrCreateUserFromOauth(
|
||||
provider: OAuthProviderName,
|
||||
externalAccount: OAuthAccount,
|
||||
tokens: Tokens
|
||||
@@ -219,6 +224,10 @@ export class OAuthController {
|
||||
return connectedAccount.user;
|
||||
}
|
||||
|
||||
if (!this.config.auth.allowSignup) {
|
||||
throw new SignUpForbidden();
|
||||
}
|
||||
|
||||
const user = await this.models.user.fulfill(externalAccount.email, {
|
||||
avatarUrl: externalAccount.avatarUrl,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { OAuthProviderFactory } from '../factory';
|
||||
export interface OAuthAccount {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user