Compare commits

..

6 Commits

Author SHA1 Message Date
Yifeng Wang
3d2d399796 refactor: extract common builder 2025-05-29 16:13:48 +08:00
Yifeng Wang
6483f36723 refactor: use path builder 2025-05-29 16:13:48 +08:00
Yifeng Wang
df2ecf2bec feat(editor): support connector dom renderer 2025-05-29 16:13:48 +08:00
Yifeng Wang
148c718a12 fix: review 2025-05-29 16:13:48 +08:00
Yifeng Wang
c4af1e77d0 fix: test 2025-05-29 16:13:48 +08:00
Yifeng Wang
6a0eb80903 feat(editor): support triangle and diamond shape in shape dom renderer 2025-05-29 16:13:48 +08:00
207 changed files with 2462 additions and 4247 deletions

View File

@@ -151,8 +151,7 @@ jobs:
- name: Clippy
run: |
rustup component add clippy
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
cargo clippy --all-targets --all-features -- -D warnings
check-git-status:
name: Check Git Status
@@ -924,7 +923,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
run: cargo nextest run --release --no-fail-fast
copilot-api-test:
name: Server Copilot Api Test

View File

@@ -117,10 +117,31 @@ jobs:
name: android
path: packages/frontend/apps/android/dist
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
determine-ios-runner:
runs-on: ubuntu-latest
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
View File

@@ -281,12 +281,12 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "3.0.8"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell_polyfill",
"once_cell",
"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.1",
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.13.0",
@@ -546,9 +546,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
dependencies = [
"serde",
]
@@ -603,9 +603,9 @@ dependencies = [
[[package]]
name = "bpaf"
version = "0.9.20"
version = "0.9.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31"
checksum = "4848ed5727d39a7573551c205bcb1ccd88c8cad4ed2c80f62e2316f208196b8d"
[[package]]
name = "bstr"
@@ -697,9 +697,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.24"
version = "1.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
dependencies = [
"shlex",
]
@@ -815,9 +815,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.39"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
@@ -825,9 +825,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.39"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
@@ -901,9 +901,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
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.1",
"bitflags 2.9.0",
"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.1",
"bitflags 2.9.0",
"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.1",
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
@@ -1272,9 +1272,9 @@ dependencies = [
[[package]]
name = "docx-rust"
version = "0.1.10"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75463cd42bd579fdf76cf10ff1c4fb5988568c726f8ed4c2ed3921ddb6ef5c89"
checksum = "e6a8e9647d314f66d467a705715111d045955949b5dbcf2dc7aee89e078af83d"
dependencies = [
"derive_more",
"hard-xml",
@@ -1339,9 +1339,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.12"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -1595,16 +1595,15 @@ dependencies = [
[[package]]
name = "generator"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows 0.61.1",
"windows 0.58.0",
]
[[package]]
@@ -1690,9 +1689,9 @@ dependencies = [
[[package]]
name = "hard-xml"
version = "1.41.0"
version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b07b8ba970e18a03dbb79f6786b6e4d6f198a0ac839aa5182017001bb8dee17"
checksum = "18b4f798dc293307dc5445b47403a317adc8131782963f3ad4700a836bd26c93"
dependencies = [
"hard-xml-derive",
"jetscii",
@@ -1703,11 +1702,11 @@ dependencies = [
[[package]]
name = "hard-xml-derive"
version = "1.41.0"
version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8"
checksum = "5ef0ad18601a1563729b311c9d49e5d46b1c987dc0353742cb8219f61f4339a6"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -1826,7 +1825,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.57.0",
"windows-core 0.61.0",
]
[[package]]
@@ -1911,9 +1910,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
dependencies = [
"displaydoc",
"icu_collections 2.0.0",
@@ -1927,9 +1926,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
[[package]]
name = "icu_provider"
@@ -2050,14 +2049,15 @@ dependencies = [
[[package]]
name = "io-surface"
version = "0.16.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e"
checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e"
dependencies = [
"cgl",
"core-foundation",
"core-foundation-sys",
"leaky-cow",
"libc",
]
[[package]]
@@ -2188,12 +2188,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
version = "0.8.8"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.0",
]
[[package]]
@@ -2279,7 +2279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fa2559e99ba0f26a12458aabc754432c805bbb8cba516c427825a997af1fb7"
dependencies = [
"aes",
"bitflags 2.9.1",
"bitflags 2.9.0",
"cbc",
"ecb",
"encoding_rs",
@@ -2390,13 +2390,13 @@ dependencies = [
[[package]]
name = "mio"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2410,12 +2410,12 @@ dependencies = [
[[package]]
name = "napi"
version = "3.0.0-beta.3"
version = "3.0.0-alpha.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a5c343e6e1fb57bf3ea3386638c4affb394ee932708128840a56aaac3d6a8ab"
checksum = "c857a2b38c994db8bec785554ab4216d45ad63469832070c86a992be0b5491ad"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.0",
"chrono",
"ctor",
"napi-build",
@@ -2426,18 +2426,17 @@ dependencies = [
[[package]]
name = "napi-build"
version = "2.2.0"
version = "2.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
checksum = "e28acfa557c083f6e254a786e01ba253fc56f18ee000afcd4f79af735f73a6da"
[[package]]
name = "napi-derive"
version = "3.0.0-beta.3"
version = "3.0.0-alpha.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d23065ee795a4b1a8755fdf4a39c2a229679f01f923a8feea33f045d6d96cb"
checksum = "c7165d931d54f68115e651330d5fe0ae0081133d3f4ee3ab55b0b808f0c23f71"
dependencies = [
"convert_case 0.8.0",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
@@ -2446,9 +2445,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "2.0.0-beta.3"
version = "2.0.0-alpha.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348aaac2c51b5d11cf90cf7670b470c7f4d1607d15c338efd4d3db361003e4f5"
checksum = "ce3f36354262054df8e1c3a73bdcd36ea13f130feb1e4d86b67cab9e10d6ef6d"
dependencies = [
"convert_case 0.8.0",
"proc-macro2",
@@ -2459,9 +2458,9 @@ dependencies = [
[[package]]
name = "napi-sys"
version = "3.0.0-alpha.2"
version = "3.0.0-alpha.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b443b980b2258dbaa31b99115e74da6c0866e537278309d566b4672a2f8df516"
checksum = "4cc061b99c514ad4b7abc99d4db1ca24b9542b7ff48b4760bd9f82b24611534d"
dependencies = [
"libloading",
]
@@ -2478,7 +2477,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2626,7 +2625,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"dispatch2",
"objc2",
]
@@ -2643,7 +2642,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
@@ -2674,12 +2673,6 @@ 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"
@@ -3016,7 +3009,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
dependencies = [
"bit-set 0.8.0",
"bit-vec 0.8.0",
"bitflags 2.9.1",
"bitflags 2.9.0",
"lazy_static",
"num-traits",
"rand 0.8.5",
@@ -3045,7 +3038,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"memchr",
"unicase",
]
@@ -3250,7 +3243,7 @@ version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
]
[[package]]
@@ -3425,7 +3418,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys",
@@ -3468,9 +3461,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "rusty-fork"
@@ -3724,9 +3717,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.10"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -3753,9 +3746,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -3766,9 +3759,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3803,9 +3796,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
dependencies = [
"proc-macro2",
"quote",
@@ -3816,9 +3809,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
dependencies = [
"dotenvy",
"either",
@@ -3835,19 +3828,20 @@ dependencies = [
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.101",
"tempfile",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.9.1",
"bitflags 2.9.0",
"byteorder",
"bytes",
"chrono",
@@ -3884,13 +3878,13 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.9.1",
"bitflags 2.9.0",
"byteorder",
"chrono",
"crc",
@@ -3922,9 +3916,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
dependencies = [
"atoi",
"chrono",
@@ -4438,9 +4432,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
@@ -4576,9 +4570,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.5"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
checksum = "69aff09fea9a41fb061ae6b206cb87cac1b8db07df31be3ba271fbc26760f213"
dependencies = [
"cc",
"regex",
@@ -4941,13 +4935,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.17.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"wasm-bindgen",
]
[[package]]
@@ -5123,9 +5115,9 @@ dependencies = [
[[package]]
name = "weezl"
version = "0.1.10"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "whoami"
@@ -5165,7 +5157,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5186,24 +5178,12 @@ dependencies = [
[[package]]
name = "windows"
version = "0.61.1"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"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",
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
@@ -5220,26 +5200,28 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.61.2"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
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"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"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",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
[[package]]
@@ -5253,6 +5235,17 @@ 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"
@@ -5275,6 +5268,17 @@ 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"
@@ -5292,16 +5296,6 @@ 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"
@@ -5313,18 +5307,37 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
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"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
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"
dependencies = [
"windows-link",
]
@@ -5380,7 +5393,7 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
@@ -5388,12 +5401,19 @@ dependencies = [
]
[[package]]
name = "windows-threading"
version = "0.1.0"
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows-link",
"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",
]
[[package]]
@@ -5408,6 +5428,12 @@ 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"
@@ -5420,6 +5446,12 @@ 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"
@@ -5432,12 +5464,24 @@ 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"
@@ -5450,6 +5494,12 @@ 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"
@@ -5462,6 +5512,12 @@ 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"
@@ -5474,6 +5530,12 @@ 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"
@@ -5486,6 +5548,12 @@ 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"
@@ -5501,7 +5569,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
]
[[package]]
@@ -5570,7 +5638,7 @@ dependencies = [
"serde_json",
"smol_str",
"thiserror 2.0.12",
"yrs 0.23.4",
"yrs 0.23.1",
]
[[package]]
@@ -5601,7 +5669,7 @@ dependencies = [
"regex",
"y-octo",
"y-sync",
"yrs 0.23.4",
"yrs 0.23.1",
]
[[package]]
@@ -5679,9 +5747,9 @@ dependencies = [
[[package]]
name = "yrs"
version = "0.23.4"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1d740a98b12112352f05ddfc06c1505b66fca116601f9388b84be45d21f84d"
checksum = "4a7cab84724ae7f361a8c92465f5160922cbb941a499e1a8cacd103351ab9c78"
dependencies = [
"arc-swap",
"async-lock",
@@ -5692,7 +5760,7 @@ dependencies = [
"serde_json",
"smallstr",
"smallvec",
"thiserror 2.0.12",
"thiserror 1.0.69",
]
[[package]]

View File

@@ -47,9 +47,9 @@ log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1"
nanoid = "0.4"
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-beta.3" }
napi-derive = { version = "3.0.0-alpha.28" }
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.45"
tokio = "1.37"
tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.23" }
tree-sitter-c-sharp = { version = "0.23" }

View File

@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let style: EmbedCardStyle = model.props.style;
let { style } = model.props;
if (!styles.includes(style)) {
style = styles[0];
@@ -482,26 +482,24 @@ 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',
},
] as const
).filter(action => BookmarkStyles.includes(action.id)),
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)),
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null;

View File

@@ -259,18 +259,18 @@ const builtinToolbarConfig = {
conversionsActionGroup,
{
id: 'c.style',
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
] as const
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
].filter(action =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
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',
},
] as const
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
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)
),
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;

View File

@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
let style: EmbedCardStyle = model.props.style;
let { style } = model.props;
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: EmbedCardStyle = model.props.style;
let { style } = model.props;
if (!styles.includes(style)) {
style =
@@ -441,11 +441,7 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
let { style } = model.props;
let flavour = 'affine:bookmark';
if (
!BookmarkStyles.includes(
style as (typeof BookmarkStyles)[number]
)
) {
if (!BookmarkStyles.includes(style)) {
style = BookmarkStyles[0];
}
@@ -521,26 +517,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',
},
] as const
).filter(action => EmbedGithubStyles.includes(action.id)),
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)
),
when(ctx) {
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
},

View File

@@ -16,7 +16,6 @@ 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 {
@@ -88,12 +87,6 @@ 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"
@@ -109,7 +102,6 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
})}
></div>
${widgets}
`;
}
@@ -186,22 +178,11 @@ 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) ||
onTitle)
(model.isLocked() || !isTransparent(model.props.background))
);
},
onSelect(context) {
return context.default(context);
},
};
},
}

View File

@@ -241,35 +241,20 @@ export class EdgelessFrameManager extends GfxExtension {
surfaceModel.elementAdded.subscribe(({ id, local }) => {
const element = surfaceModel.getElementById(id);
if (element && local) {
// 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
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
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]);
}

View File

@@ -9,10 +9,7 @@ import {
getSurfaceComponent,
} from '@blocksuite/affine-block-surface';
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
import type {
EmbedCardStyle,
ShapeElementModel,
} from '@blocksuite/affine-model';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import {
BookmarkStyles,
DEFAULT_NOTE_HEIGHT,
@@ -239,7 +236,7 @@ export class EdgelessClipboardController extends PageClipboard {
const options: Record<string, unknown> = {};
let flavour = 'affine:bookmark';
let style: EmbedCardStyle = BookmarkStyles[0];
let style = BookmarkStyles[0];
let isInternalLink = false;
let isLinkedBlock = false;

View File

@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
) as SurfaceBlockModel;
}
get viewportElement(): HTMLElement {
private 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;
}

View File

@@ -43,25 +43,6 @@ 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',
@@ -160,18 +141,6 @@ 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>();
@@ -217,7 +186,6 @@ export class DomRenderer {
private _initViewport() {
this._disposables.add(
this.viewport.viewportUpdated.subscribe(() => {
this._markViewportDirty();
this.refresh();
})
);
@@ -227,7 +195,6 @@ export class DomRenderer {
if (this._sizeUpdatedRafId) return;
this._sizeUpdatedRafId = requestConnectedFrame(() => {
this._sizeUpdatedRafId = null;
this._markSizeDirty();
this._resetSize();
this._render();
this.refresh();
@@ -241,7 +208,6 @@ export class DomRenderer {
if (this.usePlaceholder !== shouldRenderPlaceholders) {
this.usePlaceholder = shouldRenderPlaceholders;
this._markUsePlaceholderDirty();
this.refresh();
}
})
@@ -341,292 +307,6 @@ 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[] = [];
@@ -707,4 +387,100 @@ 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();
}
}

View File

@@ -29,6 +29,12 @@ export class OpenDocDropdownMenu extends SignalWatcher(
gap: unset !important;
}
editor-icon-button {
.label {
font-weight: 400;
}
}
div[data-orientation] {
width: 264px;
gap: 4px;

View File

@@ -9,7 +9,6 @@ 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',

View File

@@ -16,6 +16,5 @@ export const renderFilterBar = (props: DataViewWidgetProps) => {
.vars="${filterTrait.view.vars$}"
.filterGroup="${filterTrait.filter$}"
.onChange="${filterTrait.filterSet}"
.dataViewLogic="${props.dataViewLogic}"
></filter-bar>`;
};

View File

@@ -16,7 +16,6 @@ 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) {
@@ -100,7 +99,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
requestAnimationFrame(() => {
this.expandGroup(element, index);
});
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
});
};
@@ -208,9 +206,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
@property({ attribute: false })
accessor vars!: ReadonlySignal<Variable[]>;
@property({ attribute: false })
accessor dataViewLogic!: DataViewUILogicBase;
}
declare global {

View File

@@ -26,10 +26,7 @@ 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 {
type DataViewUILogicBase,
popCreateFilter,
} from '../../../core/index.js';
import { popCreateFilter } from '../../../core/index.js';
import {
type FilterGroupView,
getDepth,
@@ -378,7 +375,6 @@ export const popFilterRoot = (
props: {
filterTrait: FilterTrait;
onBack: () => void;
dataViewLogic: DataViewUILogicBase;
}
) => {
const filterTrait = props.filterTrait;
@@ -418,10 +414,6 @@ export const popFilterRoot = (
...value,
conditions: [...value.conditions, filter],
});
props.dataViewLogic.eventTrace(
'CreateDatabaseFilter',
{}
);
},
},
{ middleware: subMenuMiddleware }

View File

@@ -75,7 +75,6 @@ export class DataViewHeaderToolsFilter extends WidgetBase {
conditions: [filter],
};
this.toggleShowFilter(true);
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
}
);

View File

@@ -145,16 +145,13 @@ const createSettingMenus = (
popFilterRoot(target, {
filterTrait: filterTrait,
onBack: reopen,
dataViewLogic: dataViewLogic,
});
dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
});
} else {
popFilterRoot(target, {
filterTrait: filterTrait,
onBack: reopen,
dataViewLogic: dataViewLogic,
});
}
},

View File

@@ -192,14 +192,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
this._updateTitleInMeta();
this.requestUpdate();
};
if (this._rootModel) {
const rootModel = this._rootModel;
rootModel.props.title.yText.observe(updateMetaTitle);
this._disposables.add(() => {
rootModel.props.title.yText.unobserve(updateMetaTitle);
});
}
this._rootModel?.props.title.yText.observe(updateMetaTitle);
this._disposables.add(() => {
this._rootModel?.props.title.yText.unobserve(updateMetaTitle);
});
}
override render() {

View File

@@ -0,0 +1,11 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { connectorDomRenderer } from './connector-dom/index.js';
/**
* Extension to register the DOM-based renderer for 'connector' elements.
*/
export const ConnectorDomRendererExtension = DomElementRendererExtension(
'connector',
connectorDomRenderer
);

View File

@@ -0,0 +1,367 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import {
type ConnectorElementModel,
ConnectorMode,
DefaultTheme,
type PointStyle,
} from '@blocksuite/affine-model';
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
import { isConnectorWithLabel } from '../../connector-manager.js';
import { DEFAULT_ARROW_SIZE } from '../utils.js';
interface PathBounds {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
function calculatePathBounds(path: PointLocation[]): PathBounds {
if (path.length === 0) {
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
}
let minX = path[0][0];
let minY = path[0][1];
let maxX = path[0][0];
let maxY = path[0][1];
for (const point of path) {
minX = Math.min(minX, point[0]);
minY = Math.min(minY, point[1]);
maxX = Math.max(maxX, point[0]);
maxY = Math.max(maxY, point[1]);
}
return { minX, minY, maxX, maxY };
}
function createConnectorPath(
points: PointLocation[],
mode: ConnectorMode
): string {
if (points.length < 2) return '';
const pathBuilder = new SVGPathBuilder();
pathBuilder.moveTo(points[0][0], points[0][1]);
if (mode === ConnectorMode.Curve) {
// Use bezier curves
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
pathBuilder.curveTo(
prev.absOut[0],
prev.absOut[1],
curr.absIn[0],
curr.absIn[1],
curr[0],
curr[1]
);
}
} else {
// Use straight lines
for (let i = 1; i < points.length; i++) {
pathBuilder.lineTo(points[i][0], points[i][1]);
}
}
return pathBuilder.build();
}
function createArrowMarker(
id: string,
style: PointStyle,
color: string,
strokeWidth: number,
isStart: boolean = false
): SVGMarkerElement {
const marker = document.createElementNS(
'http://www.w3.org/2000/svg',
'marker'
);
const size = DEFAULT_ARROW_SIZE * (strokeWidth / 2);
marker.id = id;
marker.setAttribute('viewBox', '0 0 20 20');
marker.setAttribute('refX', isStart ? '20' : '0');
marker.setAttribute('refY', '10');
marker.setAttribute('markerWidth', String(size));
marker.setAttribute('markerHeight', String(size));
marker.setAttribute('orient', 'auto');
marker.setAttribute('markerUnits', 'strokeWidth');
switch (style) {
case 'Arrow': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute(
'd',
isStart ? 'M 20 5 L 10 10 L 20 15 Z' : 'M 0 5 L 10 10 L 0 15 Z'
);
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
marker.append(path);
break;
}
case 'Triangle': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute(
'd',
isStart ? 'M 20 7 L 12 10 L 20 13 Z' : 'M 0 7 L 8 10 L 0 13 Z'
);
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
marker.append(path);
break;
}
case 'Circle': {
const circle = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
circle.setAttribute('cx', '10');
circle.setAttribute('cy', '10');
circle.setAttribute('r', '4');
circle.setAttribute('fill', color);
circle.setAttribute('stroke', color);
marker.append(circle);
break;
}
case 'Diamond': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute('d', 'M 10 6 L 14 10 L 10 14 L 6 10 Z');
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
marker.append(path);
break;
}
}
return marker;
}
function renderConnectorLabel(
model: ConnectorElementModel,
container: HTMLElement,
renderer: DomRenderer,
zoom: number
) {
if (!isConnectorWithLabel(model) || !model.labelXYWH) {
return;
}
const [lx, ly, lw, lh] = model.labelXYWH;
const {
labelStyle: {
color,
fontSize,
fontWeight,
fontStyle,
fontFamily,
textAlign,
},
} = model;
// Create label element
const labelElement = document.createElement('div');
labelElement.style.position = 'absolute';
labelElement.style.left = `${lx * zoom}px`;
labelElement.style.top = `${ly * zoom}px`;
labelElement.style.width = `${lw * zoom}px`;
labelElement.style.height = `${lh * zoom}px`;
labelElement.style.pointerEvents = 'none';
labelElement.style.overflow = 'hidden';
labelElement.style.display = 'flex';
labelElement.style.alignItems = 'center';
labelElement.style.justifyContent =
textAlign === 'center'
? 'center'
: textAlign === 'right'
? 'flex-end'
: 'flex-start';
// Style the text
labelElement.style.color = renderer.getColorValue(
color,
DefaultTheme.black,
true
);
labelElement.style.fontSize = `${fontSize * zoom}px`;
labelElement.style.fontWeight = fontWeight;
labelElement.style.fontStyle = fontStyle;
labelElement.style.fontFamily = fontFamily;
labelElement.style.textAlign = textAlign;
labelElement.style.lineHeight = '1.2';
labelElement.style.whiteSpace = 'pre-wrap';
labelElement.style.wordWrap = 'break-word';
// Add text content
if (model.text) {
labelElement.textContent = model.text.toString();
}
container.append(labelElement);
}
/**
* Renders a ConnectorElementModel to a given HTMLElement using DOM/SVG.
* This function is intended to be registered via the DomElementRendererExtension.
*
* @param model - The connector element model containing rendering properties.
* @param element - The HTMLElement to apply the connector's styles to.
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
*/
export const connectorDomRenderer = (
model: ConnectorElementModel,
element: HTMLElement,
renderer: DomRenderer
): void => {
const { zoom } = renderer.viewport;
const {
mode,
path: points,
strokeStyle,
frontEndpointStyle,
rearEndpointStyle,
strokeWidth,
stroke,
} = model;
// Clear previous content
element.innerHTML = '';
// Early return if no path points
if (!points || points.length < 2) {
return;
}
// Calculate bounds for the SVG viewBox
const pathBounds = calculatePathBounds(points);
const padding = Math.max(strokeWidth * 2, 20); // Add padding for arrows
const svgWidth = (pathBounds.maxX - pathBounds.minX + padding * 2) * zoom;
const svgHeight = (pathBounds.maxY - pathBounds.minY + padding * 2) * zoom;
const offsetX = pathBounds.minX - padding;
const offsetY = pathBounds.minY - padding;
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = `${offsetX * zoom}px`;
svg.style.top = `${offsetY * zoom}px`;
svg.style.width = `${svgWidth}px`;
svg.style.height = `${svgHeight}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${svgWidth / zoom} ${svgHeight / zoom}`);
// Create defs for markers
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.append(defs);
const strokeColor = renderer.getColorValue(
stroke,
DefaultTheme.connectorColor,
true
);
// Create markers for endpoints
let startMarkerId = '';
let endMarkerId = '';
if (frontEndpointStyle !== 'None') {
startMarkerId = `start-marker-${model.id}`;
const startMarker = createArrowMarker(
startMarkerId,
frontEndpointStyle,
strokeColor,
strokeWidth,
true
);
defs.append(startMarker);
}
if (rearEndpointStyle !== 'None') {
endMarkerId = `end-marker-${model.id}`;
const endMarker = createArrowMarker(
endMarkerId,
rearEndpointStyle,
strokeColor,
strokeWidth,
false
);
defs.append(endMarker);
}
// Create path element
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
// Adjust points relative to the SVG coordinate system
const adjustedPoints = points.map(point => {
const adjustedPoint = new PointLocation([
point[0] - offsetX,
point[1] - offsetY,
]);
if (point.absIn) {
adjustedPoint.in = [
point.absIn[0] - offsetX - adjustedPoint[0],
point.absIn[1] - offsetY - adjustedPoint[1],
];
}
if (point.absOut) {
adjustedPoint.out = [
point.absOut[0] - offsetX - adjustedPoint[0],
point.absOut[1] - offsetY - adjustedPoint[1],
];
}
return adjustedPoint;
});
const pathData = createConnectorPath(adjustedPoints, mode);
pathElement.setAttribute('d', pathData);
pathElement.setAttribute('stroke', strokeColor);
pathElement.setAttribute('stroke-width', String(strokeWidth));
pathElement.setAttribute('fill', 'none');
pathElement.setAttribute('stroke-linecap', 'round');
pathElement.setAttribute('stroke-linejoin', 'round');
// Apply stroke style
if (strokeStyle === 'dash') {
pathElement.setAttribute('stroke-dasharray', '12,12');
}
// Apply markers
if (startMarkerId) {
pathElement.setAttribute('marker-start', `url(#${startMarkerId})`);
}
if (endMarkerId) {
pathElement.setAttribute('marker-end', `url(#${endMarkerId})`);
}
svg.append(pathElement);
element.append(svg);
// Set element size and position
element.style.width = `${model.w * zoom}px`;
element.style.height = `${model.h * zoom}px`;
element.style.overflow = 'visible';
element.style.pointerEvents = 'none';
// Set z-index for layering
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
// Render label if present
renderConnectorLabel(model, element, renderer, zoom);
};

View File

@@ -2,6 +2,7 @@ export * from './adapter';
export * from './connector-manager';
export * from './connector-tool';
export * from './element-renderer';
export { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
export * from './element-transform';
export * from './text';
export * from './toolbar/config';

View File

@@ -7,6 +7,7 @@ import { ConnectionOverlay } from './connector-manager';
import { ConnectorTool } from './connector-tool';
import { effects } from './effects';
import { ConnectorElementRendererExtension } from './element-renderer';
import { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
import { ConnectorFilter } from './element-transform';
import { connectorToolbarExtension } from './toolbar/config';
import { connectorQuickTool } from './toolbar/quick-tool';
@@ -24,6 +25,7 @@ export class ConnectorViewExtension extends ViewExtensionProvider {
super.setup(context);
context.register(ConnectorElementView);
context.register(ConnectorElementRendererExtension);
context.register(ConnectorDomRendererExtension);
if (this.isEdgeless(context.scope)) {
context.register(ConnectorTool);
context.register(ConnectorFilter);

View File

@@ -1,6 +1,6 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import { MindmapElementModel } from '@blocksuite/affine-model';
import { type Bound } from '@blocksuite/global/gfx';
import { Bound } from '@blocksuite/global/gfx';
import {
type DragExtensionInitializeContext,
type ExtensionDragMoveContext,
@@ -74,63 +74,47 @@ export class SnapExtension extends InteractivityExtension {
return {};
}
let alignBound: Bound | null = null;
return {
onResizeStart(context) {
snapOverlay.setMovingElements(context.elements);
alignBound = snapOverlay.setMovingElements(context.elements);
},
onResizeMove(context) {
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;
}
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
return;
}
if (alignDirection.length > 0) {
const rst = snapOverlay.alignResize(
currentHandlePos,
alignDirection
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)
);
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,
});
dx = min * Math.sign(dx) * alignBound.w;
dy = min * Math.sign(dy) * alignBound.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();
},
};

View File

@@ -3,7 +3,7 @@ import {
ConnectorElementModel,
MindmapElementModel,
} from '@blocksuite/affine-model';
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
import type { GfxModel } from '@blocksuite/std/gfx';
interface Distance {
@@ -586,60 +586,6 @@ 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;

View File

@@ -1,6 +1,7 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
import { SVGShapeBuilder } from '@blocksuite/global/gfx';
import { manageClassNames, setStyles } from './utils';
@@ -122,25 +123,22 @@ export const shapeDomRenderer = (
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(' ');
// Generate diamond points using shared utility
svgPoints = SVGShapeBuilder.diamond(
unscaledWidth,
unscaledHeight,
strokeW
);
} else {
// triangle
// Adjusted points for triangle
svgPoints = [
`${unscaledWidth / 2},${halfStroke}`,
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
`${halfStroke},${unscaledHeight - halfStroke}`,
].join(' ');
// triangle - generate triangle points using shared utility
svgPoints = SVGShapeBuilder.triangle(
unscaledWidth,
unscaledHeight,
strokeW
);
}
// Determine if stroke should be visible and its color

View File

@@ -188,8 +188,6 @@ 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}

View File

@@ -30,12 +30,11 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
*/
type BackwardCompatibleUndefined = undefined;
export const AttachmentBlockStyles = [
export const AttachmentBlockStyles: EmbedCardStyle[] = [
'cubeThick',
'horizontalThin',
'pdf',
'citation',
] as const satisfies EmbedCardStyle[];
] as const;
export type AttachmentBlockProps = {
name: string;

View File

@@ -15,13 +15,13 @@ import type {
LinkPreviewData,
} from '../../utils/index.js';
export const BookmarkStyles = [
export const BookmarkStyles: EmbedCardStyle[] = [
'vertical',
'horizontal',
'list',
'cube',
'citation',
] as const satisfies EmbedCardStyle[];
] as const;
export type BookmarkBlockProps = {
style: (typeof BookmarkStyles)[number];

View File

@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
description: string | null;
};
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
export type EmbedFigmaBlockProps = {
style: (typeof EmbedFigmaStyles)[number];

View File

@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
assignees: string[] | null;
};
export const EmbedGithubStyles = [
export const EmbedGithubStyles: EmbedCardStyle[] = [
'vertical',
'horizontal',
'list',
'cube',
] as const satisfies EmbedCardStyle[];
] as const;
export type EmbedGithubBlockProps = {
style: (typeof EmbedGithubStyles)[number];

View File

@@ -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 = ['html'] as const satisfies EmbedCardStyle[];
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
export type EmbedHtmlBlockProps = {
style: (typeof EmbedHtmlStyles)[number];

View File

@@ -7,7 +7,7 @@ import { BlockModel } from '@blocksuite/store';
import { type EmbedCardStyle } from '../../../utils/index.js';
export const EmbedIframeStyles = ['figma'] as const satisfies EmbedCardStyle[];
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
export type EmbedIframeBlockProps = {
url: string; // the original url that user input

View File

@@ -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 = [
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
'vertical',
'horizontal',
'list',
'cube',
'horizontalThin',
'citation',
] as const satisfies EmbedCardStyle[];
];
export type EmbedLinkedDocBlockProps = {
style: (typeof EmbedLinkedDocStyles)[number];
style: EmbedCardStyle;
caption: string | null;
footnoteIdentifier: string | null;
} & ReferenceInfo;

View File

@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
description: string | null;
};
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
export type EmbedLoomBlockProps = {
style: (typeof EmbedLoomStyles)[number];

View File

@@ -5,9 +5,7 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedSyncedDocStyles = [
'syncedDoc',
] as const satisfies EmbedCardStyle[];
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
export type EmbedSyncedDocBlockProps = {
style: EmbedCardStyle;

View File

@@ -1,7 +1,6 @@
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';
@@ -31,10 +30,7 @@ export type EmbedCardModel = InstanceType<
ExternalEmbedModel | InternalEmbedModel
>;
export type LinkableEmbedModel =
| EmbedCardModel
| EmbedIframeBlockModel
| BookmarkBlockModel;
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;

View File

@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
creatorImage: string | null;
};
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
export type EmbedYoutubeBlockProps = {
style: (typeof EmbedYoutubeStyles)[number];

View File

@@ -80,8 +80,4 @@ 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);
});
});

View File

@@ -37,7 +37,7 @@ const handlePoint = (
};
const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
const afterExportSubscription = slots.afterExport.subscribe(payload => {
slots.afterExport.subscribe(payload => {
if (payload.type === 'block') {
const snapshot = payload.snapshot;
@@ -53,14 +53,10 @@ const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
}
}
});
return () => {
afterExportSubscription.unsubscribe();
};
};
export const copyMiddleware = (std: BlockStdScope): TransformerMiddleware => {
return ({ slots }) => {
return sliceText(slots, std);
sliceText(slots, std);
};
};

View File

@@ -3,7 +3,7 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const fileNameMiddleware =
(fileName?: string): TransformerMiddleware =>
({ slots }) => {
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
slots.beforeImport.subscribe(payload => {
if (payload.type !== 'page') {
return;
}
@@ -20,8 +20,4 @@ export const fileNameMiddleware =
],
};
});
return () => {
beforeImportSubscription.unsubscribe();
};
};

View File

@@ -528,7 +528,7 @@ export const pasteMiddleware = (
): TransformerMiddleware => {
return ({ slots }) => {
let tr: PasteTr | undefined;
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
const { snapshot } = payload;
flatNote(snapshot);
@@ -543,18 +543,13 @@ export const pasteMiddleware = (
}
}
});
const afterImportSubscription = slots.afterImport.subscribe(payload => {
slots.afterImport.subscribe(payload => {
if (tr && payload.type === 'slice') {
tr.pasted();
tr.focusPasted();
tr.convertToLinkedDoc();
}
});
return () => {
beforeImportSubscription.unsubscribe();
afterImportSubscription.unsubscribe();
};
};
};

View File

@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
map(({ model }) => model)
);
const afterImportBlockSubscription = afterImportBlock$
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
const replaceLinkedPageIdSubscription = afterImportBlock$
afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [ParagraphBlockModel, ListBlockModel])
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
}
});
const replaceSurfaceRefIdSubscription = afterImportBlock$
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
const replaceLinkedDocIdSubscription = afterImportBlock$
afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
// Before Import
const beforeImportPageSubscription = slots.beforeImport
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;
});
const beforeImportBlockSubscription = slots.beforeImport
slots.beforeImport
.pipe(
filter(
(payload): payload is BeforeImportBlockPayload =>
@@ -244,13 +244,4 @@ export const replaceIdMiddleware =
});
}
});
return () => {
afterImportBlockSubscription.unsubscribe();
replaceLinkedPageIdSubscription.unsubscribe();
replaceSurfaceRefIdSubscription.unsubscribe();
replaceLinkedDocIdSubscription.unsubscribe();
beforeImportPageSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
};

View File

@@ -5,42 +5,33 @@ export const surfaceRefToEmbed =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
let pageId: string | null = null;
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
}
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
}
);
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
});
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();
};
});
};

View File

@@ -3,13 +3,9 @@ import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
export const titleMiddleware =
(metas: DocMeta[]): TransformerMiddleware =>
({ slots, adapterConfigs }) => {
const beforeExportSubscription = slots.beforeExport.subscribe(() => {
slots.beforeExport.subscribe(() => {
for (const meta of metas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
return () => {
beforeExportSubscription.unsubscribe();
};
};

View File

@@ -79,7 +79,7 @@ export const uploadMiddleware = (
}
}
const blockViewSubscription = blockView$
blockView$
.pipe(
map(payload => {
if (assetsManager.uploadingAssetsMap.size === 0) return null;
@@ -110,9 +110,5 @@ export const uploadMiddleware = (
)
)
.subscribe();
return () => {
blockViewSubscription.unsubscribe();
};
};
};

View File

@@ -11,9 +11,6 @@ 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;
@@ -24,20 +21,16 @@ const toURL = (str: string) => {
}
};
function resolveURL(str: string, baseUrl: string, padded = false) {
function resolveURL(str: string) {
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 =
origin === baseUrl ||
TLD_REGEXP.test(hostname) ||
(padded ? false : IPV4_ADDR_REGEXP.test(hostname));
allowed = TLD_REGEXP.test(hostname);
}
return { url, allowed };
@@ -75,10 +68,10 @@ export function normalizeUrl(str: string) {
*
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
*/
export function isValidUrl(str: string, baseUrl = location.origin) {
export function isValidUrl(str: string) {
str = str.trim();
let result = resolveURL(str, baseUrl);
let result = resolveURL(str);
if (result && !result.allowed) return false;
@@ -87,7 +80,7 @@ export function isValidUrl(str: string, baseUrl = location.origin) {
if (!hasScheme) {
const dotIdx = str.indexOf('.');
if (dotIdx > 0 && dotIdx < str.length - 1) {
result = resolveURL(`https://${str}`, baseUrl, true);
result = resolveURL(`https://${str}`);
}
}
}

View File

@@ -40,7 +40,7 @@ export const gfxBlocksFilter = (
}
return ({ slots, transformerConfigs }) => {
const beforeExportSubscription = slots.beforeExport.subscribe(payload => {
slots.beforeExport.subscribe(payload => {
if (payload.type !== 'block') {
return;
}
@@ -54,7 +54,7 @@ export const gfxBlocksFilter = (
}
});
const afterExportSubscription = slots.afterExport.subscribe(payload => {
slots.afterExport.subscribe(payload => {
if (payload.type !== 'block') {
return;
}
@@ -110,10 +110,5 @@ export const gfxBlocksFilter = (
});
}
});
return () => {
beforeExportSubscription.unsubscribe();
afterExportSubscription.unsubscribe();
};
};
};

View File

@@ -1,56 +0,0 @@
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;
}
});
};

View File

@@ -9,45 +9,36 @@ export const newIdCrossDoc =
let samePage = false;
const oldToNewIdMap = new Map<string, string>();
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;
}
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();
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;
}
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
}
);
});
return () => {
beforeImportSliceSubscription.unsubscribe();
afterImportBlockSubscription.unsubscribe();
};
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;
}
});
};

View File

@@ -7,25 +7,19 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const reorderList =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
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);
}
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();
};
});
};

View File

@@ -76,7 +76,6 @@ 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 {
@@ -1434,7 +1433,6 @@ export class DragEventWatcher {
newIdCrossDoc(std),
reorderList(std),
surfaceRefToEmbed(std),
cardStyleUpdater(std),
];
if (selectedIds) {

View File

@@ -374,8 +374,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
type: 'resize' | 'rotate';
angle: number;
handle: ResizeHandle;
flipX?: boolean;
flipY?: boolean;
pure?: boolean;
}) => {
if (!options) {
@@ -383,25 +381,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
return 'default';
}
const { type, angle, flipX, flipY } = options;
const { type, angle, handle } = 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);
@@ -645,7 +626,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
onResizeStart: () => {
this._mode = 'resize';
},
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
if (lockRatio) {
this._scaleDirection = handle;
this._scalePercent = `${Math.round(scaleX * 100)}%`;
@@ -661,8 +642,6 @@ 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: () => {
@@ -673,14 +652,6 @@ 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),

View File

@@ -1,21 +1,43 @@
import { type FrameBlockModel } from '@blocksuite/affine-model';
import { FrameBlockModel, type RootBlockModel } 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<FrameBlockModel> {
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;
}
override render() {
return html`<affine-frame-title
.model=${this.model}
data-id=${this.model.id}
></affine-frame-title>`;
return repeat(
this._frames,
({ id }) => id,
frame =>
html`<affine-frame-title
.model=${frame}
data-id=${frame.id}
></affine-frame-title>`
);
}
}
export const frameTitleWidget = WidgetViewExtension(
'affine:frame',
'affine:page',
AFFINE_FRAME_TITLE_WIDGET,
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
);

View File

@@ -14,7 +14,6 @@ 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(
@@ -136,13 +135,12 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
const frameTitleWidget = this.edgeless.std.view.getWidget(
AFFINE_FRAME_TITLE_WIDGET,
this.frameModel.id
rootBlockId
) as AffineFrameTitleWidget | null;
if (!frameTitleWidget) return nothing;
const frameTitle =
frameTitleWidget.querySelector<AffineFrameTitle>('affine-frame-title');
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
const colors = frameTitle?.colors ?? {
background: cssVarV2('edgeless/frame/background/white'),

View File

@@ -142,10 +142,12 @@ 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 = '0px';
this.style.top = '0px';
this.style.left = `${anchor[0]}px`;
this.style.top = `${anchor[1]}px`;
this.style.display = hidden ? 'none' : 'flex';
this.style.transform = transformOperation.join(' ');
this.style.maxWidth = `${maxWidth}px`;
@@ -203,6 +205,18 @@ 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 || '');

View File

@@ -566,29 +566,23 @@ Optional flag to insert before sibling
### updateBlock()
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
> **updateBlock**(`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` | `T`
`string` | `BlockModel`\<`object`\>
##### callBackOrProps
Either a callback function to execute or properties to update
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
`Partial`\<`BlockProps`\> | () => `void`
#### Returns

View File

@@ -0,0 +1,73 @@
import { describe, expect, test } from 'vitest';
import { SVGPathBuilder, SVGShapeBuilder } from '../gfx/svg-path.js';
describe('SVGPathBuilder', () => {
test('should build a simple path', () => {
const pathBuilder = new SVGPathBuilder();
const result = pathBuilder.moveTo(10, 20).lineTo(30, 40).build();
expect(result).toBe('M 10 20 L 30 40');
});
test('should build a path with curves', () => {
const pathBuilder = new SVGPathBuilder();
const result = pathBuilder
.moveTo(0, 0)
.curveTo(10, 0, 10, 10, 20, 10)
.build();
expect(result).toBe('M 0 0 C 10 0 10 10 20 10');
});
test('should build a closed path', () => {
const pathBuilder = new SVGPathBuilder();
const result = pathBuilder
.moveTo(0, 0)
.lineTo(10, 0)
.lineTo(5, 10)
.closePath()
.build();
expect(result).toBe('M 0 0 L 10 0 L 5 10 Z');
});
test('should clear commands', () => {
const pathBuilder = new SVGPathBuilder();
pathBuilder.moveTo(10, 20).lineTo(30, 40);
pathBuilder.clear();
const result = pathBuilder.moveTo(0, 0).build();
expect(result).toBe('M 0 0');
});
});
describe('SVGShapeBuilder', () => {
test('should generate diamond polygon points', () => {
const result = SVGShapeBuilder.diamond(100, 80, 2);
expect(result).toBe('50,1 99,40 50,79 1,40');
});
test('should generate triangle polygon points', () => {
const result = SVGShapeBuilder.triangle(100, 80, 2);
expect(result).toBe('50,1 99,79 1,79');
});
test('should generate diamond path', () => {
const result = SVGShapeBuilder.diamondPath(100, 80, 2);
expect(result).toBe('M 50 1 L 99 40 L 50 79 L 1 40 Z');
});
test('should generate triangle path', () => {
const result = SVGShapeBuilder.trianglePath(100, 80, 2);
expect(result).toBe('M 50 1 L 99 79 L 1 79 Z');
});
test('should handle zero stroke width', () => {
const diamondResult = SVGShapeBuilder.diamond(100, 80, 0);
expect(diamondResult).toBe('50,0 100,40 50,80 0,40');
const triangleResult = SVGShapeBuilder.triangle(100, 80, 0);
expect(triangleResult).toBe('50,0 100,80 0,80');
});
});

View File

@@ -4,4 +4,5 @@ export * from './math.js';
export * from './model/index.js';
export * from './perfect-freehand/index.js';
export * from './polyline.js';
export * from './svg-path.js';
export * from './xywh.js';

View File

@@ -0,0 +1,160 @@
interface PathCommand {
command: string;
coordinates: number[];
}
/**
* A utility class for building SVG path strings using command-based API.
* Supports moveTo, lineTo, curveTo operations and can build complete path strings.
*/
export class SVGPathBuilder {
private commands: PathCommand[] = [];
/**
* Move to a specific point without drawing
*/
moveTo(x: number, y: number): this {
this.commands.push({
command: 'M',
coordinates: [x, y],
});
return this;
}
/**
* Draw a line to a specific point
*/
lineTo(x: number, y: number): this {
this.commands.push({
command: 'L',
coordinates: [x, y],
});
return this;
}
/**
* Draw a cubic Bézier curve
*/
curveTo(
cp1x: number,
cp1y: number,
cp2x: number,
cp2y: number,
x: number,
y: number
): this {
this.commands.push({
command: 'C',
coordinates: [cp1x, cp1y, cp2x, cp2y, x, y],
});
return this;
}
/**
* Close the current path
*/
closePath(): this {
this.commands.push({
command: 'Z',
coordinates: [],
});
return this;
}
/**
* Build the complete SVG path string
*/
build(): string {
const pathSegments = this.commands.map(cmd => {
const coords = cmd.coordinates.join(' ');
return coords ? `${cmd.command} ${coords}` : cmd.command;
});
return pathSegments.join(' ');
}
/**
* Clear all commands and reset the builder
*/
clear(): this {
this.commands = [];
return this;
}
}
/**
* Create SVG polygon points string for common shapes
*/
export class SVGShapeBuilder {
/**
* Generate diamond (rhombus) polygon points
*/
static diamond(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
return [
`${width / 2},${halfStroke}`,
`${width - halfStroke},${height / 2}`,
`${width / 2},${height - halfStroke}`,
`${halfStroke},${height / 2}`,
].join(' ');
}
/**
* Generate triangle polygon points
*/
static triangle(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
return [
`${width / 2},${halfStroke}`,
`${width - halfStroke},${height - halfStroke}`,
`${halfStroke},${height - halfStroke}`,
].join(' ');
}
/**
* Generate diamond path using SVGPathBuilder
*/
static diamondPath(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
const pathBuilder = new SVGPathBuilder();
return pathBuilder
.moveTo(width / 2, halfStroke)
.lineTo(width - halfStroke, height / 2)
.lineTo(width / 2, height - halfStroke)
.lineTo(halfStroke, height / 2)
.closePath()
.build();
}
/**
* Generate triangle path using SVGPathBuilder
*/
static trianglePath(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
const pathBuilder = new SVGPathBuilder();
return pathBuilder
.moveTo(width / 2, halfStroke)
.lineTo(width - halfStroke, height - halfStroke)
.lineTo(halfStroke, height - halfStroke)
.closePath()
.build();
}
}

View File

@@ -947,34 +947,23 @@ export class InteractivityManager extends GfxExtension {
...options,
lockRatio,
elements,
onResizeMove: ({
scaleX,
scaleY,
originalBound,
handleSign,
handlePos,
currentHandlePos,
lockRatio,
}) => {
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
const suggested: {
scaleX: number;
scaleY: number;
dx: number;
dy: number;
priority?: number;
}[] = [];
const suggest = (distance: { scaleX: number; scaleY: number }) => {
const suggest = (distance: { dx: number; dy: number }) => {
suggested.push(distance);
};
extensionHandlers.forEach(ext => {
ext.onResizeMove?.({
scaleX,
scaleY,
dx,
dy,
elements,
handle,
handleSign,
handlePos,
originalBound,
currentHandlePos,
handle,
lockRatio,
suggest,
});
@@ -984,9 +973,9 @@ export class InteractivityManager extends GfxExtension {
return (a.priority ?? 0) - (b.priority ?? 0);
});
return last(suggested) ?? { scaleX, scaleY };
return last(suggested) ?? { dx, dy };
},
onResizeStart: ({ handleSign, handlePos, data }) => {
onResizeStart: ({ data }) => {
this.activeInteraction$.value = {
type: 'resize',
elements,
@@ -995,8 +984,6 @@ export class InteractivityManager extends GfxExtension {
ext.onResizeStart?.({
elements,
handle,
handlePos,
handleSign,
});
});
@@ -1058,15 +1045,13 @@ export class InteractivityManager extends GfxExtension {
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
},
onResizeEnd: ({ handleSign, handlePos, data }) => {
onResizeEnd: ({ data }) => {
this.activeInteraction$.value = null;
extensionHandlers.forEach(ext => {
ext.onResizeEnd?.({
elements,
handle,
handlePos,
handleSign,
});
});
options.onResizeEnd?.();

View File

@@ -2,7 +2,6 @@ import {
Bound,
getCommonBoundWithRotation,
type IBound,
type IPoint,
type IVec,
} from '@blocksuite/global/gfx';
@@ -30,7 +29,7 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
'bottom',
];
type ReadonlyIBound = Readonly<Required<IBound>>;
type ElementInitialSnapshot = Readonly<Required<IBound>>;
export interface OptionResize {
elements: GfxModel[];
@@ -38,18 +37,16 @@ export interface OptionResize {
lockRatio: boolean;
event: PointerEvent;
onResizeMove: (payload: {
scaleX: number;
scaleY: number;
dx: number;
dy: number;
originalBound: IBound;
handleSign: IPoint;
handlePos: IVec;
currentHandlePos: IVec;
handleSign: {
xSign: number;
ySign: number;
};
lockRatio: boolean;
}) => { scaleX: number; scaleY: number };
}) => { dx: number; dy: number };
onResizeUpdate: (payload: {
lockRatio: boolean;
scaleX: number;
@@ -62,16 +59,8 @@ export interface OptionResize {
matrix: DOMMatrix;
}[];
}) => void;
onResizeStart?: (payload: {
handlePos: IVec;
handleSign: IPoint;
data: { model: GfxModel }[];
}) => void;
onResizeEnd?: (payload: {
handlePos: IVec;
handleSign: IPoint;
data: { model: GfxModel }[];
}) => void;
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
}
export type RotateOption = {
@@ -106,102 +95,11 @@ 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,
@@ -209,32 +107,19 @@ export class ResizeController {
event,
} = options;
const originals: ReadonlyIBound[] = elements.map(el => ({
const originals: ElementInitialSnapshot[] = elements.map(el => ({
x: el.x,
y: el.y,
w: el.w,
h: el.h,
rotate: el.rotate,
}));
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 originalBound = getCommonBoundWithRotation(originals);
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
event.clientX,
event.clientY,
]);
const transform = this.getCoordsTransform(originalBound, handle);
const handleSign = {
x: transform.xSign,
y: transform.ySign,
};
const handleSign = this.getHandleSign(handle);
const onPointerMove = (e: PointerEvent) => {
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
@@ -245,69 +130,45 @@ export class ResizeController {
dx: currPt[0] - startPt[0],
dy: currPt[1] - startPt[1],
};
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,
const shouldLockRatio = lockRatio || e.shiftKey;
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,
transform,
scaleInfo,
startPt,
delta,
handleSign,
onResizeUpdate
);
} else {
this.resizeMulti(
originalBound,
originals,
elements,
transform,
scaleInfo,
startPt,
delta,
handleSign,
onResizeUpdate
);
}
};
onResizeStart?.({
handleSign,
handlePos: transform.handlePos,
data: elements.map(model => ({ model })),
});
onResizeStart?.({ data: elements.map(model => ({ model })) });
const onPointerUp = () => {
this.host.removeEventListener('pointermove', onPointerMove);
this.host.removeEventListener('pointerup', onPointerUp);
onResizeEnd?.({
handleSign,
handlePos: transform.handlePos,
data: elements.map(model => ({ model })),
});
onResizeEnd?.({ data: elements.map(model => ({ model })) });
};
this.host.addEventListener('pointermove', onPointerMove);
@@ -315,15 +176,55 @@ export class ResizeController {
}
private resizeSingle(
orig: ReadonlyIBound,
orig: ElementInitialSnapshot,
model: GfxModel,
lockRatio: boolean,
transform: ReturnType<typeof ResizeController.prototype.getCoordsTransform>,
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
startPt: IVec,
delta: {
dx: number;
dy: number;
},
handleSign: { xSign: number; ySign: number },
updateCallback: OptionResize['onResizeUpdate']
) {
const { toLocalM, toLocalRotatedM, toLocal, toModel } = transform;
const { scaleX, scaleY, scaleM } = scale;
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 [visualTopLeft, visualBottomRight] = [
new DOMPoint(orig.x, orig.y),
@@ -381,14 +282,45 @@ export class ResizeController {
}
private resizeMulti(
originals: ReadonlyIBound[],
originalBound: Bound,
originals: ElementInitialSnapshot[],
elements: GfxModel[],
transform: ReturnType<ResizeController['getCoordsTransform']>,
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
startPt: IVec,
delta: {
dx: number;
dy: number;
},
handleSign: { xSign: number; ySign: number },
updateCallback: OptionResize['onResizeUpdate']
) {
const { toLocalM } = transform;
const { scaleX, scaleY, scaleM } = scale;
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 data = elements.map((model, i) => {
const orig = originals[i];
@@ -425,7 +357,7 @@ export class ResizeController {
startRotate(option: RotateOption) {
const { event, elements, onRotateUpdate } = option;
const originals: ReadonlyIBound[] = elements.map(el => ({
const originals: ElementInitialSnapshot[] = elements.map(el => ({
x: el.x,
y: el.y,
w: el.w,
@@ -497,7 +429,7 @@ export class ResizeController {
}
private rotateSingle(option: {
orig: ReadonlyIBound;
orig: ElementInitialSnapshot;
model: GfxModel;
startPt: IVec;
currentPt: IVec;
@@ -549,7 +481,7 @@ export class ResizeController {
}
private rotateMulti(option: {
origs: ReadonlyIBound[];
origs: ElementInitialSnapshot[];
models: GfxModel[];
startPt: IVec;
currentPt: IVec;
@@ -635,23 +567,23 @@ export class ResizeController {
private getHandleSign(handle: ResizeHandle) {
switch (handle) {
case 'top-left':
return { x: -1, y: -1 };
return { xSign: -1, ySign: -1 };
case 'top':
return { x: 0, y: -1 };
return { xSign: 0, ySign: -1 };
case 'top-right':
return { x: 1, y: -1 };
return { xSign: 1, ySign: -1 };
case 'right':
return { x: 1, y: 0 };
return { xSign: 1, ySign: 0 };
case 'bottom-right':
return { x: 1, y: 1 };
return { xSign: 1, ySign: 1 };
case 'bottom':
return { x: 0, y: 1 };
return { xSign: 0, ySign: 1 };
case 'bottom-left':
return { x: -1, y: 1 };
return { xSign: -1, ySign: 1 };
case 'left':
return { x: -1, y: 0 };
return { xSign: -1, ySign: 0 };
default:
return { x: 0, y: 0 };
return { xSign: 0, ySign: 0 };
}
}
}

View File

@@ -1,5 +1,3 @@
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
import type { GfxModel } from '../../model/model';
import type { ResizeHandle } from '../resize/manager';
@@ -10,16 +8,6 @@ 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;
};
@@ -28,14 +16,15 @@ export type ExtensionElementResizeEndContext =
export type ExtensionElementResizeMoveContext =
ExtensionElementResizeStartContext & {
scaleX: number;
scaleY: number;
originalBound: IBound;
currentHandlePos: IVec;
dx: number;
dy: number;
lockRatio: boolean;
suggest: (distance: { scaleX: number; scaleY: number }) => void;
handleSign: {
xSign: number;
ySign: number;
};
suggest: (distance: { dx: number; dy: number }) => void;
};

View File

@@ -825,7 +825,7 @@ export class LayerManager extends GfxExtension {
const block = store.getModelById(payload.id);
if (block instanceof GfxBlockElementModel) {
this.delete(block);
this.delete(block as GfxBlockElementModel);
}
}
})
@@ -834,29 +834,20 @@ export class LayerManager extends GfxExtension {
const watchSurface = (surface: SurfaceBlockModel) => {
let lastChildMap = new Map(surface.childMap.peek());
this._disposable.add(
surface.childMap.subscribe(currentChildMap => {
currentChildMap.forEach((_, id) => {
surface.childMap.subscribe(val => {
val.forEach((_, id) => {
if (lastChildMap.has(id)) {
lastChildMap.delete(id);
return;
}
});
lastChildMap.forEach((_, id) => {
const model = this._doc.getModelById(id);
if (model instanceof GfxBlockElementModel) {
this.delete(model);
const block = this._doc.getBlock(id);
if (block?.model) {
this.delete(block.model as GfxBlockElementModel);
}
});
currentChildMap.forEach((_, id) => {
const model = store.getModelById(id);
if (
model instanceof GfxBlockElementModel &&
!this.blocks.includes(model)
) {
this.add(model);
}
});
lastChildMap = new Map(currentChildMap);
lastChildMap = new Map(val);
})
);

View File

@@ -19,5 +19,3 @@ export type BlockSysProps = {
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;
export type PropsOfModel<T> = T extends BlockModel<infer P> ? P : never;

View File

@@ -22,8 +22,6 @@ import {
type BlockModel,
type BlockOptions,
type BlockProps,
type BlockSysProps,
type PropsOfModel,
type YBlock,
} from '../block/index.js';
import { DocCRUD } from './crud.js';
@@ -854,12 +852,9 @@ export class Store {
*
* @category Block CRUD
*/
updateBlock<T extends BlockModel = BlockModel>(
modelOrId: T | string,
callBackOrProps:
| (() => void)
| Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps>
updateBlock(
modelOrId: BlockModel | string,
callBackOrProps: (() => void) | Partial<BlockProps>
) {
if (this.readonly) {
console.error('cannot modify data in readonly mode');

View File

@@ -113,8 +113,6 @@ type TransformerMiddlewareOptions = {
transformerConfigs: Map<string, unknown>;
};
type TransformerMiddlewareCleanup = () => void;
export type TransformerMiddleware = (
options: TransformerMiddlewareOptions
) => void | TransformerMiddlewareCleanup;
) => void;

View File

@@ -1,4 +1,3 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { nextTick } from '@blocksuite/global/utils';
import { Subject } from 'rxjs';
@@ -68,8 +67,6 @@ 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>(),
@@ -369,16 +366,13 @@ export class Transformer {
this._docCRUD = docCRUD;
middlewares.forEach(middleware => {
const cleanup = middleware({
middleware({
slots: this._slots,
docCRUD: this._docCRUD,
assetsManager: this._assetsManager,
adapterConfigs: this._adapterConfigs,
transformerConfigs: this._transformerConfigs,
});
if (cleanup) {
this._disposables.add(cleanup);
}
});
}
@@ -652,9 +646,4 @@ export class Transformer {
reset() {
this._assetsManager.cleanup();
}
[Symbol.dispose]() {
this._disposables.dispose();
this._assetsManager.cleanup();
}
}

View File

@@ -132,13 +132,12 @@ export class DocEngine {
this.logger
);
const subscriber = state.mainPeer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
});
cleanUp.push(() => {
subscriber.unsubscribe();
});
cleanUp.push(
state.mainPeer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
}).unsubscribe
);
this.updateSyncingState(state.mainPeer, state.shadowPeers);
@@ -153,15 +152,12 @@ export class DocEngine {
this.priorityTarget,
this.logger
);
const subscriber = peer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
});
cleanUp.push(() => {
subscriber.unsubscribe();
});
cleanUp.push(
peer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
}).unsubscribe
);
return peer;
});

View File

@@ -0,0 +1,158 @@
import { DomRenderer } from '@blocksuite/affine-block-surface';
import { beforeEach, describe, expect, test } from 'vitest';
import { wait } from '../utils/common.js';
import { getSurface } from '../utils/edgeless.js';
import { setupEditor } from '../utils/setup.js';
describe('Connector rendering with DOM renderer', () => {
beforeEach(async () => {
const cleanup = await setupEditor('edgeless', [], {
enableDomRenderer: true,
});
return cleanup;
});
test('should use DomRenderer when enable_dom_renderer flag is true', async () => {
const surface = getSurface(doc, editor);
expect(surface).not.toBeNull();
expect(surface?.renderer).toBeInstanceOf(DomRenderer);
});
test('should render a connector element as a DOM node', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
// Create two shapes to connect
const shape1Id = surfaceModel.addElement({
type: 'shape',
xywh: '[100, 100, 80, 60]',
});
const shape2Id = surfaceModel.addElement({
type: 'shape',
xywh: '[300, 200, 80, 60]',
});
// Create a connector between the shapes
const connectorProps = {
type: 'connector',
source: { id: shape1Id },
target: { id: shape2Id },
stroke: '#000000',
strokeWidth: 2,
};
const connectorId = surfaceModel.addElement(connectorProps);
await wait(100);
const connectorElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
expect(connectorElement).toBeInstanceOf(HTMLElement);
// Check if SVG element is present for connector rendering
const svgElement = connectorElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
});
test('should render connector with different stroke styles', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
// Create a dashed connector
const connectorProps = {
type: 'connector',
source: { position: [100, 100] },
target: { position: [200, 200] },
strokeStyle: 'dash',
stroke: '#ff0000',
strokeWidth: 4,
};
const connectorId = surfaceModel.addElement(connectorProps);
// Wait for path generation and rendering
await wait(500);
const connectorElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
const svgElement = connectorElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
// Find the main path element (not the ones inside markers)
const pathElements = svgElement?.querySelectorAll('path');
// The main connector path should be the last one (after marker paths)
const pathElement = pathElements?.[pathElements.length - 1];
expect(pathElement).not.toBeNull();
// Check stroke-dasharray attribute
const strokeDasharray = pathElement!.getAttribute('stroke-dasharray');
expect(strokeDasharray).toBe('12,12');
});
test('should render connector with arrow endpoints', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const connectorProps = {
type: 'connector',
source: { position: [100, 100] },
target: { position: [200, 200] },
frontEndpointStyle: 'Triangle',
rearEndpointStyle: 'Arrow',
};
const connectorId = surfaceModel.addElement(connectorProps);
await wait(100);
const connectorElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
// Check for markers in defs
const defsElement = connectorElement?.querySelector('defs');
expect(defsElement).not.toBeNull();
const markers = defsElement?.querySelectorAll('marker');
expect(markers?.length).toBeGreaterThan(0);
});
test('should remove connector DOM node when element is deleted', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
expect(surfaceView.renderer).toBeInstanceOf(DomRenderer);
const connectorProps = {
type: 'connector',
source: { position: [50, 50] },
target: { position: [150, 150] },
};
const connectorId = surfaceModel.addElement(connectorProps);
await wait(100);
let connectorElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
surfaceModel.deleteElement(connectorId);
await wait(100);
connectorElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).toBeNull();
});
});

View File

@@ -31,15 +31,12 @@ describe('frame', () => {
);
await wait();
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 frameTitleWidget = service.std.view.getWidget(
'affine-frame-title-widget',
doc.root!.id
) as AffineFrameTitleWidget | null;
const frameTitle = getFrameTitle(frame);
const frameTitle = frameTitleWidget?.getFrameTitle(frame);
const rect = frameTitle?.getBoundingClientRect();
expect(frameTitle).toBeTruthy();
@@ -61,7 +58,7 @@ describe('frame', () => {
);
await wait();
const nestedTitle = getFrameTitle(nestedFrame);
const nestedTitle = frameTitleWidget?.getFrameTitle(nestedFrame);
expect(nestedTitle).toBeTruthy();
if (!nestedTitle) return;

View File

@@ -40,7 +40,6 @@ describe('basic', () => {
xywh: '[100, 0, 100, 100]',
index: service.generateIndex(),
})!;
await wait(0); // wait next frame
frameId = service.crud.addBlock(
'affine:frame',
{

View File

@@ -7,40 +7,30 @@ 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) => {
let doc = collection.getDoc(id);
const hasDoc = !!doc;
if (!doc) {
doc = collection.createDoc(id);
}
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);
const store = doc.getStore({ id });
store.load();
// Add note block inside root block
const noteId = doc.addBlock(
'affine:note',
{ xywh: '[0, 100, 800, 640]' },
rootId
);
// 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);
// Import preset markdown content inside note block
await MarkdownTransformer.importMarkdownToBlock({
doc,
blockId: noteId,
markdown: presetMarkdown,
extensions: getTestStoreManager().get('store'),
});
// 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();
doc.resetHistory();
};
preset.id = 'preset';

View File

@@ -24,7 +24,6 @@
"@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",

View File

@@ -1,8 +1,6 @@
@import '@toeverything/theme/style.css';
@import '@toeverything/theme/fonts.css';
@import 'katex/dist/katex.min.css';
@font-face {
font-family: 'color-emoji';
src:

View File

@@ -8,11 +8,6 @@ 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
@@ -27,11 +22,6 @@ export declare function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
export interface ParsedDoc {
name: string
chunks: Array<Chunk>
}
export declare function parseDoc(filePath: string, doc: Buffer): Promise<ParsedDoc>
export declare function parseDoc(filePath: string, doc: Buffer): Promise<{ name: string, chunks: Array<{index: number, content: string}> }>
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>

View File

@@ -32,7 +32,7 @@
"build:debug": "napi build"
},
"devDependencies": {
"@napi-rs/cli": "3.0.0-alpha.81",
"@napi-rs/cli": "3.0.0-alpha.78",
"lib0": "^0.2.99",
"tiktoken": "^1.0.17",
"tinybench": "^4.0.0",

View File

@@ -2,21 +2,9 @@ use affine_common::doc_loader::Doc;
use napi::{
anyhow::anyhow,
bindgen_prelude::{AsyncTask, Buffer},
Env, Result, Task,
Env, JsObject, 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,
}
@@ -26,20 +14,24 @@ impl Document {
self.inner.name.clone()
}
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>>()
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)
}
}
@@ -51,22 +43,21 @@ pub struct AsyncParseDocResponse {
#[napi]
impl Task for AsyncParseDocResponse {
type Output = Document;
type JsValue = ParsedDoc;
type JsValue = JsObject;
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, doc: Document) -> Result<Self::JsValue> {
Ok(ParsedDoc {
name: doc.name(),
chunks: doc.chunks(),
})
fn resolve(&mut self, env: Env, doc: Document) -> Result<Self::JsValue> {
doc.resolve(env)
}
}
#[napi]
#[napi(
ts_return_type = "Promise<{ name: string, chunks: Array<{index: number, content: string}> }>"
)]
pub fn parse_doc(file_path: String, doc: Buffer) -> AsyncTask<AsyncParseDocResponse> {
AsyncTask::new(AsyncParseDocResponse {
file_path,

View File

@@ -1,7 +1,7 @@
use std::convert::TryFrom;
use affine_common::hashcash::Stamp;
use napi::{bindgen_prelude::AsyncTask, Env, Result as NapiResult, Task};
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, 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 = bool;
type JsValue = JsBoolean;
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, output: bool) -> NapiResult<Self::JsValue> {
Ok(output)
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
env.get_boolean(output)
}
}
@@ -49,14 +49,14 @@ pub struct AsyncMintChallengeResponse {
#[napi]
impl Task for AsyncMintChallengeResponse {
type Output = String;
type JsValue = String;
type JsValue = JsString;
fn compute(&mut self) -> NapiResult<Self::Output> {
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
}
fn resolve(&mut self, _: Env, output: String) -> NapiResult<Self::JsValue> {
Ok(output)
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
env.create_string(&output)
}
}

View File

@@ -1,9 +1,2 @@
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
$$;
-- AlterTable
ALTER TABLE "ai_workspace_files" ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';

View File

@@ -5,25 +5,16 @@
- 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.
*/
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_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");
END IF;
END
$$;
-- 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");

View File

@@ -28,11 +28,11 @@
"dependencies": {
"@affine/reader": "workspace:*",
"@affine/server-native": "workspace:*",
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/anthropic": "^1.2.10",
"@ai-sdk/google": "^1.2.18",
"@ai-sdk/google-vertex": "^2.2.23",
"@ai-sdk/openai": "^1.3.22",
"@ai-sdk/perplexity": "^1.1.9",
"@ai-sdk/google-vertex": "^2.2.22",
"@ai-sdk/openai": "^1.3.21",
"@ai-sdk/perplexity": "^1.1.6",
"@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": "^6.0.0",
"nestjs-cls": "^5.0.0",
"nodemailer": "^7.0.0",
"on-headers": "^1.0.2",
"piscina": "^5.0.0-alpha.0",

View File

@@ -47,33 +47,5 @@ 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();

View File

@@ -272,68 +272,13 @@ 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! Im 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 AFFiNE AI?',
content: 'what is ssot',
params: {
files: [
{

View File

@@ -79,7 +79,7 @@ export class MockCopilotProvider extends OpenAIProvider {
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Structured],
output: [ModelOutputType.Text],
},
],
},

View File

@@ -48,7 +48,7 @@ export class JobQueue {
const removed = await queue.remove(jobId);
if (removed) {
this.logger.log(`Job ${jobName}(id=${jobId}) removed from queue ${ns}`);
this.logger.log(`Job ${jobName} removed from queue ${ns}`);
return job.data.payload;
}

View File

@@ -1,135 +0,0 @@
# 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

View File

@@ -1,37 +0,0 @@
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);
});

View File

@@ -1,78 +0,0 @@
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);
},
};

View File

@@ -1,4 +1,3 @@
export * from './duration';
export * from './promise';
export * from './request';
export * from './stream';

View File

@@ -203,9 +203,7 @@ export class SpaceSyncGateway
await client.join(room);
}
} else {
if (spaceType === SpaceType.Workspace) {
this.event.emit('workspace.embedding', { workspaceId: spaceId });
}
this.event.emit('workspace.embedding', { workspaceId: spaceId });
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
}

View File

@@ -1,75 +1,41 @@
import { Logger } from '@nestjs/common';
import {
createOpenAI,
type OpenAIProvider as VercelOpenAIProvider,
} from '@ai-sdk/openai';
import { embedMany, generateObject } from 'ai';
import { chunk } from 'lodash-es';
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 { ChunkSimilarity, Embedding } from '../../../models';
import { OpenAIConfig } from '../providers/openai';
import {
EMBEDDING_DIMENSIONS,
EmbeddingClient,
getReRankSchema,
type ReRankResult,
ReRankResult,
} from './types';
const RERANK_PROMPT = 'Rerank results';
const RERANK_MODEL = 'gpt-4.1-mini';
export class ProductionEmbeddingClient extends EmbeddingClient {
private readonly logger = new Logger(ProductionEmbeddingClient.name);
export class OpenAIEmbeddingClient extends EmbeddingClient {
readonly #instance: VercelOpenAIProvider;
constructor(
private readonly providerFactory: CopilotProviderFactory,
private readonly prompt: PromptService
) {
constructor(config: OpenAIConfig) {
super();
}
override async configured(): Promise<boolean> {
const embedding = await this.providerFactory.getProvider({
outputType: ModelOutputType.Embedding,
this.#instance = createOpenAI({
apiKey: config.apiKey,
baseURL: config.baseUrl,
});
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 provider = await this.getProvider({
outputType: ModelOutputType.Embedding,
const modelInstance = this.#instance.embedding('text-embedding-3-large', {
dimensions: EMBEDDING_DIMENSIONS,
});
this.logger.verbose(`Using provider ${provider.type} for embedding`, input);
const embeddings = await provider.embedding(
{ inputTypes: [ModelInputType.Text] },
input,
{ dimensions: EMBEDDING_DIMENSIONS }
);
const { embeddings } = await embedMany({
model: modelInstance,
values: input,
});
return Array.from(embeddings.entries()).map(([index, embedding]) => ({
index,
@@ -78,6 +44,27 @@ export class ProductionEmbeddingClient 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,
>(
@@ -85,36 +72,19 @@ export class ProductionEmbeddingClient extends EmbeddingClient {
embeddings: Chunk[],
signal?: AbortSignal
): Promise<ReRankResult> {
if (!embeddings.length) return [];
const prompt = this.getRelevancePrompt(query, embeddings);
const modelInstance = this.#instance(RERANK_MODEL);
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 [];
}
const {
object: { ranks },
} = await generateObject({
model: modelInstance,
prompt,
schema: getReRankSchema(embeddings.length),
maxRetries: 3,
abortSignal: signal,
});
return ranks;
}
override async reRank<Chunk extends ChunkSimilarity = ChunkSimilarity>(
@@ -140,10 +110,6 @@ export class ProductionEmbeddingClient 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);
}
@@ -158,21 +124,6 @@ export class ProductionEmbeddingClient 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) => ({

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import {
AFFiNELogger,
BlobNotFound,
CallMetric,
Config,
CopilotContextFileNotSupported,
DocNotFound,
EventBus,
@@ -14,11 +14,9 @@ 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 { getEmbeddingClient } from './embedding';
import { OpenAIEmbeddingClient } from './embedding';
import type { Chunk, DocFragment } from './types';
import { EMBEDDING_DIMENSIONS, EmbeddingClient } from './types';
@@ -31,12 +29,11 @@ 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
) {
@@ -56,8 +53,10 @@ export class CopilotContextDocJob {
private async setup() {
this.supportEmbedding =
await this.models.copilotContext.checkEmbeddingAvailable();
if (this.supportEmbedding) {
this.client = await getEmbeddingClient(this.providerFactory, this.prompt);
if (this.supportEmbedding && this.config.copilot.providers.openai.apiKey) {
this.client = new OpenAIEmbeddingClient(
this.config.copilot.providers.openai
);
}
}
@@ -66,7 +65,6 @@ export class CopilotContextDocJob {
return this.client as EmbeddingClient;
}
@CallMetric('ai', 'addFileEmbeddingQueue')
async addFileEmbeddingQueue(file: Jobs['copilot.embedding.files']) {
if (!this.supportEmbedding) return;
@@ -89,14 +87,6 @@ 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',
{
@@ -107,7 +97,6 @@ export class CopilotContextDocJob {
{
jobId: `workspace:embedding:${workspaceId}:${docId}`,
priority: options?.priority ?? 1,
timestamp: Date.now(),
}
);
}
@@ -140,10 +129,6 @@ 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',
@@ -195,7 +180,7 @@ export class CopilotContextDocJob {
);
}
private async readCopilotBlob(
async readCopilotBlob(
userId: string,
workspaceId: string,
blobId: string,
@@ -345,9 +330,6 @@ 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);
@@ -389,9 +371,6 @@ 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;

View File

@@ -2,6 +2,7 @@ import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import {
Cache,
Config,
CopilotInvalidContext,
NoCopilotProviderAvailable,
OnEvent,
@@ -14,11 +15,9 @@ import {
ContextFile,
Models,
} from '../../../models';
import { PromptService } from '../prompt';
import { CopilotProviderFactory } from '../providers';
import { getEmbeddingClient } from './embedding';
import { OpenAIEmbeddingClient } from './embedding';
import { ContextSession } from './session';
import type { EmbeddingClient } from './types';
import { EmbeddingClient } from './types';
const CONTEXT_SESSION_KEY = 'context-session';
@@ -28,24 +27,26 @@ export class CopilotContextService implements OnApplicationBootstrap {
private client: EmbeddingClient | undefined;
constructor(
private readonly config: Config,
private readonly cache: Cache,
private readonly models: Models,
private readonly providerFactory: CopilotProviderFactory,
private readonly prompt: PromptService
private readonly models: Models
) {}
@OnEvent('config.init')
async onConfigInit() {
await this.setup();
onConfigInit() {
this.setup();
}
@OnEvent('config.changed')
async onConfigChanged() {
await this.setup();
onConfigChanged() {
this.setup();
}
private async setup() {
this.client = await getEmbeddingClient(this.providerFactory, this.prompt);
private setup() {
const configure = this.config.copilot.providers.openai;
if (configure.apiKey) {
this.client = new OpenAIEmbeddingClient(configure);
}
}
async onApplicationBootstrap() {

View File

@@ -69,10 +69,6 @@ export type Chunk = {
export const EMBEDDING_DIMENSIONS = 1024;
export abstract class EmbeddingClient {
async configured() {
return true;
}
async getFileEmbeddings(
file: File,
chunkMapper: (chunk: Chunk[]) => Chunk[],

View File

@@ -335,66 +335,7 @@ 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',
@@ -1617,7 +1558,7 @@ const imageActions: Prompt[] = [
];
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
model: 'claude-sonnet-4@20250514',
model: 'gpt-4.1',
optionalModels: [
'gpt-4.1',
'o3',

View File

@@ -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, true, true);
const [system, msgs] = await chatToGPTMessage(messages);
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) {
// claude 3.5 sonnet doesn't support reasoning config
return model.includes('sonnet') && !model.startsWith('claude-3-5-sonnet');
// only claude 3.7 sonnet supports reasoning config
return model.startsWith('claude-3-7-sonnet');
}
}

View File

@@ -103,7 +103,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Structured],
output: [ModelOutputType.Text],
defaultForOutputType: true,
},
],
@@ -113,7 +113,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Structured],
output: [ModelOutputType.Text],
},
],
},
@@ -122,16 +122,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Structured],
},
],
},
{
id: 'gpt-4.1-nano',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Structured],
output: [ModelOutputType.Text],
},
],
},
@@ -292,8 +283,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),
},
@@ -331,10 +322,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),
},
@@ -397,9 +388,8 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
model: modelInstance,
system,
messages: msgs,
temperature: options.temperature ?? 0,
maxTokens: options.maxTokens ?? 4096,
maxRetries: options.maxRetries ?? 3,
temperature: ('temperature' in options && options.temperature) || 0,
maxTokens: ('maxTokens' in options && options.maxTokens) || 4096,
schema,
providerOptions: {
openai: options.user ? { user: options.user } : {},

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