Compare commits

...

20 Commits

Author SHA1 Message Date
Chen
5526696357 fix: update toggle switch active class name 2025-04-11 10:31:02 +08:00
cuikaipeng
5be0292536 fix(editor): the switch button style set in the TOC does not effect 2025-04-10 21:59:03 +08:00
L-Sun
823bf40a57 fix(editor): fix overlay of tool is not shown or repeated when switching tool (#11575)
Close [BS-3029](https://linear.app/affine-design/issue/BS-3029/frame-里面的-shape-没办法进入文本编辑模式)
Close [BS-3082](https://linear.app/affine-design/issue/BS-3082/按s切换至shape工具,在白板上点击会创建两个shape)
Close [BS-3091](https://linear.app/affine-design/issue/BS-3082/按s切换至shape工具,在白板上点击会创建两个shape)

## Fix Shape Tool Issues

This PR addresses several issues with the shape and mindmap tools functionality in the editor:

1. **Fix text editing after mode switching**: Resolves an issue where users couldn't edit text in shapes after switching editor modes. The fix ensures the edgeless block is properly retrieved when double-clicking on a shape.

2. **Improve tool switching behavior**: Fixes issues with tool overlays not showing or being repeated when switching between tools. This includes:
   - Properly handling tool overlay visibility
   - Ensuring only one tool is active at a time when using keyboard shortcuts
   - Adding proper cleanup when switching tools

3. **Add comprehensive tests**: Adds new test cases to verify:
   - Shape creation with keyboard shortcuts
   - Shape text editing after mode switching
   - Tool switching behavior with keyboard shortcuts
2025-04-10 13:39:22 +00:00
L-Sun
588659ef67 fix(editor): connector target position NaN (#11606)
Close [BS-3086](https://linear.app/affine-design/issue/BS-3086/frame里套frame,连一下connector,拖两下,白板损坏)

### What Changes
- Fixed `bound.toRelative` may be return `NaN` when `bound.w === 0 || bound.h ===0`
- Remove type assertions from `connector-manager.ts` for more type safety
2025-04-10 12:33:24 +00:00
donteatfriedrice
d5aebc1421 fix(editor): enhance markdown latex preprocessing (#11597)
Close [BS-2440](https://linear.app/affine-design/issue/BS-2440/inline-latex-markdown-adapter-需要更精确的处理)
2025-04-10 12:15:24 +00:00
yoyoyohamapi
25418b402a fix(core): replace zero-width non-ioiner for editor content​ (#11591) 2025-04-10 11:55:31 +00:00
liuyi
f0fb1447a4 chore(core): update license price link (#11616) 2025-04-10 19:54:48 +08:00
EYHN
0f39ab4ea4 fix(core): not revalidate notification count when logged out (#11617) 2025-04-10 19:54:36 +08:00
renovate
ffad5d0a2e chore: bump up nestjs to v11.0.15 (#11615)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@nestjs/common](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common)) | [`11.0.14` -> `11.0.15`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.0.14/11.0.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcommon/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcommon/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/core](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core)) | [`11.0.14` -> `11.0.15`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.0.14/11.0.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcore/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcore/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/platform-express](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-express)) | [`11.0.14` -> `11.0.15`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/11.0.14/11.0.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-express/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-express/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-express/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-express/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/platform-socket.io](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io)) | [`11.0.14` -> `11.0.15`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-socket.io/11.0.14/11.0.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-socket.io/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-socket.io/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-socket.io/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-socket.io/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/websockets](https://redirect.github.com/nestjs/nest) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/websockets)) | [`11.0.14` -> `11.0.15`](https://renovatebot.com/diffs/npm/@nestjs%2fwebsockets/11.0.14/11.0.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fwebsockets/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fwebsockets/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fwebsockets/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fwebsockets/11.0.14/11.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>nestjs/nest (@&#8203;nestjs/common)</summary>

### [`v11.0.15`](https://redirect.github.com/nestjs/nest/releases/tag/v11.0.15)

[Compare Source](https://redirect.github.com/nestjs/nest/compare/v11.0.14...v11.0.15)

##### v11.0.15 (2025-04-10)

##### Bug fixes

-   `platform-fastify`
    -   [#&#8203;14935](https://redirect.github.com/nestjs/nest/pull/14935) fix(fastify): methods comparison ([@&#8203;johaven](https://redirect.github.com/johaven))

##### Committers: 1

-   Johan Legrand ([@&#8203;johaven](https://redirect.github.com/johaven))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-10 11:28:10 +00:00
JimmFly
a166760041 fix(server): adjust expire time of invite link (#11609) 2025-04-10 11:13:27 +00:00
liuyi
e79e4c9e9b refactor(core): add version in worker url (#11614)
Co-authored-by: renovate <29139614+renovate@users.noreply.github.com>
2025-04-10 11:12:49 +00:00
renovate
a6ddfdd85e chore: bump up oxlint version to v0.16.5 (#11612)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.16.4` -> `0.16.5`](https://renovatebot.com/diffs/npm/oxlint/0.16.4/0.16.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.16.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.16.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.16.4/0.16.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.16.4/0.16.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.16.5`](https://redirect.github.com/oxc-project/oxc/releases/tag/oxlint_v0.16.5): oxlint v0.16.5

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v0.16.4...oxlint_v0.16.5)

#### \[0.16.5] - 2025-04-07

Reduced Oxlint binary size from 6.32 MB to 5.8 MB.

##### Features

-   [`2f6810a`](https://redirect.github.com/oxc-project/oxc/commit/2f6810a) editor: Add named fixes for code actions ([#&#8203;10203](https://redirect.github.com/oxc-project/oxc/issues/10203)) (camchenry)
-   [`32b9d1e`](https://redirect.github.com/oxc-project/oxc/commit/32b9d1e) language_server: Add `fix_kind` flag ([#&#8203;10226](https://redirect.github.com/oxc-project/oxc/issues/10226)) (Sysix)
-   [`dab1bd8`](https://redirect.github.com/oxc-project/oxc/commit/dab1bd8) language_server: Search for nested configurations by initialization ([#&#8203;10120](https://redirect.github.com/oxc-project/oxc/issues/10120)) (Sysix)
-   [`794b180`](https://redirect.github.com/oxc-project/oxc/commit/794b180) linter: Add messages for complex fixes ([#&#8203;10279](https://redirect.github.com/oxc-project/oxc/issues/10279)) (camchenry)
-   [`bde73b5`](https://redirect.github.com/oxc-project/oxc/commit/bde73b5) linter: Add unicorn/no-accessor-recursion rule ([#&#8203;9971](https://redirect.github.com/oxc-project/oxc/issues/9971)) (yefan)

##### Bug Fixes

-   [`03ba760`](https://redirect.github.com/oxc-project/oxc/commit/03ba760) linter: `jsdoc/require-param`: skip rule if any doc has `@type` tag ([#&#8203;10282](https://redirect.github.com/oxc-project/oxc/issues/10282)) (Cam McHenry)
-   [`f2eff56`](https://redirect.github.com/oxc-project/oxc/commit/f2eff56) linter: Fix `rule_id` for some diagnostics formats ([#&#8203;10251](https://redirect.github.com/oxc-project/oxc/issues/10251)) (Alexander S.)
-   [`7c54ea1`](https://redirect.github.com/oxc-project/oxc/commit/7c54ea1) linter: Rule `no-restricted-imports` allow combination of `paths` and `patterns` ([#&#8203;10224](https://redirect.github.com/oxc-project/oxc/issues/10224)) (Sysix)
-   [`6174129`](https://redirect.github.com/oxc-project/oxc/commit/6174129) linter: Run `react/no-children-props` only when react framework is found ([#&#8203;10225](https://redirect.github.com/oxc-project/oxc/issues/10225)) (Sysix)
-   [`cc1267e`](https://redirect.github.com/oxc-project/oxc/commit/cc1267e) linter: Fix `Display` impl for `ConfigBuilderError` ([#&#8203;10239](https://redirect.github.com/oxc-project/oxc/issues/10239)) (overlookmotel)

##### Documentation

-   [`f115f71`](https://redirect.github.com/oxc-project/oxc/commit/f115f71) editor: Add readme block for possible configurations ([#&#8203;10243](https://redirect.github.com/oxc-project/oxc/issues/10243)) (Sysix)
-   [`3d4ed3e`](https://redirect.github.com/oxc-project/oxc/commit/3d4ed3e) linter: Rule `eslint/eqeqeq` add "null" & "smart" options ([#&#8203;10258](https://redirect.github.com/oxc-project/oxc/issues/10258)) (Jacob Smith)
-   [`ec34ef3`](https://redirect.github.com/oxc-project/oxc/commit/ec34ef3) rules/react: Adding missing code block ending ([#&#8203;10218](https://redirect.github.com/oxc-project/oxc/issues/10218)) (Cannonbark)

##### Testing

-   [`297d07f`](https://redirect.github.com/oxc-project/oxc/commit/297d07f) editor: Add e2e tests for creating oxlint configurations on the fly ([#&#8203;10138](https://redirect.github.com/oxc-project/oxc/issues/10138)) (Sysix)
-   [`29be469`](https://redirect.github.com/oxc-project/oxc/commit/29be469) editor: Add test for code actions ([#&#8203;10168](https://redirect.github.com/oxc-project/oxc/issues/10168)) (camchenry)
-   [`ba817a9`](https://redirect.github.com/oxc-project/oxc/commit/ba817a9) editor: Add E2E Diagnostics test ([#&#8203;10133](https://redirect.github.com/oxc-project/oxc/issues/10133)) (Sysix)
-   [`72238fc`](https://redirect.github.com/oxc-project/oxc/commit/72238fc) linter: Ensure complex fixes have messages ([#&#8203;10280](https://redirect.github.com/oxc-project/oxc/issues/10280)) (camchenry)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-10 10:09:49 +00:00
doodlewind
dba8e00fb6 feat(editor): add basic note support in turbo renderer (#11607)
After landing layout tree refactoring, this PR adds basic note support in turbo renderer.

In this demo recording, the code and image block needs to be further supported.

[Screen Recording 2025-04-10 at 5.16.15 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/lEGcysB4lFTEbCwZ8jMv/2e416b41-5609-4e52-a90f-5b7bb77db682.mov" />](https://app.graphite.dev/media/video/lEGcysB4lFTEbCwZ8jMv/2e416b41-5609-4e52-a90f-5b7bb77db682.mov)
2025-04-10 09:52:33 +00:00
doodlewind
69d4620753 refactor(editor): cleanup turbo renderer entry config with doc guide (#11605) 2025-04-10 09:52:32 +00:00
renovate
dbf09ea055 chore: bump up nestjs to v11.0.14 (#11608)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@nestjs/common](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common)) | [`11.0.13` -> `11.0.14`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.0.13/11.0.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcommon/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcommon/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/core](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core)) | [`11.0.13` -> `11.0.14`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.0.13/11.0.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcore/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcore/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/platform-express](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-express)) | [`11.0.13` -> `11.0.14`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/11.0.13/11.0.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-express/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-express/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-express/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-express/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/platform-socket.io](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io)) | [`11.0.13` -> `11.0.14`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-socket.io/11.0.13/11.0.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-socket.io/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-socket.io/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-socket.io/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-socket.io/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/websockets](https://redirect.github.com/nestjs/nest) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/websockets)) | [`11.0.13` -> `11.0.14`](https://renovatebot.com/diffs/npm/@nestjs%2fwebsockets/11.0.13/11.0.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fwebsockets/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fwebsockets/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fwebsockets/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fwebsockets/11.0.13/11.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>nestjs/nest (@&#8203;nestjs/common)</summary>

### [`v11.0.14`](https://redirect.github.com/nestjs/nest/compare/v11.0.13...dbc03787d5c1ccf9929dd1be7a1369b27ae95449)

[Compare Source](https://redirect.github.com/nestjs/nest/compare/v11.0.13...v11.0.14)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/core)</summary>

### [`v11.0.14`](https://redirect.github.com/nestjs/nest/compare/v11.0.13...dbc03787d5c1ccf9929dd1be7a1369b27ae95449)

[Compare Source](https://redirect.github.com/nestjs/nest/compare/v11.0.13...v11.0.14)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/platform-express)</summary>

### [`v11.0.14`](https://redirect.github.com/nestjs/nest/compare/v11.0.13...dbc03787d5c1ccf9929dd1be7a1369b27ae95449)

[Compare Source](https://redirect.github.com/nestjs/nest/compare/v11.0.13...v11.0.14)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/platform-socket.io)</summary>

### [`v11.0.14`](https://redirect.github.com/nestjs/nest/releases/tag/v11.0.14)

[Compare Source](https://redirect.github.com/nestjs/nest/compare/v11.0.13...v11.0.14)

#### v11.0.14 (2025-04-09)

##### Bug fixes

-   `platform-fastify`
    -   [#&#8203;14511](https://redirect.github.com/nestjs/nest/pull/14511) fix(fastify): adds the non-standard http methods to the instance ([@&#8203;johaven](https://redirect.github.com/johaven))

##### Committers: 1

-   Johan Legrand ([@&#8203;johaven](https://redirect.github.com/johaven))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-10 09:36:36 +00:00
darkskygit
2822146a4d ci(server): update copilot ci condition (#11601) 2025-04-10 09:19:26 +00:00
renovate
c36dc9318c chore: bump up all non-major dependencies (#11600)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence | Type | Update |
|---|---|---|---|---|---|---|---|
| [@react-email/components](https://redirect.github.com/resend/react-email) ([source](https://redirect.github.com/resend/react-email/tree/HEAD/packages/components)) | [`0.0.35` -> `0.0.36`](https://renovatebot.com/diffs/npm/@react-email%2fcomponents/0.0.35/0.0.36) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@react-email%2fcomponents/0.0.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@react-email%2fcomponents/0.0.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@react-email%2fcomponents/0.0.35/0.0.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@react-email%2fcomponents/0.0.35/0.0.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [bullmq](https://bullmq.io/) ([source](https://redirect.github.com/taskforcesh/bullmq)) | [`5.48.0` -> `5.48.1`](https://renovatebot.com/diffs/npm/bullmq/5.48.0/5.48.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/bullmq/5.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/bullmq/5.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/bullmq/5.48.0/5.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/bullmq/5.48.0/5.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [cc](https://redirect.github.com/rust-lang/cc-rs) | `1.2.17` -> `1.2.18` | [![age](https://developer.mend.io/api/mc/badges/age/crate/cc/1.2.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/cc/1.2.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/cc/1.2.17/1.2.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/cc/1.2.17/1.2.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | build-dependencies | patch |
| [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) | `0.1.45` -> `0.1.46` | [![age](https://developer.mend.io/api/mc/badges/age/crate/mimalloc/0.1.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/mimalloc/0.1.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/mimalloc/0.1.45/0.1.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/mimalloc/0.1.45/0.1.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [react-email](https://redirect.github.com/resend/react-email) ([source](https://redirect.github.com/resend/react-email/tree/HEAD/packages/react-email)) | [`4.0.3` -> `4.0.7`](https://renovatebot.com/diffs/npm/react-email/4.0.3/4.0.7) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-email/4.0.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-email/4.0.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-email/4.0.3/4.0.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-email/4.0.3/4.0.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [androidx.compose.material3:material3](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.3.2) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.3.1` -> `1.3.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3:material3/1.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.compose.material3:material3/1.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.compose.material3:material3/1.3.1/1.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3:material3/1.3.1/1.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://redirect.github.com/Kotlin/kotlinx.coroutines) | `1.10.1` -> `1.10.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-core/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-core/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-core/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-core/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://redirect.github.com/Kotlin/kotlinx.coroutines) | `1.10.1` -> `1.10.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [com.android.tools.build:gradle](https://developer.android.com/studio/build) ([source](https://android.googlesource.com/platform/tools/base)) | `8.7.2` -> `8.9.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.tools.build:gradle/8.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.android.tools.build:gradle/8.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.android.tools.build:gradle/8.7.2/8.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.tools.build:gradle/8.7.2/8.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [androidx.core:core-ktx](https://developer.android.com/jetpack/androidx/releases/core#1.16.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.15.0` -> `1.16.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.core:core-ktx/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.core:core-ktx/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.core:core-ktx/1.15.0/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.core:core-ktx/1.15.0/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |

---

### Release Notes

<details>
<summary>resend/react-email (@&#8203;react-email/components)</summary>

### [`v0.0.36`](https://redirect.github.com/resend/react-email/blob/HEAD/packages/components/CHANGELOG.md#0036)

[Compare Source](https://redirect.github.com/resend/react-email/compare/@react-email/components@0.0.35...@react-email/components@0.0.36)

##### Patch Changes

-   Updated dependencies \[[`f4c5456`](https://redirect.github.com/resend/react-email/commit/f4c5456)]
-   Updated dependencies \[[`f4c5456`](https://redirect.github.com/resend/react-email/commit/f4c5456)]
    -   [@&#8203;react-email/render](https://redirect.github.com/react-email/render)[@&#8203;1](https://redirect.github.com/1).0.6
    -   [@&#8203;react-email/code-block](https://redirect.github.com/react-email/code-block)[@&#8203;0](https://redirect.github.com/0).0.12
    -   [@&#8203;react-email/body](https://redirect.github.com/react-email/body)[@&#8203;0](https://redirect.github.com/0).0.11
    -   [@&#8203;react-email/button](https://redirect.github.com/react-email/button)[@&#8203;0](https://redirect.github.com/0).0.19
    -   [@&#8203;react-email/code-inline](https://redirect.github.com/react-email/code-inline)[@&#8203;0](https://redirect.github.com/0).0.5
    -   [@&#8203;react-email/column](https://redirect.github.com/react-email/column)[@&#8203;0](https://redirect.github.com/0).0.13
    -   [@&#8203;react-email/container](https://redirect.github.com/react-email/container)[@&#8203;0](https://redirect.github.com/0).0.15
    -   [@&#8203;react-email/font](https://redirect.github.com/react-email/font)[@&#8203;0](https://redirect.github.com/0).0.9
    -   [@&#8203;react-email/head](https://redirect.github.com/react-email/head)[@&#8203;0](https://redirect.github.com/0).0.12
    -   [@&#8203;react-email/heading](https://redirect.github.com/react-email/heading)[@&#8203;0](https://redirect.github.com/0).0.15
    -   [@&#8203;react-email/hr](https://redirect.github.com/react-email/hr)[@&#8203;0](https://redirect.github.com/0).0.11
    -   [@&#8203;react-email/html](https://redirect.github.com/react-email/html)[@&#8203;0](https://redirect.github.com/0).0.11
    -   [@&#8203;react-email/img](https://redirect.github.com/react-email/img)[@&#8203;0](https://redirect.github.com/0).0.11
    -   [@&#8203;react-email/link](https://redirect.github.com/react-email/link)[@&#8203;0](https://redirect.github.com/0).0.12
    -   [@&#8203;react-email/markdown](https://redirect.github.com/react-email/markdown)[@&#8203;0](https://redirect.github.com/0).0.14
    -   [@&#8203;react-email/preview](https://redirect.github.com/react-email/preview)[@&#8203;0](https://redirect.github.com/0).0.12
    -   [@&#8203;react-email/row](https://redirect.github.com/react-email/row)[@&#8203;0](https://redirect.github.com/0).0.12
    -   [@&#8203;react-email/section](https://redirect.github.com/react-email/section)[@&#8203;0](https://redirect.github.com/0).0.16
    -   [@&#8203;react-email/tailwind](https://redirect.github.com/react-email/tailwind)[@&#8203;1](https://redirect.github.com/1).0.4
    -   [@&#8203;react-email/text](https://redirect.github.com/react-email/text)[@&#8203;0](https://redirect.github.com/0).1.1

</details>

<details>
<summary>taskforcesh/bullmq (bullmq)</summary>

### [`v5.48.1`](https://redirect.github.com/taskforcesh/bullmq/compare/v5.48.0...b00731469ebbc65d35003c48da093e48c0ccc6ed)

[Compare Source](https://redirect.github.com/taskforcesh/bullmq/compare/v5.48.0...v5.48.1)

</details>

<details>
<summary>rust-lang/cc-rs (cc)</summary>

### [`v1.2.18`](https://redirect.github.com/rust-lang/cc-rs/blob/HEAD/CHANGELOG.md#1218---2025-04-04)

[Compare Source](https://redirect.github.com/rust-lang/cc-rs/compare/cc-v1.2.17...cc-v1.2.18)

##### Other

-   Regenerate target info ([#&#8203;1450](https://redirect.github.com/rust-lang/cc-rs/pull/1450))
-   Use `std::thread::available_parallelism` for determining the default number of jobs ([#&#8203;1447](https://redirect.github.com/rust-lang/cc-rs/pull/1447))
-   Fix mips64-openwrt-linux-musl parsing ([#&#8203;1449](https://redirect.github.com/rust-lang/cc-rs/pull/1449))
-   Use compiler prefix `x86_64-linux-musl` ([#&#8203;1443](https://redirect.github.com/rust-lang/cc-rs/pull/1443))

</details>

<details>
<summary>purpleprotocol/mimalloc_rust (mimalloc)</summary>

### [`v0.1.46`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.46): Version 0.1.46

[Compare Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.45...v0.1.46)

##### Changes

-   Fixed musl builds.

</details>

<details>
<summary>resend/react-email (react-email)</summary>

### [`v4.0.7`](https://redirect.github.com/resend/react-email/releases/tag/react-email%404.0.7): react-email 4.0.7

[Compare Source](https://redirect.github.com/resend/react-email/compare/react-email@4.0.6...react-email@4.0.7)

#### What's Changed

-   [`403e415`](https://redirect.github.com/resend/react-email/commit/403e415): Fix `deno` not working as an option for `email build`

**Full Changelog**: https://github.com/resend/react-email/compare/react-email@4.0.6...react-email@4.0.7

### [`v4.0.6`](https://redirect.github.com/resend/react-email/blob/HEAD/packages/react-email/CHANGELOG.md#406)

[Compare Source](https://redirect.github.com/resend/react-email/compare/react-email@4.0.5...react-email@4.0.6)

##### Patch Changes

-   [`809130e`](https://redirect.github.com/resend/react-email/commit/809130e): `next@15.2.4`

### [`v4.0.5`](https://redirect.github.com/resend/react-email/releases/tag/react-email%404.0.5): react-email 4.0.5

[Compare Source](https://redirect.github.com/resend/react-email/compare/react-email@4.0.4...react-email@4.0.5)

#### What's Changed

-   [`e1dc351`](https://redirect.github.com/resend/react-email/commit/e1dc351): Add support for path aliases when linter runs tailwind config

**Full Changelog**: https://github.com/resend/react-email/compare/react-email@4.0.4...react-email@4.0.5

### [`v4.0.4`](https://redirect.github.com/resend/react-email/releases/tag/react-email%404.0.4): react-email 4.0.4

[Compare Source](https://redirect.github.com/resend/react-email/compare/react-email@4.0.3...react-email@4.0.4)

#### What's Changed

-   [`1a7f9e6`](https://redirect.github.com/resend/react-email/commit/1a7f9e6): Update dependencies: `next@15.2.3`

**Full Changelog**: https://github.com/resend/react-email/compare/react-email@4.0.3...react-email@4.0.4

</details>

<details>
<summary>Kotlin/kotlinx.coroutines (org.jetbrains.kotlinx:kotlinx-coroutines-core)</summary>

### [`v1.10.2`](https://redirect.github.com/Kotlin/kotlinx.coroutines/blob/HEAD/CHANGES.md#Version-1102)

[Compare Source](https://redirect.github.com/Kotlin/kotlinx.coroutines/compare/1.10.1...1.10.2)

-   Fixed the `kotlinx-coroutines-debug` JAR file including the `module-info.class` file twice, resulting in failures in various tooling ([#&#8203;4314](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4314)). Thanks, [@&#8203;RyuNen344](https://redirect.github.com/RyuNen344)!
-   Fixed `Flow.stateIn` hanging when the scope is cancelled in advance or the flow is empty ([#&#8203;4322](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4322)). Thanks, [@&#8203;francescotescari](https://redirect.github.com/francescotescari)!
-   Improved handling of dispatcher failures in `.limitedParallelism` ([#&#8203;4330](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4330)) and during flow collection ([#&#8203;4272](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4272)).
-   Fixed `runBlocking` failing to run its coroutine to completion in some cases if its JVM thread got interrupted ([#&#8203;4399](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4399)).
-   Small tweaks, fixes, and documentation improvements.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-10 09:04:18 +00:00
doodlewind
f85b35227b feat(editor): replace flat layout cache with tree in turbo renderer (#11319)
### TL;DR

Refactored the BlockSuite turbo renderer to use a hierarchical tree structure for layouts instead of a flat list, improving rendering accuracy and performance.

### What changed?

- Redesigned the layout system to use a tree structure (`ViewportLayoutTree`) that better represents the document hierarchy
- Added `blockId` to all layout objects for better tracking and debugging
- Updated the layout query mechanism to work with models directly instead of components
- Enhanced error handling with more descriptive warnings and error messages
- Improved the painting process to traverse the layout tree recursively
- Fixed viewport coordinate calculations for more accurate rendering
- Updated the worker communication to support the new tree-based layout structure

### Why make this change?

The previous flat layout structure didn't properly represent the hierarchical nature of documents, leading to rendering issues with nested blocks. This tree-based approach:

1. Better represents the actual document structure
2. Improves rendering accuracy for nested elements
3. Makes debugging easier with more consistent block identification
4. Provides a more robust foundation for future rendering optimizations
5. Reduces the likelihood of rendering artifacts when scrolling or zooming
2025-04-10 08:49:23 +00:00
liuyi
b8e93ed714 chore(server): change default deployment type in dev mode (#11593) 2025-04-10 08:47:36 +00:00
Flrande
cc257f4fbe chore(editor): add track for mention (#11594) 2025-04-10 16:33:11 +08:00
63 changed files with 1204 additions and 660 deletions

View File

@@ -791,6 +791,8 @@ jobs:
with:
filters: |
changed:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- 'packages/frontend/core/src/blocksuite/ai/**'
- 'tests/affine-cloud-copilot/**'

12
Cargo.lock generated
View File

@@ -601,9 +601,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.17"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
@@ -1997,9 +1997,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libmimalloc-sys"
version = "0.1.41"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
dependencies = [
"cc",
"libc",
@@ -2153,9 +2153,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mimalloc"
version = "0.1.45"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
dependencies = [
"libmimalloc-sys",
]

View File

@@ -3769,6 +3769,48 @@ bbb
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('escapes dollar signs followed by a digit or space and digit', async () => {
const markdown =
'The price of the T-shirt is $9.15 and the price of the hat is $ 8\n';
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert:
'The price of the T-shirt is $9.15 and the price of the hat is $ 8',
},
],
},
},
children: [],
},
],
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
});
test('reference', async () => {

View File

@@ -3,21 +3,84 @@ import {
MarkdownPreprocessorExtension,
} from '@blocksuite/affine-shared/adapters';
function escapeBrackets(text: string) {
const pattern =
/(```[\S\s]*?```|`.*?`)|\\\[([\S\s]*?[^\\])\\]|\\\((.*?)\\\)/g;
return text.replaceAll(
pattern,
(match, codeBlock, squareBracket, roundBracket) => {
if (codeBlock) {
return codeBlock;
} else if (squareBracket) {
return `$$${squareBracket}$$`;
} else if (roundBracket) {
return `$${roundBracket}$`;
}
return match;
}
);
}
function escapeMhchem(text: string) {
return text.replaceAll('$\\ce{', '$\\\\ce{').replaceAll('$\\pu{', '$\\\\pu{');
}
/**
* Preprocess the content to protect code blocks and LaTeX expressions
* reference issue: https://github.com/remarkjs/react-markdown/issues/785
* reference comment: https://github.com/remarkjs/react-markdown/issues/785#issuecomment-2307567823
* @param content - The content to preprocess
* @returns The preprocessed content
*/
function preprocessLatex(content: string) {
// Protect code blocks
const codeBlocks: string[] = [];
let preprocessedContent = content;
preprocessedContent = preprocessedContent.replace(
/(```[\s\S]*?```|`[^`\n]+`)/g,
(_, code) => {
codeBlocks.push(code);
return `<<CODE_BLOCK_${codeBlocks.length - 1}>>`;
}
);
// Protect existing LaTeX expressions
const latexExpressions: string[] = [];
preprocessedContent = preprocessedContent.replace(
/(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\(.*?\\\))/g,
match => {
latexExpressions.push(match);
return `<<LATEX_${latexExpressions.length - 1}>>`;
}
);
// Escape dollar signs that are likely currency indicators
preprocessedContent = preprocessedContent.replace(/\$(?=\d)/g, '\\$');
// Restore LaTeX expressions
preprocessedContent = preprocessedContent.replace(
/<<LATEX_(\d+)>>/g,
(_, index) => latexExpressions[parseInt(index)]
);
// Restore code blocks
preprocessedContent = preprocessedContent.replace(
/<<CODE_BLOCK_(\d+)>>/g,
(_, index) => codeBlocks[parseInt(index)]
);
// Apply additional escaping functions
preprocessedContent = escapeBrackets(preprocessedContent);
preprocessedContent = escapeMhchem(preprocessedContent);
return preprocessedContent;
}
const latexPreprocessor: MarkdownAdapterPreprocessor = {
name: 'latex',
levels: ['block', 'slice', 'doc'],
preprocess: content => {
// Replace block-level LaTeX delimiters \[ \] with $$ $$
const blockProcessedContent = content.replace(
/\\\[(.*?)\\\]/gs,
(_, equation) => `$$${equation}$$`
);
// Replace inline LaTeX delimiters \( \) with $ $
const inlineProcessedContent = blockProcessedContent.replace(
/\\\((.*?)\\\)/gs,
(_, equation) => `$${equation}$`
);
return inlineProcessedContent;
return preprocessLatex(content);
},
};

View File

@@ -6,8 +6,9 @@ import {
segmentSentences,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord } from '@blocksuite/std/gfx';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ListLayout } from './list-painter.worker';
@@ -21,24 +22,27 @@ export class ListLayoutHandlerExtension extends BlockLayoutHandlerExtension<List
);
}
queryLayout(component: GfxBlockComponent): ListLayout | null {
// Select all list items within this list block
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): ListLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
if (!component) return null;
// Find the list items within this specific list component
const listItemSelector =
'.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]';
const listItemNodes = component.querySelectorAll(listItemSelector);
if (listItemNodes.length === 0) return null;
const viewportRecord = component.gfx.viewport.deserializeRecord(
component.dataset.viewportState
);
if (!viewportRecord) return null;
const { zoom, viewScale } = viewportRecord;
const list: ListLayout = {
type: 'affine:list',
items: [],
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
listItemNodes.forEach(listItemNode => {

View File

@@ -77,10 +77,15 @@ class ListLayoutPainter implements BlockLayoutPainter {
return;
}
if (!isListLayout(layout)) return;
if (!isListLayout(layout)) {
console.warn(
'Expected list layout but received different format:',
layout
);
return;
}
const renderedPositions = new Set<string>();
layout.items.forEach(item => {
const fontSize = item.fontSize;
const baselineY = getBaseline(fontSize);

View File

@@ -14,6 +14,7 @@
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-fragment-doc-title": "workspace:*",
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
"@blocksuite/affine-inline-preset": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
@@ -37,7 +38,8 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/note-painter.worker.ts"
},
"files": [
"src",

View File

@@ -6,3 +6,5 @@ export * from './edgeless-clipboard-config';
export * from './note-block';
export * from './note-edgeless-block';
export * from './note-spec';
export * from './turbo/note-layout-handler';
export * from './turbo/note-painter.worker';

View File

@@ -0,0 +1,81 @@
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
import {
BlockLayoutHandlerExtension,
BlockLayoutHandlersIdentifier,
} from '@blocksuite/affine-gfx-turbo-renderer';
import {
ColorScheme,
type NoteBlockModel,
resolveColor,
} from '@blocksuite/affine-model';
import type { Container } from '@blocksuite/global/di';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { NoteLayout } from './note-painter.worker';
export class NoteLayoutHandlerExtension extends BlockLayoutHandlerExtension<NoteLayout> {
readonly blockType = 'affine:note';
static override setup(di: Container) {
di.addImpl(
BlockLayoutHandlersIdentifier('note'),
NoteLayoutHandlerExtension
);
}
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): NoteLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
if (!component) return null;
// Get the note container element
const noteContainer = component.querySelector('.affine-note-mask');
if (!noteContainer) return null;
// Get the bounding client rect of the note container
const clientRect = noteContainer.getBoundingClientRect();
// Convert client coordinates to model coordinates
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
clientRect.x,
clientRect.y,
]);
const { zoom, viewScale } = viewportRecord;
// Cast model to NoteBlockModel to access background property from props
const noteModel = model as NoteBlockModel;
const background = noteModel.props.background;
// Resolve the color to a string
const backgroundString = resolveColor(background, ColorScheme.Light);
// Create the note layout object
const noteLayout: NoteLayout = {
type: 'affine:note',
blockId: model.id,
rect: {
x: modelX,
y: modelY,
w: clientRect.width / zoom / viewScale,
h: clientRect.height / zoom / viewScale,
},
background: backgroundString,
};
return noteLayout;
}
calculateBound(layout: NoteLayout) {
const rect: Rect = layout.rect;
return {
rect,
subRects: [rect], // The note is represented by a single rectangle
};
}
}

View File

@@ -0,0 +1,49 @@
import type {
BlockLayout,
BlockLayoutPainter,
WorkerToHostMessage,
} from '@blocksuite/affine-gfx-turbo-renderer';
import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter';
export interface NoteLayout extends BlockLayout {
type: 'affine:note';
background?: string;
}
function isNoteLayout(layout: BlockLayout): layout is NoteLayout {
return layout.type === 'affine:note';
}
class NoteLayoutPainter implements BlockLayoutPainter {
paint(
ctx: OffscreenCanvasRenderingContext2D,
layout: BlockLayout,
layoutBaseX: number,
layoutBaseY: number
): void {
if (!isNoteLayout(layout)) {
const message: WorkerToHostMessage = {
type: 'paintError',
error: 'Invalid layout format',
blockType: 'affine:note',
};
self.postMessage(message);
return;
}
// Get the layout rectangle
const x = layout.rect.x - layoutBaseX;
const y = layout.rect.y - layoutBaseY;
const width = layout.rect.w;
const height = layout.rect.h;
ctx.fillStyle = layout.background || 'rgb(255, 255, 255)';
ctx.fillRect(x, y, width, height);
ctx.strokeRect(x, y, width, height);
}
}
export const NoteLayoutPainterExtension = BlockLayoutPainterExtension(
'affine:note',
NoteLayoutPainter
);

View File

@@ -11,6 +11,7 @@
{ "path": "../surface" },
{ "path": "../../components" },
{ "path": "../../fragments/doc-title" },
{ "path": "../../gfx/turbo-renderer" },
{ "path": "../../inlines/preset" },
{ "path": "../../model" },
{ "path": "../../rich-text" },

View File

@@ -6,8 +6,9 @@ import {
segmentSentences,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord } from '@blocksuite/std/gfx';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ParagraphLayout } from './paragraph-painter.worker';
@@ -21,58 +22,54 @@ export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension
);
}
queryLayout(component: GfxBlockComponent): ParagraphLayout | null {
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): ParagraphLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
const paragraphSelector =
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]';
const paragraphNodes = component.querySelectorAll(paragraphSelector);
if (paragraphNodes.length === 0) return null;
const viewportRecord = component.gfx.viewport.deserializeRecord(
component.dataset.viewportState
);
if (!viewportRecord) return null;
const paragraphNode = component.querySelector(paragraphSelector);
if (!paragraphNode) return null;
const { zoom, viewScale } = viewportRecord;
const paragraph: ParagraphLayout = {
type: 'affine:paragraph',
sentences: [],
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
paragraphNodes.forEach(paragraphNode => {
const computedStyle = window.getComputedStyle(paragraphNode);
const fontSizeStr = computedStyle.fontSize;
const fontSize = parseInt(fontSizeStr);
const computedStyle = window.getComputedStyle(paragraphNode);
const fontSizeStr = computedStyle.fontSize;
const fontSize = parseInt(fontSizeStr);
const sentences = segmentSentences(paragraphNode.textContent || '');
const sentenceLayouts = sentences.map(sentence => {
const sentenceRects = getSentenceRects(paragraphNode, sentence);
const rects = sentenceRects.map(({ text, rect }) => {
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x,
rect.y,
]);
return {
text,
rect: {
x: modelX,
y: modelY,
w: rect.w / zoom / viewScale,
h: rect.h / zoom / viewScale,
},
};
});
const sentences = segmentSentences(paragraphNode.textContent || '');
const sentenceLayouts = sentences.map(sentence => {
const sentenceRects = getSentenceRects(paragraphNode, sentence);
const rects = sentenceRects.map(({ text, rect }) => {
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x,
rect.y,
]);
return {
text: sentence,
rects,
fontSize,
text,
rect: {
x: modelX,
y: modelY,
w: rect.w / zoom / viewScale,
h: rect.h / zoom / viewScale,
},
};
});
paragraph.sentences.push(...sentenceLayouts);
return {
text: sentence,
rects,
fontSize,
};
});
paragraph.sentences.push(...sentenceLayouts);
return paragraph;
}

View File

@@ -73,10 +73,15 @@ class ParagraphLayoutPainter implements BlockLayoutPainter {
return;
}
if (!isParagraphLayout(layout)) return; // cast to ParagraphLayout
if (!isParagraphLayout(layout)) {
console.warn(
'Expected paragraph layout but received different format:',
layout
);
return;
}
const renderedPositions = new Set<string>();
layout.sentences.forEach(sentence => {
const fontSize = sentence.fontSize;
const baselineY = getBaseline(fontSize);

View File

@@ -49,7 +49,7 @@ import {
import type { BaseSelection, Store } from '@blocksuite/store';
import { effect, signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { query, state } from 'lit/decorators.js';
import { query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -103,17 +103,12 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
margin: 0 auto;
position: relative;
overflow: hidden;
pointer-events: none;
user-select: none;
}
.ref-viewport-event-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
inset: 0;
}
`;
@@ -139,11 +134,11 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
return this._referencedModel;
}
private _focusBlock() {
private readonly _handleClick = () => {
this.selection.update(() => {
return [this.selection.create(BlockSelection, { blockId: this.blockId })];
});
}
};
private _initHotkey() {
const selection = this.host.selection;
@@ -178,7 +173,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
this.bindHotKey({
Enter: () => {
if (!this._focused) return;
if (!this.selected$.value) return;
addParagraph();
return true;
},
@@ -260,17 +255,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
}
}
private _initSelection() {
const selection = this.host.selection;
this._disposables.add(
selection.slots.changed.subscribe(selList => {
this._focused = selList.some(
sel => sel.blockId === this.blockId && sel.is(BlockSelection)
);
})
);
}
private _initViewport() {
const refreshViewport = () => {
if (!this._referenceXYWH$.value) return;
@@ -436,7 +420,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
this._initHotkey();
this._initViewport();
this._initReferencedModel();
this._initSelection();
}
override firstUpdated() {
@@ -462,10 +445,10 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
<div
class=${classMap({
'affine-surface-ref': true,
focused: this._focused,
focused: this.selected$.value,
})}
data-theme=${edgelessTheme}
@click=${this._focusBlock}
@click=${this._handleClick}
>
${content}
</div>
@@ -488,9 +471,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
this.std.get(DocModeProvider).setEditorMode('edgeless');
}
@state()
private accessor _focused: boolean = false;
@query('.affine-surface-ref')
accessor hoverableContainer!: HTMLDivElement;

View File

@@ -17,8 +17,12 @@ export interface IModelCoord {
y: number;
}
// TODO(@L-Sun): we should remove this list when refactor the pointerOut event to pointerLeave,
// since the previous will be triggered when the pointer move to the area of the its children elements
// see: https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerout_event
export const EXCLUDING_MOUSE_OUT_CLASS_LIST = [
'affine-note-mask',
'edgeless-block-portal-note',
'affine-block-children-container',
'edgeless-container',
];

View File

@@ -12,7 +12,6 @@ import type { IBound, IVec, IVec3 } from '@blocksuite/global/gfx';
import {
almostEqual,
Bound,
clamp,
getBezierCurveBoundingBox,
getBezierParameters,
getBoundFromPoints,
@@ -85,7 +84,7 @@ export function calculateNearestLocation(
) {
const { x, y, w, h } = bounds;
return locations
.map(offset => [x + offset[0] * w, y + offset[1] * h] as IVec)
.map<IVec>(offset => [x + offset[0] * w, y + offset[1] * h])
.map(point => getPointFromBoundsWithRotation(bounds, point))
.reduce(
(prev, curr, index) => {
@@ -99,7 +98,7 @@ export function calculateNearestLocation(
return prev;
},
[...locations[0]]
) as IVec;
);
}
function rBound(ele: GfxModel, anti = false): IBound {
@@ -139,21 +138,19 @@ export function getAnchors(ele: GfxModel) {
const anchors: { point: PointLocation; coord: IVec }[] = [];
const rotate = ele.rotate;
[
[bound.center[0], bound.y - offset],
[bound.center[0], bound.maxY + offset],
[bound.x - offset, bound.center[1]],
[bound.maxX + offset, bound.center[1]],
]
.map(vec =>
getPointFromBoundsWithRotation({ ...bound, rotate }, vec as IVec)
)
(
[
[bound.center[0], bound.y - offset],
[bound.center[0], bound.maxY + offset],
[bound.x - offset, bound.center[1]],
[bound.maxX + offset, bound.center[1]],
] satisfies IVec[]
)
.map(vec => getPointFromBoundsWithRotation({ ...bound, rotate }, vec))
.forEach(vec => {
const rst = ele.getLineIntersections(bound.center as IVec, vec as IVec);
if (!rst) {
console.error(`Failed to get line intersections for ${ele.id}`);
return;
}
const rst = ele.getLineIntersections(bound.center, vec);
if (!rst) return;
const originPoint = getPointFromBoundsWithRotation(
{ ...bound, rotate: -rotate },
rst[0]
@@ -164,7 +161,7 @@ export function getAnchors(ele: GfxModel) {
}
function getConnectableRelativePosition(connectable: GfxModel, position: IVec) {
const location = connectable.getRelativePointLocation(position as IVec);
const location = connectable.getRelativePointLocation(position);
if (isVecZero(Vec.sub(position, [0, 0.5])))
location.tangent = Vec.rot([0, -1], toRadian(connectable.rotate));
else if (isVecZero(Vec.sub(position, [1, 0.5])))
@@ -184,7 +181,11 @@ export function getNearestConnectableAnchor(ele: Connectable, point: IVec) {
);
}
function closestPoint(points: PointLocation[], point: IVec) {
function closestPoint(
points: PointLocation[],
point: IVec
): PointLocation | null {
if (points.length === 0) return null;
const rst = points.map(p => ({ p, d: Vec.dist(p, point) }));
rst.sort((a, b) => a.d - b.d);
return rst[0].p;
@@ -245,7 +246,7 @@ function filterConnectablePoints<T extends IVec3 | IVec>(
): T[] {
return points.filter(point => {
if (!bound) return true;
return !bound.isPointInBound(point as IVec);
return !bound.isPointInBound([point[0], point[1]]);
});
}
@@ -368,15 +369,17 @@ function pushGapMidPoint(
bound.lowerLine,
bound2.upperLine,
bound2.lowerLine,
].map(line => {
return lineIntersects(
point as unknown as IVec,
[point[0], point[1] + 1],
line[0],
line[1],
true
) as IVec;
});
]
.map(line => {
return lineIntersects(
[point[0], point[1]],
[point[0], point[1] + 1],
line[0],
line[1],
true
);
})
.filter(p => p !== null);
rst.sort((a, b) => a[1] - b[1]);
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
pushWithPriority(points, [midPoint], 6);
@@ -399,15 +402,17 @@ function pushGapMidPoint(
bound.rightLine,
bound2.leftLine,
bound2.rightLine,
].map(line => {
return lineIntersects(
point as unknown as IVec,
[point[0] + 1, point[1]],
line[0],
line[1],
true
) as IVec;
});
]
.map(line => {
return lineIntersects(
[point[0], point[1]],
[point[0] + 1, point[1]],
line[0],
line[1],
true
);
})
.filter(p => p !== null);
rst.sort((a, b) => a[0] - b[0]);
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
pushWithPriority(points, [midPoint], 6);
@@ -480,14 +485,14 @@ function getConnectablePoints(
expandEndBound: Bound | null
) {
const lineBound = Bound.fromPoints([
startPoint,
endPoint,
] as unknown[] as IVec[]);
[startPoint[0], startPoint[1]],
[endPoint[0], endPoint[1]],
]);
const outerBound =
expandStartBound &&
expandEndBound &&
expandStartBound.unite(expandEndBound);
let points = [nextStartPoint, lastEndPoint] as IVec3[];
let points = [nextStartPoint, lastEndPoint];
pushWithPriority(points, lineBound.getVerticesAndMidpoints());
if (!startBound || !endBound) {
@@ -534,7 +539,7 @@ function getConnectablePoints(
pushWithPriority(points, expandStartBound.getVerticesAndMidpoints());
pushWithPriority(
points,
expandStartBound.include(lastEndPoint as unknown as IVec).points
expandStartBound.include([lastEndPoint[0], lastEndPoint[1]]).points
);
}
@@ -542,7 +547,7 @@ function getConnectablePoints(
pushWithPriority(points, expandEndBound.getVerticesAndMidpoints());
pushWithPriority(
points,
expandEndBound.include(nextStartPoint as unknown as IVec).points
expandEndBound.include([nextStartPoint[0], nextStartPoint[1]]).points
);
}
@@ -561,7 +566,7 @@ function getConnectablePoints(
almostEqual(item[0], point[0], 0.02) &&
almostEqual(item[1], point[1], 0.02)
);
}) as IVec3[];
});
if (!startEnds[0] || !startEnds[1]) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
@@ -603,7 +608,9 @@ function mergePath(points: IVec[] | IVec3[]) {
continue;
result.push([cur[0], cur[1]]);
}
result.push(last(points as IVec[]) as IVec);
if (points.length !== 0) {
result.push([points[points.length - 1][0], points[points.length - 1][1]]);
}
for (let i = 0; i < result.length - 1; i++) {
const cur = result[i];
const next = result[i + 1];
@@ -687,7 +694,7 @@ function getNextPoint(
offsetW = 10,
offsetH = 10
) {
const result: IVec = Array.from(point) as IVec;
const result: IVec = [point[0], point[1]];
if (almostEqual(bound.x, result[0])) result[0] -= offsetX;
else if (almostEqual(bound.y, result[1])) result[1] -= offsetY;
else if (almostEqual(bound.maxX, result[0])) result[0] += offsetW;
@@ -993,7 +1000,7 @@ export class ConnectionOverlay extends Overlay {
this.highlightPoint = anchor.point;
result = {
id: connectable.id,
position: anchor.coord as IVec,
position: anchor.coord,
};
}
}
@@ -1001,7 +1008,7 @@ export class ConnectionOverlay extends Overlay {
if (shortestDistance < 8 && result) break;
// if not, check if closes to bound
const nearestPoint = connectable.getNearestPoint(point as IVec) as IVec;
const nearestPoint = connectable.getNearestPoint(point);
if (Vec.dist(nearestPoint, point) < 8) {
this.highlightPoint = nearestPoint;
@@ -1013,9 +1020,7 @@ export class ConnectionOverlay extends Overlay {
target.push(connectable);
result = {
id: connectable.id,
position: bound
.toRelative(originPoint)
.map(n => clamp(n, 0, 1)) as IVec,
position: Vec.clampV(bound.toRelative(originPoint), 0, 1),
};
}
@@ -1048,7 +1053,7 @@ export class ConnectionOverlay extends Overlay {
// at last, if not, just return the point
if (!result) {
result = {
position: point as IVec,
position: point,
};
}
@@ -1383,7 +1388,7 @@ export class ConnectorPathGenerator extends PathGenerator {
const eb = Bound.deserialize(end.xywh);
const startPoint = getNearestConnectableAnchor(start, eb.center);
const endPoint = getNearestConnectableAnchor(end, sb.center);
return [startPoint, endPoint];
return (startPoint && endPoint && [startPoint, endPoint]) ?? [];
} else {
const endPoint = this._getConnectionPoint(connector, 'target');
const startPoint = this._getConnectionPoint(connector, 'source');

View File

@@ -1,6 +1,7 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import { noop } from '@blocksuite/global/utils';
import type { GfxController } from '@blocksuite/std/gfx';
import { startWith } from 'rxjs';
import type { RoughCanvas } from '../utils/rough/canvas';
import { Overlay } from './overlay';
@@ -18,10 +19,11 @@ export class ToolOverlay extends Overlay {
super(gfx);
this.x = 0;
this.y = 0;
this.globalAlpha = 0;
this.globalAlpha = 1;
this.gfx = gfx;
this.disposables.add(
this.gfx.viewport.viewportUpdated.subscribe(() => {
this.gfx.viewport.viewportUpdated.pipe(startWith(null)).subscribe(() => {
// when viewport is updated, we should keep the overlay in the same position
// to get last mouse position and convert it to model coordinates
const pos = this.gfx.tool.lastMousePos$.value;

View File

@@ -38,11 +38,11 @@ const styles = css`
transition: 0.1s;
}
label.subscribe {
label.on {
background: var(--affine-primary-color);
}
label.subscribe:after {
label.on:after {
left: calc(100% - 1px);
transform: translateX(-100%);
}

View File

@@ -326,6 +326,16 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
},
{ global: true }
);
// since there is not a tool called mindmap, we need to cancel the drag when the tool is changed
this.disposables.add(
this.gfx.tool.currentToolName$.subscribe(toolName => {
// FIXME: remove the assertion after gfx tool refactor
if ((toolName as string) !== 'empty' && this.readyToDrop) {
this.draggableController.cancel();
}
})
);
}
override render() {

View File

@@ -26,7 +26,6 @@ export class NoteOverlay extends ToolOverlay {
constructor(gfx: GfxController, background: Color) {
super(gfx);
this.globalAlpha = 0;
this.backgroundColor = gfx.std
.get(ThemeProvider)
.getColorValue(background, DefaultTheme.noteBackgrounColor, true);

View File

@@ -236,30 +236,39 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
const locked = this.gfx.viewport.locked;
const selection = this.gfx.selection;
if (locked || selection.editing) return;
if (this.readyToDrop) {
const activeIndex = shapes.findIndex(
s => s.name === this.draggingShape
);
const nextIndex = (activeIndex + 1) % shapes.length;
const next = shapes[nextIndex];
this.draggingShape = next.name;
this.draggableController.cancelWithoutAnimation();
}
const el = this.shapeContainer.querySelector(
`.shape.${this.draggingShape}`
) as HTMLElement;
if (!el) {
console.error('Edgeless toolbar Shape element not found');
if (
this.gfx.tool.dragging$.peek() &&
this.gfx.tool.currentToolName$.peek() === 'shape'
) {
return;
}
const { x, y } = this.gfx.tool.lastMousePos$.peek();
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
const { left, top } = viewport;
const clientPos = { x: x + left, y: y + top };
this.draggableController.dragAndMoveTo(el, clientPos);
const activeIndex = shapes.findIndex(
s => s.name === this.draggingShape
);
const nextIndex = (activeIndex + 1) % shapes.length;
const next = shapes[nextIndex];
this.draggingShape = next.name;
if (this.readyToDrop) {
this.draggableController.cancelWithoutAnimation();
const el = this.shapeContainer.querySelector(
`.shape.${this.draggingShape}`
) as HTMLElement;
if (!el) {
console.error('Edgeless toolbar Shape element not found');
return;
}
const { x, y } = this.gfx.tool.lastMousePos$.peek();
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
const { left, top } = viewport;
const clientPos = { x: x + left, y: y + top };
this.draggableController.dragAndMoveTo(el, clientPos);
} else {
this.setEdgelessTool('shape', {
shapeName: this.draggingShape,
});
}
},
},
{ global: true }

View File

@@ -89,7 +89,6 @@ export class ShapeTool extends BaseTool<ShapeToolOption> {
private _hideOverlay() {
if (!this._shapeOverlay) return;
this._shapeOverlay.globalAlpha = 0;
(this.gfx.surfaceComponent as SurfaceBlockComponent)?.refresh();
}

View File

@@ -13,9 +13,9 @@ export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
}
private _initDblClickToEdit(): void {
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
this.on('dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
if (edgeless && !this.model.isLocked()) {
mountShapeTextEditor(this.model, edgeless);
}

View File

@@ -1,5 +1,7 @@
import { createIdentifier } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std';
import type { EditorHost } from '@blocksuite/std';
import type { ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { Extension } from '@blocksuite/store';
import type { BlockLayout, Rect } from '../types';
@@ -8,7 +10,13 @@ export abstract class BlockLayoutHandlerExtension<
T extends BlockLayout = BlockLayout,
> extends Extension {
abstract readonly blockType: string;
abstract queryLayout(component: GfxBlockComponent): T | null;
abstract queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): T | null;
abstract calculateBound(layout: T): {
rect: Rect;
subRects: Rect[];

View File

@@ -7,8 +7,9 @@ import type { ExtensionType } from '@blocksuite/store';
import type {
BlockLayoutPainter,
BlockLayoutTreeNode,
HostToWorkerMessage,
ViewportLayout,
ViewportLayoutTree,
WorkerToHostMessage,
} from '../types';
@@ -33,8 +34,8 @@ export class ViewportLayoutPainter {
private zoom = 1;
public provider: ServiceProvider;
getPainter(type: string): BlockLayoutPainter | undefined {
return this.provider.get(BlockPainterProvider(type));
getPainter(type: string): BlockLayoutPainter | null {
return this.provider.getOptional(BlockPainterProvider(type));
}
constructor(extensions: ExtensionType[]) {
@@ -66,24 +67,28 @@ export class ViewportLayoutPainter {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
paint(layout: ViewportLayout, version: number) {
paint(layout: ViewportLayoutTree, version: number) {
const { canvas, ctx } = this;
if (!canvas || !ctx) return;
if (layout.rect.w === 0 || layout.rect.h === 0) {
console.warn('empty layout rect');
return;
}
this.paintTree(layout, version);
}
paintTree(layout: ViewportLayoutTree, version: number) {
const { canvas, ctx } = this;
const { overallRect } = layout;
if (!canvas || !ctx) return;
this.clearBackground();
ctx.scale(this.zoom, this.zoom);
layout.blocks.forEach(blockLayout => {
const painter = this.getPainter(blockLayout.type);
if (!painter) return;
painter.paint(ctx, blockLayout, layout.rect.x, layout.rect.y);
});
const paintNode = (node: BlockLayoutTreeNode) => {
const painter = this.getPainter(node.type);
painter?.paint(ctx, node.layout, overallRect.x, overallRect.y);
node.children.forEach(paintNode);
};
layout.roots.forEach(root => paintNode(root));
const bitmap = canvas.transferToImageBitmap();
const message: WorkerToHostMessage = {
type: 'bitmapPainted',

View File

@@ -1,12 +1,14 @@
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import {
GfxBlockElementModel,
GfxControllerIdentifier,
type Viewport,
} from '@blocksuite/std/gfx';
import { type Viewport } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { BlockLayoutHandlersIdentifier } from './layout/block-layout-provider';
import type { BlockLayout, RenderingState, ViewportLayout } from './types';
import type {
BlockLayout,
BlockLayoutTreeNode,
RenderingState,
ViewportLayoutTree,
} from './types';
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
const hostRect = host.getBoundingClientRect();
@@ -21,33 +23,10 @@ export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
canvas.style.pointerEvents = 'none';
}
function getBlockLayouts(host: EditorHost): BlockLayout[] {
const gfx = host.std.get(GfxControllerIdentifier);
const models = gfx.gfxElements.filter(e => e instanceof GfxBlockElementModel);
const components = models
.map(model => gfx.view.get(model.id))
.filter(Boolean) as GfxBlockComponent[];
const layouts: BlockLayout[] = [];
components.forEach(component => {
const layoutHandlers = host.std.provider.getAll(
BlockLayoutHandlersIdentifier
);
const handlersArray = Array.from(layoutHandlers.values());
for (const handler of handlersArray) {
const layout = handler.queryLayout(component);
if (layout) {
layouts.push(layout);
}
}
});
return layouts;
}
export function getViewportLayout(
export function getViewportLayoutTree(
host: EditorHost,
viewport: Viewport
): ViewportLayout {
): ViewportLayoutTree {
const zoom = viewport.zoom;
let layoutMinX = Infinity;
@@ -55,36 +34,106 @@ export function getViewportLayout(
let layoutMaxX = -Infinity;
let layoutMaxY = -Infinity;
const blockLayouts = getBlockLayouts(host);
const store = host.std.store;
const rootModel = store.root;
if (!rootModel) {
return { roots: [], overallRect: { x: 0, y: 0, w: 0, h: 0 } };
}
const providers = host.std.provider.getAll(BlockLayoutHandlersIdentifier);
const providersArray = Array.from(providers.values());
blockLayouts.forEach(blockLayout => {
const provider = providersArray.find(p => p.blockType === blockLayout.type);
if (!provider) return;
// Recursive function to build the tree structure
const buildLayoutTreeNode = (
model: BlockModel,
ancestorViewportState?: string | null
): BlockLayoutTreeNode | null => {
const baseLayout: BlockLayout = {
blockId: model.id,
type: model.flavour,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
const { rect } = provider.calculateBound(blockLayout);
const handler = providersArray.find(p => p.blockType === model.flavour);
layoutMinX = Math.min(layoutMinX, rect.x);
layoutMinY = Math.min(layoutMinY, rect.y);
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
});
// Determine the correct viewport state to use
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
const currentViewportState = component?.dataset.viewportState;
const effectiveViewportState =
currentViewportState ?? ancestorViewportState;
const defaultViewportState = {
left: 0,
top: 0,
viewportX: 0,
viewportY: 0,
zoom: 1,
viewScale: 1,
};
const layoutModelCoord = [layoutMinX, layoutMinY];
const viewportRecord = effectiveViewportState
? viewport.deserializeRecord(effectiveViewportState) ||
defaultViewportState
: defaultViewportState;
const layoutData = handler?.queryLayout(model, host, viewportRecord);
if (handler && layoutData) {
const { rect } = handler.calculateBound(layoutData);
baseLayout.rect = rect;
layoutMinX = Math.min(layoutMinX, rect.x);
layoutMinY = Math.min(layoutMinY, rect.y);
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
}
const children: BlockLayoutTreeNode[] = [];
for (const childModel of model.children) {
const childNode = buildLayoutTreeNode(childModel, effectiveViewportState);
if (childNode) {
children.push(childNode);
}
}
// Create node for this block - ALWAYS return a node
// Return the node structure including the layout (either real or fallback)
return {
blockId: model.id,
type: model.flavour,
layout: layoutData ? { ...baseLayout, ...layoutData } : baseLayout,
children,
};
};
const roots: BlockLayoutTreeNode[] = [];
const rootNode = buildLayoutTreeNode(rootModel);
if (rootNode) {
roots.push(rootNode);
}
// If no valid layouts were found, use default values
if (layoutMinX === Infinity) {
layoutMinX = 0;
layoutMinY = 0;
layoutMaxX = 0;
layoutMaxY = 0;
}
// Calculate overall rectangle
const w = (layoutMaxX - layoutMinX) / zoom / viewport.viewScale;
const h = (layoutMaxY - layoutMinY) / zoom / viewport.viewScale;
const layout: ViewportLayout = {
blocks: blockLayouts,
rect: {
x: layoutModelCoord[0],
y: layoutModelCoord[1],
const result = {
roots,
overallRect: {
x: layoutMinX,
y: layoutMinY,
w: Math.max(w, 0),
h: Math.max(h, 0),
},
};
return layout;
return result;
}
export function debugLog(message: string, state: RenderingState) {
@@ -98,14 +147,15 @@ export function debugLog(message: string, state: RenderingState) {
export function paintPlaceholder(
host: EditorHost,
canvas: HTMLCanvasElement,
layout: ViewportLayout | null,
layout: ViewportLayoutTree | null,
viewport: Viewport
) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
if (!layout) return;
if (!ctx || !layout) return;
const dpr = window.devicePixelRatio;
const layoutViewCoord = viewport.toViewCoord(layout.rect.x, layout.rect.y);
const { overallRect } = layout;
const layoutViewCoord = viewport.toViewCoord(overallRect.x, overallRect.y);
const offsetX = layoutViewCoord[0];
const offsetY = layoutViewCoord[1];
@@ -120,30 +170,28 @@ export function paintPlaceholder(
);
const handlersArray = Array.from(layoutHandlers.values());
layout.blocks.forEach((blockLayout, blockIndex) => {
ctx.fillStyle = colors[blockIndex % colors.length];
const renderedPositions = new Set<string>();
const handler = handlersArray.find(h => h.blockType === blockLayout.type);
if (!handler) return;
const { subRects } = handler.calculateBound(blockLayout);
subRects.forEach(rect => {
const x = ((rect.x - layout.rect.x) * viewport.zoom + offsetX) * dpr;
const y = ((rect.y - layout.rect.y) * viewport.zoom + offsetY) * dpr;
const paintNode = (node: BlockLayoutTreeNode, depth: number = 0) => {
const { layout: nodeLayout, type } = node;
const handler = handlersArray.find(h => h.blockType === type);
if (handler) {
ctx.fillStyle = colors[depth % colors.length];
const rect = nodeLayout.rect;
const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
const width = rect.w * viewport.zoom * dpr;
const height = rect.h * viewport.zoom * dpr;
const posKey = `${x},${y}`;
if (renderedPositions.has(posKey)) return;
ctx.fillRect(x, y, width, height);
if (width > 10 && height > 5) {
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
ctx.strokeRect(x, y, width, height);
}
}
renderedPositions.add(posKey);
});
});
if (node.children.length > 0) {
node.children.forEach(childNode => paintNode(childNode, depth + 1));
}
};
layout.roots.forEach(rootNode => paintNode(rootNode));
}

View File

@@ -19,7 +19,7 @@ import { debounceTime } from 'rxjs/operators';
import {
debugLog,
getViewportLayout,
getViewportLayoutTree,
paintPlaceholder,
syncCanvasSize,
} from './renderer-utils';
@@ -28,7 +28,7 @@ import type {
RendererOptions,
RenderingState,
TurboRendererConfig,
ViewportLayout,
ViewportLayoutTree,
WorkerToHostMessage,
} from './types';
@@ -42,6 +42,34 @@ const defaultOptions: RendererOptions = {
export const TurboRendererConfigFactory =
ConfigExtensionFactory<TurboRendererConfig>('viewport-turbo-renderer');
/**
* Manages the Turbo Rendering process for the viewport, coordinating between the main thread and a painter worker.
* Turbo Rendering optimizes performance by rendering block content onto a canvas bitmap,
* falling back to standard DOM rendering during interactions.
*
* To add Turbo Rendering support for a new block type (e.g., 'affine:my-block'):
*
* 1. **In the block's package (e.g., `blocksuite/affine/blocks/my-block`):**
* a. Add `@blocksuite/affine/gfx/turbo-renderer` as a dependency in `package.json` and create a `src/turbo` directory.
* b. Implement the Layout Handler (e.g., `MyBlockLayoutHandlerExtension`) and Painter Worker (e.g., `MyBlockLayoutPainterExtension`). Refer to `ParagraphLayoutHandlerExtension` and `ParagraphLayoutPainterExtension` in `blocksuite/affine/blocks/block-paragraph` for implementation examples.
* c. Export the Layout Handler and Painter Worker extensions from the block package's main `src/index.ts` by adding these two explicit export statements:
* ```typescript
* export * from './turbo/my-block-layout-handler';
* export * from './turbo/my-block-painter.worker';
* ```
* d. Add an export mapping for the painter worker in `package.json` under the `exports` field (e.g., `"./turbo-painter": "./src/turbo/my-block-painter.worker.ts"`).
* e. Add a TypeScript project reference to `blocksuite/affine/gfx/turbo-renderer` in `tsconfig.json`.
*
* 2. **In the application integration point (e.g., `packages/frontend/core/src/blocksuite/extensions` and `blocksuite/integration-test/src/__tests__/utils/renderer-entry.ts`):**
* a. In `turbo-renderer.ts` (or the file setting up `TurboRendererConfigFactory`):
* - Import and add the new Layout Handler extension to the `patchTurboRendererExtension` array (or equivalent DI setup). See how `ParagraphLayoutHandlerExtension` is added as a reference.
* b. In `turbo-painter.worker.ts` (the painter worker entry point):
* - Import and add the new Painter Worker extension to the `ViewportLayoutPainter` constructor's extension array. See how `ParagraphLayoutPainterExtension` is added as a reference.
*
* 3. **Run `yarn affine init`** from the workspace root to update generated configuration files (`workspace.gen.ts`) and the lockfile (`yarn.lock`).
*
* **Note:** Always ensure the directory structure and export patterns match the `paragraph` block (`blocksuite/affine/blocks/block-paragraph`) for consistency.
*/
export class ViewportTurboRendererExtension extends GfxExtension {
static override key = 'viewportTurboRenderer';
@@ -49,7 +77,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
private readonly worker: Worker;
private readonly disposables = new DisposableGroup();
private layoutCacheData: ViewportLayout | null = null;
private layoutCacheData: ViewportLayoutTree | null = null;
private layoutVersion = 0;
private bitmap: ImageBitmap | null = null;
private viewportElement: GfxViewportElement | null = null;
@@ -172,9 +200,9 @@ export class ViewportTurboRendererExtension extends GfxExtension {
get layoutCache() {
if (this.layoutCacheData) return this.layoutCacheData;
const layout = getViewportLayout(this.std.host, this.viewport);
const layoutTree = getViewportLayoutTree(this.std.host, this.viewport);
this.debugLog('Layout cache updated');
return (this.layoutCacheData = layout);
return (this.layoutCacheData = layoutTree);
}
async refresh() {
@@ -248,8 +276,8 @@ export class ViewportTurboRendererExtension extends GfxExtension {
type: 'paintLayout',
data: {
layout,
width: layout.rect.w,
height: layout.rect.h,
width: layout.overallRect.w,
height: layout.overallRect.h,
dpr,
zoom: this.viewport.zoom,
version: currentVersion,
@@ -316,17 +344,18 @@ export class ViewportTurboRendererExtension extends GfxExtension {
if (!ctx) return;
this.clearCanvas();
const layoutViewCoord = this.viewport.toViewCoord(
layout.rect.x,
layout.rect.y
layout.overallRect.x,
layout.overallRect.y
);
ctx.drawImage(
bitmap,
layoutViewCoord[0] * window.devicePixelRatio,
layoutViewCoord[1] * window.devicePixelRatio,
layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
layout.rect.h * window.devicePixelRatio * this.viewport.zoom
layout.overallRect.w * window.devicePixelRatio * this.viewport.zoom,
layout.overallRect.h * window.devicePixelRatio * this.viewport.zoom
);
this.debugLog('Bitmap drawn to canvas');

View File

@@ -14,13 +14,14 @@ export interface ViewportState {
}
export interface BlockLayout extends Record<string, unknown> {
blockId: string;
type: string;
rect?: Rect;
}
export interface ViewportLayout {
blocks: BlockLayout[];
rect: Rect;
rect: {
x: number;
y: number;
w: number;
h: number;
};
}
export interface TextRect {
@@ -60,7 +61,7 @@ export type WorkerToHostMessage = MessageBitmapPainted | MessagePaintError;
export type MessagePaint = {
type: 'paintLayout';
data: {
layout: ViewportLayout;
layout: ViewportLayoutTree;
width: number;
height: number;
dpr: number;
@@ -89,3 +90,15 @@ export interface TurboRendererConfig {
}
export type HostToWorkerMessage = MessagePaint;
export interface BlockLayoutTreeNode {
blockId: string;
type: string;
layout: BlockLayout;
children: BlockLayoutTreeNode[];
}
export interface ViewportLayoutTree {
roots: BlockLayoutTreeNode[];
overallRect: BlockLayout['rect'];
}

View File

@@ -11,6 +11,24 @@ export function randomSeed(): number {
return Math.floor(Math.random() * 2 ** 31);
}
/**
* Calculates the intersection point of two line segments.
*
* @param sp - Start point of the first line segment [x, y]
* @param ep - End point of the first line segment [x, y]
* @param sp2 - Start point of the second line segment [x, y]
* @param ep2 - End point of the second line segment [x, y]
* @param infinite - If true, treats the lines as infinite lines rather than line segments
* @returns The intersection point [x, y] if the lines intersect, null if they are parallel or coincident
*
* @example
* const intersection = lineIntersects([0, 0], [2, 2], [0, 2], [2, 0]);
* // Returns [1, 1] - the intersection point of the two line segments
*
* @example
* const parallel = lineIntersects([0, 0], [2, 2], [0, 1], [2, 3], true);
* // Returns null - the lines are parallel
*/
export function lineIntersects(
sp: IVec,
ep: IVec,
@@ -45,10 +63,23 @@ export function lineIntersects(
return null;
}
/**
* Finds the nearest point on a polygon to a given point.
*
* @param points - Array of points defining the polygon vertices [x, y][]
* @param point - The point to find the nearest point to [x, y]
* @returns The nearest point on the polygon to the given point
* @throws Error if points array is empty or has less than 2 points
*/
export function polygonNearestPoint(points: IVec[], point: IVec) {
const len = points.length;
let rst: IVec;
let dis = Infinity;
if (len < 2) {
throw new Error('Polygon must have at least 2 points');
}
let rst: IVec = points[0]; // Initialize with first point as fallback
let dis = Vec.dist(points[0], point);
for (let i = 0; i < len; i++) {
const p = points[i];
const p2 = points[(i + 1) % len];
@@ -59,7 +90,7 @@ export function polygonNearestPoint(points: IVec[], point: IVec) {
rst = temp;
}
}
return rst!;
return rst;
}
export function polygonPointDistance(points: IVec[], point: IVec) {

View File

@@ -341,8 +341,15 @@ export class Bound implements IBound {
return serializeXYWH(this.x, this.y, this.w, this.h);
}
/**
* Convert a point to relative coordinates.
* @param point - The point to convert.
* @returns The normalized relative coordinates of the point.
*/
toRelative([x, y]: IVec): IVec {
return [(x - this.x) / this.w, (y - this.y) / this.h];
const normalizedX = this.w === 0 ? 0 : (x - this.x) / this.w;
const normalizedY = this.h === 0 ? 0 : (y - this.y) / this.h;
return [normalizedX, normalizedY];
}
toXYWH(): XYWH {

View File

@@ -565,6 +565,8 @@ export class Vec {
* @param n
* @param min
*/
static clampV(A: IVec, min: number, max?: number): IVec;
static clampV(A: number[], min: number): number[];
// eslint-disable-next-line @typescript-eslint/unified-signatures

View File

@@ -214,7 +214,12 @@ export class Viewport {
* This property is used to calculate the scale of the editor.
*/
get viewScale() {
if (!this._shell || this._cachedOffsetWidth === null) return 1;
if (
!this._shell ||
this._cachedOffsetWidth === null ||
this._cachedOffsetWidth === 0
)
return 1;
return this.boundingClientRect.width / this._cachedOffsetWidth;
}

View File

@@ -1,3 +1,4 @@
import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
import {
TurboRendererConfigFactory,
@@ -11,6 +12,7 @@ import { createPainterWorker, setupEditor } from './setup.js';
async function init() {
setupEditor('edgeless', [
ParagraphLayoutHandlerExtension,
ListLayoutHandlerExtension,
TurboRendererConfigFactory({
painterWorkerEntry: createPainterWorker,
}),

View File

@@ -103,7 +103,7 @@ async function createEditor(
export function createPainterWorker() {
const worker = new Worker(
new URL('./turbo-painter-entry.worker.ts', import.meta.url),
new URL('./turbo-painter.worker.ts', import.meta.url),
{
type: 'module',
}

View File

@@ -1,4 +0,0 @@
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine-block-paragraph/turbo-painter';
import { ViewportLayoutPainter } from '@blocksuite/affine-gfx-turbo-renderer/painter';
new ViewportLayoutPainter([ParagraphLayoutPainterExtension]);

View File

@@ -0,0 +1,10 @@
import { ListLayoutPainterExtension } from '@blocksuite/affine-block-list/turbo-painter';
import { NoteLayoutPainterExtension } from '@blocksuite/affine-block-note/turbo-painter';
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine-block-paragraph/turbo-painter';
import { ViewportLayoutPainter } from '@blocksuite/affine-gfx-turbo-renderer/painter';
new ViewportLayoutPainter([
ParagraphLayoutPainterExtension,
ListLayoutPainterExtension,
NoteLayoutPainterExtension,
]);

View File

@@ -82,7 +82,7 @@
"husky": "^9.1.7",
"lint-staged": "^15.2.11",
"msw": "^2.6.8",
"oxlint": "0.16.4",
"oxlint": "0.16.5",
"prettier": "^3.4.2",
"semver": "^7.6.3",
"serve": "^14.2.4",

View File

@@ -67,7 +67,7 @@
"@opentelemetry/semantic-conventions": "^1.28.0",
"@prisma/client": "^5.22.0",
"@prisma/instrumentation": "^5.22.0",
"@react-email/components": "0.0.35",
"@react-email/components": "0.0.36",
"@socket.io/redis-adapter": "^8.3.0",
"ai": "^4.1.51",
"bullmq": "^5.40.2",
@@ -140,7 +140,7 @@
"c8": "^10.1.3",
"cross-env": "^7.0.3",
"nodemon": "^3.1.7",
"react-email": "4.0.3",
"react-email": "4.0.7",
"sinon": "^20.0.0",
"supertest": "^7.0.0",
"why-is-node-running": "^3.2.2"

View File

@@ -162,7 +162,7 @@ export class TeamWorkspaceResolver {
if (Number.isSafeInteger(expireTime)) {
return {
link: this.url.link(`/invite/${id.inviteId}`),
expireTime: new Date(Date.now() + expireTime),
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
};
}
}
@@ -188,7 +188,7 @@ export class TeamWorkspaceResolver {
if (Number.isSafeInteger(expireTime)) {
return {
link: this.url.link(`/invite/${invite.inviteId}`),
expireTime: new Date(Date.now() + expireTime),
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
};
}
}

View File

@@ -80,7 +80,7 @@ export class Env implements AppEnv {
);
DEPLOYMENT_TYPE = readEnv(
'DEPLOYMENT_TYPE',
DeploymentType.Selfhosted,
this.dev ? DeploymentType.Affine : DeploymentType.Selfhosted,
Object.values(DeploymentType)
);
FLAVOR = readEnv('SERVER_FLAVOR', Flavor.AllInOne, Object.values(Flavor));

View File

@@ -1,7 +1,5 @@
export function getWorkerUrl(name: string) {
if (BUILD_CONFIG.debug && !name.endsWith('.worker.js')) {
throw new Error(`worker should be named with '.worker.js', get ${name}`);
}
return environment.workerPath + name + '?v=' + BUILD_CONFIG.appVersion;
return (
environment.workerPath + `${name}-${BUILD_CONFIG.appVersion}.worker.js`
);
}

View File

@@ -9,7 +9,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.android.tools.build:gradle:8.9.1'
}
}

View File

@@ -6,15 +6,15 @@ apollo = "4.1.1"
appcompat = "1.7.0"
browser = "1.8.0"
coordinatorLayout = "1.3.0"
coreKtx = "1.15.0"
coreKtx = "1.16.0"
coreSplashScreen = "1.0.1"
googleServices = "4.4.2"
jna = "5.17.0"
junitVersion = "4.13.2"
kotlin = "2.1.20"
kotlinxCoroutines = "1.10.1"
kotlinxCoroutines = "1.10.2"
material = "1.12.0"
material3 = "1.3.1"
material3 = "1.3.2"
rustAndroid = "0.9.6"
[libraries]

View File

@@ -48,7 +48,7 @@ import { AffineTheme } from './plugins/affine-theme';
import { AIButton } from './plugins/ai-button';
const storeManagerClient = new StoreManagerClient(
new OpClient(new Worker(getWorkerUrl('nbstore.worker.js')))
new OpClient(new Worker(getWorkerUrl('nbstore')))
);
window.addEventListener('beforeunload', () => {
storeManagerClient.dispose();

View File

@@ -407,7 +407,7 @@ export function App() {
}
function createStoreManagerClient() {
const worker = new Worker(getWorkerUrl('nbstore.worker.js'));
const worker = new Worker(getWorkerUrl('nbstore'));
const { port1: nativeDBApiChannelServer, port2: nativeDBApiChannelClient } =
new MessageChannel();
AsyncCall<typeof NbStoreNativeDBApis>(NbStoreNativeDBApis, {

View File

@@ -23,7 +23,7 @@ import { RouterProvider } from 'react-router-dom';
let storeManagerClient: StoreManagerClient;
const workerUrl = getWorkerUrl('nbstore.worker.js');
const workerUrl = getWorkerUrl('nbstore');
if (window.SharedWorker) {
const worker = new SharedWorker(workerUrl, { name: 'affine-shared-worker' });
storeManagerClient = new StoreManagerClient(new OpClient(worker.port));

View File

@@ -24,13 +24,15 @@ const cache = createEmotionCache();
let storeManagerClient: StoreManagerClient;
const workerUrl = getWorkerUrl('nbstore.worker.js');
const workerUrl = getWorkerUrl('nbstore');
if (
window.SharedWorker &&
localStorage.getItem('disableSharedWorker') !== 'true'
) {
const worker = new SharedWorker(workerUrl);
const worker = new SharedWorker(workerUrl, {
name: 'affine-shared-worker',
});
storeManagerClient = new StoreManagerClient(new OpClient(worker.port));
} else {
const worker = new Worker(workerUrl);

View File

@@ -1,8 +1,10 @@
import { ListLayoutPainterExtension } from '@blocksuite/affine/blocks/list';
import { NoteLayoutPainterExtension } from '@blocksuite/affine/blocks/note';
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine/blocks/paragraph';
import { ViewportLayoutPainter } from '@blocksuite/affine/gfx/turbo-renderer';
new ViewportLayoutPainter([
ParagraphLayoutPainterExtension,
ListLayoutPainterExtension,
NoteLayoutPainterExtension,
]);

View File

@@ -1,5 +1,6 @@
import { getWorkerUrl } from '@affine/env/worker';
import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
import { NoteLayoutHandlerExtension } from '@blocksuite/affine/blocks/note';
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
import {
TurboRendererConfigFactory,
@@ -7,7 +8,7 @@ import {
} from '@blocksuite/affine/gfx/turbo-renderer';
function createPainterWorker() {
const worker = new Worker(getWorkerUrl('turbo-painter-entry.worker.js'));
const worker = new Worker(getWorkerUrl('turbo-painter'));
return worker;
}
@@ -15,6 +16,7 @@ export function patchTurboRendererExtension() {
return [
ParagraphLayoutHandlerExtension,
ListLayoutHandlerExtension,
NoteLayoutHandlerExtension,
TurboRendererConfigFactory({
options: {
zoomThreshold: 1,

View File

@@ -276,7 +276,7 @@ const ActionModal = ({
),
2: (
<a
href="https://affine.pro/pricing"
href="https://affine.pro/pricing/?type=selfhost#table"
target="_blank"
rel="noreferrer"
style={{ color: 'var(--affine-link-color)' }}

View File

@@ -376,6 +376,10 @@ export class AtMenuConfigService extends Service {
close();
track.doc.editor.atMenu.mentionMember({
type: 'member',
});
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange || inlineRange.length !== 0) return;
@@ -440,6 +444,8 @@ export class AtMenuConfigService extends Service {
const err = UserFriendlyError.fromAny(error);
if (err.is(ErrorNames.MENTION_USER_DOC_ACCESS_DENIED)) {
track.doc.editor.atMenu.noAccessPrompted();
const canUserManage = this.guardService.can$(
'Doc_Users_Manage',
docId
@@ -456,6 +462,11 @@ export class AtMenuConfigService extends Service {
action: {
label: 'Invite',
onClick: async () => {
track.$.sharePanel.$.inviteUserDocRole({
control: 'member list',
role: 'reader',
});
try {
await this.docGrantedUsersService.updateUserRole(
id,
@@ -517,6 +528,11 @@ export class AtMenuConfigService extends Service {
icon: UserIcon(),
action: () => {
close();
track.doc.editor.atMenu.mentionMember({
type: 'invite',
});
this.dialogService.open('setting', {
activeTab: 'workspace:members',
});

View File

@@ -6,7 +6,12 @@ export { NotificationType } from './stores/notification';
import type { Framework } from '@toeverything/infra';
import { GraphQLService, ServerScope, ServerService } from '../cloud';
import {
AuthService,
GraphQLService,
ServerScope,
ServerService,
} from '../cloud';
import { GlobalSessionState } from '../storage';
import { NotificationCountService } from './services/count';
import { NotificationListService } from './services/list';
@@ -17,7 +22,7 @@ export function configureNotificationModule(framework: Framework) {
framework
.scope(ServerScope)
.service(NotificationService, [NotificationStore])
.service(NotificationCountService, [NotificationStore])
.service(NotificationCountService, [NotificationStore, AuthService])
.service(NotificationListService, [
NotificationStore,
NotificationCountService,

View File

@@ -12,17 +12,24 @@ import {
} from '@toeverything/infra';
import { EMPTY, mergeMap, switchMap, timer } from 'rxjs';
import { AccountChanged, type AuthService } from '../../cloud';
import { ServerStarted } from '../../cloud/events/server-started';
import { ApplicationFocused } from '../../lifecycle';
import type { NotificationStore } from '../stores/notification';
@OnEvent(ApplicationFocused, s => s.handleApplicationFocused)
@OnEvent(ServerStarted, s => s.handleServerStarted)
@OnEvent(AccountChanged, s => s.handleAccountChanged)
export class NotificationCountService extends Service {
constructor(private readonly store: NotificationStore) {
constructor(
private readonly store: NotificationStore,
private readonly authService: AuthService
) {
super();
}
loggedIn$ = this.authService.session.status$.map(v => v === 'authenticated');
readonly count$ = LiveData.from(this.store.watchNotificationCountCache(), 0);
readonly isLoading$ = new LiveData(false);
readonly error$ = new LiveData<any>(null);
@@ -32,9 +39,12 @@ export class NotificationCountService extends Service {
return timer(0, 30000); // revalidate every 30 seconds
}),
exhaustMapWithTrailing(() => {
return fromPromise(signal =>
this.store.getNotificationCount(signal)
).pipe(
return fromPromise(signal => {
if (!this.loggedIn$.value) {
return Promise.resolve(0);
}
return this.store.getNotificationCount(signal);
}).pipe(
mergeMap(result => {
this.setCount(result ?? 0);
return EMPTY;
@@ -57,6 +67,10 @@ export class NotificationCountService extends Service {
this.revalidate();
}
handleAccountChanged() {
this.revalidate();
}
setCount(count: number) {
this.store.setNotificationCountCache(count);
}

View File

@@ -7,7 +7,7 @@ export class PDFRenderer extends OpClient<ClientOps> {
private readonly worker: Worker;
constructor() {
const worker = new Worker(getWorkerUrl('pdf.worker.js'));
const worker = new Worker(getWorkerUrl('pdf'));
super(worker);
this.worker = worker;

View File

@@ -10,7 +10,7 @@ export function getWorkspaceProfileWorker() {
return worker;
}
const rawWorker = new Worker(getWorkerUrl('workspace-profile.worker.js'));
const rawWorker = new Worker(getWorkerUrl('workspace-profile'));
worker = new OpClient<WorkerOps>(rawWorker);
return worker;

View File

@@ -177,6 +177,10 @@ type MeetingEvents =
| 'activeMenubarAppItem';
// END SECTION
// SECTION: mention
type MentionEvents = 'mentionMember' | 'noAccessPrompted';
// END SECTION
type UserEvents =
| GeneralEvents
| AppEvents
@@ -198,7 +202,9 @@ type UserEvents =
| TemplateEvents
| NotificationEvents
| IntegrationEvents
| MeetingEvents;
| MeetingEvents
| MentionEvents;
interface PageDivision {
[page: string]: {
[segment: string]: {
@@ -405,7 +411,13 @@ const PageEvents = {
doc: {
editor: {
slashMenu: ['linkDoc', 'createDoc', 'bookmark'],
atMenu: ['linkDoc', 'import', 'createDoc'],
atMenu: [
'linkDoc',
'import',
'createDoc',
'mentionMember',
'noAccessPrompted',
],
quickSearch: ['createDoc'],
formatToolbar: ['bold'],
pageRef: ['navigate'],
@@ -698,6 +710,10 @@ export type EventArgs = {
| 'Meeting Settings'
| 'Quit AFFiNE Completely';
};
mentionMember: {
type: 'member' | 'invite' | 'more';
};
noAccessPrompted: {};
};
// for type checking

View File

@@ -28,7 +28,7 @@ export class EditorUtils {
const lines = await page.$$('page-editor .inline-editor');
const contents = await Promise.all(lines.map(el => el.innerText()));
content = contents
.map(c => c.replace(/\u200B/g, '').trim())
.map(c => c.replace(/[\u200B-\u200D\uFEFF]/g, '').trim())
.filter(c => !!c)
.join('\n');
if (!content) {
@@ -43,7 +43,9 @@ export class EditorUtils {
const edgelessNode = await page.waitForSelector(
'affine-edgeless-note .edgeless-note-page-content'
);
return (await edgelessNode.innerText()).replace(/\u200B/g, '').trim();
return (await edgelessNode.innerText())
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.trim();
}
public static async switchToEdgelessMode(page: Page) {

View File

@@ -16,7 +16,6 @@ import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addCodeBlock,
clickNewPageButton,
getBlockSuiteEditorTitle,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
@@ -178,9 +177,6 @@ test.describe('paste in multiple blocks text selection', () => {
test('paste surface-ref block to another doc as embed-linked-doc block', async ({
page,
}) => {
await openHomePage(page);
await clickNewPageButton(page, 'Clipboard Test');
await waitForEditorLoad(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
@@ -205,21 +201,18 @@ test('paste surface-ref block to another doc as embed-linked-doc block', async (
await insertIntoPageButton.click();
await clickPageModeButton(page);
await page.waitForTimeout(50);
await waitForEditorLoad(page);
await container.click();
// copy surface-ref block
const surfaceRefBlock = page.locator('.affine-surface-ref');
const surfaceRefBlock = page.locator('affine-surface-ref');
await surfaceRefBlock.click();
await page.waitForTimeout(50);
await page.waitForSelector('affine-surface-ref .focused');
await copyByKeyboard(page);
// paste to another doc
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title2 = getBlockSuiteEditorTitle(page);
await title2.pressSequentially('page2');
await page.keyboard.press('Enter');
await page.waitForTimeout(50);
await clickNewPageButton(page, 'page2');
await pressEnter(page);
// paste the surface-ref block
await pasteByKeyboard(page);

View File

@@ -1,13 +1,19 @@
import { expect } from '@playwright/test';
import { clickView } from '../utils/actions/click.js';
import {
addBasicRectShapeElement,
getSelectedBoundCount,
locatorComponentToolbar,
resizeElementByHandle,
selectNoteInEdgeless,
switchEditorMode,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
pressBackspace,
selectAllBlocksByKeyboard,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
@@ -87,3 +93,26 @@ test('should be hidden when resizing element', async ({ page }) => {
await expect(toolbar).toBeVisible();
});
test('should only one tool active at the same time when using shortcut to switch tool', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await selectAllBlocksByKeyboard(page);
await pressBackspace(page);
await page.keyboard.press('s');
await page.keyboard.press('m');
await page.keyboard.press('n');
await clickView(page, [100, 100]);
await clickView(page, [0, 0]); // click on empty space to deselect the note
await selectAllBlocksByKeyboard(page);
expect(
await getSelectedBoundCount(page),
'only a note should be created'
).toBe(1);
});

View File

@@ -9,7 +9,9 @@ import {
changeShapeStrokeStyle,
changeShapeStrokeWidth,
clickComponentToolbarMoreMenuButton,
dragBetweenViewCoords,
getEdgelessSelectedRect,
getSelectedBoundCount,
locatorComponentToolbar,
locatorEdgelessToolButton,
locatorShapeStrokeStyleButton,
@@ -24,13 +26,17 @@ import {
import {
addBasicBrushElement,
addBasicRectShapeElement,
clickView,
copyByKeyboard,
dblclickView,
dragBetweenCoords,
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
pasteByKeyboard,
pressBackspace,
pressEscape,
selectAllBlocksByKeyboard,
type,
waitNextFrame,
} from '../utils/actions/index.js';
@@ -40,6 +46,7 @@ import {
assertEdgelessNonSelectedRect,
assertEdgelessSelectedRect,
assertRichTexts,
assertSelectedBound,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
@@ -739,3 +746,45 @@ test.describe('shape hit test', () => {
await assertEdgelessCanvasText(page, 'hello world');
});
});
test('should create a shape when press s and click on canvas', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await zoomResetByKeyboard(page);
await selectAllBlocksByKeyboard(page);
await pressBackspace(page);
await page.keyboard.press('s');
await assertEdgelessTool(page, 'shape');
await clickView(page, [100, 100]);
await selectAllBlocksByKeyboard(page);
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [100, 100, 100, 100]);
});
test('shape should be editable when re-enter canvas', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await zoomResetByKeyboard(page);
await selectAllBlocksByKeyboard(page);
await pressBackspace(page);
await page.keyboard.press('s');
await dragBetweenViewCoords(page, [0, 0], [100, 100]);
await dblclickView(page, [50, 50]);
await type(page, 'hello');
await expect(page.locator('edgeless-shape-text-editor')).toBeAttached();
await assertEdgelessCanvasText(page, 'hello');
await switchEditorMode(page);
await switchEditorMode(page);
await dblclickView(page, [50, 50]);
await expect(page.locator('edgeless-shape-text-editor')).toBeAttached();
});

View File

@@ -405,10 +405,7 @@ export function createWorkerTargetConfig(
pkg: Package,
entry: string
): Omit<webpack.Configuration, 'name'> & { name: string } {
const workerName = path.basename(entry).replace(/\.([^.]+)$/, '');
if (!workerName.endsWith('.worker')) {
throw new Error('Worker name must end with `.worker.[ext]`');
}
const workerName = path.basename(entry).replace(/\.worker\.ts$/, '');
const buildConfig = getBuildConfigFromEnv(pkg);
return {
@@ -423,7 +420,7 @@ export function createWorkerTargetConfig(
[workerName]: entry,
},
output: {
filename: 'js/[name].js',
filename: `js/${workerName}-${buildConfig.appVersion}.worker.js`,
path: pkg.distPath.value,
clean: false,
globalObject: 'globalThis',

View File

@@ -270,6 +270,7 @@ export const PackageList = [
'blocksuite/affine/blocks/surface',
'blocksuite/affine/components',
'blocksuite/affine/fragments/doc-title',
'blocksuite/affine/gfx/turbo-renderer',
'blocksuite/affine/inlines/preset',
'blocksuite/affine/model',
'blocksuite/affine/rich-text',

625
yarn.lock

File diff suppressed because it is too large Load Diff