Compare commits

...

66 Commits

Author SHA1 Message Date
akumatus
512a908fd4 fix(core): generate the image cannot enter text prompt (#12717)
Close [AI-167](https://linear.app/affine-design/issue/AI-167)

![截屏2025-06-05 12.15.49.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/c1afff0e-1197-46dc-ae43-ff7257039509.png)

![截屏2025-06-05 12.14.07.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/1439b0a7-1cca-4848-aea2-84bc73c536c5.png)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved AI panel behavior with explicit modes for input and answer generation, providing a more intuitive user experience when interacting with AI features.

- **Refactor**
  - Streamlined AI panel toggling logic for more consistent and predictable panel states during different actions.

- **Tests**
  - Enhanced AI image generation test to simulate user input and send actions for more accurate end-to-end validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 09:17:28 +00:00
liuyi
71be1d424a fix(server): oidc registration (#12723) 2025-06-05 09:16:21 +00:00
renovate
d6a26b8093 chore: bump up multer version to v2.0.1 [SECURITY] (#12716)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [multer](https://redirect.github.com/expressjs/multer) | [`2.0.0` -> `2.0.1`](https://renovatebot.com/diffs/npm/multer/2.0.0/2.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/multer/2.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/multer/2.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/multer/2.0.0/2.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/multer/2.0.0/2.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

### GitHub Vulnerability Alerts

#### [CVE-2025-48997](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-g5hg-p3ph-g8qg)

### Impact

A vulnerability in Multer versions >=1.4.4-lts.1, <2.0.1 allows an attacker to trigger a Denial of Service (DoS) by sending an upload file request with an empty string field name. This request causes an unhandled exception, leading to a crash of the process.

### Patches

Users should upgrade to `2.0.1`

### Workarounds

None

### References

35a3272b61
[https://github.com/expressjs/multer/issues/1233](https://redirect.github.com/expressjs/multer/issues/1233)
[https://github.com/expressjs/multer/pull/1256](https://redirect.github.com/expressjs/multer/pull/1256)

---

### Release Notes

<details>
<summary>expressjs/multer (multer)</summary>

### [`v2.0.1`](https://redirect.github.com/expressjs/multer/blob/HEAD/CHANGELOG.md#201)

[Compare Source](https://redirect.github.com/expressjs/multer/compare/v2.0.0...v2.0.1)

-   Fix [CVE-2025-48997](https://www.cve.org/CVERecord?id=CVE-2025-48997) ([GHSA-g5hg-p3ph-g8qg](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-g5hg-p3ph-g8qg))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), 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:eyJjcmVhdGVkSW5WZXIiOiI0MC40MC4zIiwidXBkYXRlZEluVmVyIjoiNDAuNDAuMyIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-06-05 07:39:03 +00:00
EYHN
5e05952f6e feat(core): optimize tag performance (#12719)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Improved tag options management to use a reactive, real-time approach, ensuring tag options are always up to date throughout the application.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 07:13:31 +00:00
JimmFly
c1930c5937 chore: adjust general access button styles (#12718)
close AF-2685

When the button is disabled, the frontmost icon is not positioned correctly. This commit is to fix the icon position.

![CleanShot 2025-06-05 at 12 38 55@2x](https://github.com/user-attachments/assets/af2f80bc-69a0-4e33-bc8f-e5e169f769fc)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Improved the layout of the share menu trigger text by aligning its content vertically and adding spacing between elements for a cleaner appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 06:59:18 +00:00
fengmk2
b7ebd33389 chore(server): ignore rolled back error on the first time (#12714)
close #12692

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved error handling during migration rollbacks to better recognize and safely skip specific migration errors.
  - Enhanced logging for migration rollback failures to provide clearer information without interrupting the process.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 04:00:21 +00:00
Brooooooklyn
de9a3e1428 ci: fix missing environment in build-server-native (#12712)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Chores**
  - Updated workflow configuration to set the environment for the build process based on input parameters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 03:44:35 +00:00
fengmk2
374eee9196 chore(server): disable indexer by default (#12710)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Chores**
	- Updated the default setting for the indexer feature to be disabled by default.
	- Added a sample environment variable for enabling the indexer in the example configuration file.
	- Introduced a new environment variable for the indexer in the CI workflow configuration.
- **Tests**
	- Adjusted test configurations to explicitly enable the indexer feature during test execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 03:28:12 +00:00
donteatfriedrice
1bdccdbd57 feat(editor): track citation events (#12664)
Closes: [BS-3551](https://linear.app/affine-design/issue/BS-3551/citation埋点)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced citation tracking across attachments, bookmarks, embedded documents, paragraphs, footnotes, rename modals, and toolbars for actions like editing, deleting, expanding, and hovering on citations.
  - Introduced a centralized citation service to unify citation detection and telemetry event management.
- **Chores**
  - Updated service exports and telemetry modules to include the new citation service and citation-related event types.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 03:09:24 +00:00
donteatfriedrice
053efb61f0 fix(editor): should set event dispatcher active as false when document is hidden (#12559)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - The application now automatically becomes inactive when the document is hidden, improving resource management and responsiveness to visibility changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-05 02:28:32 +00:00
pengx17
c7aebd0412 fix(electron): revert back electron to v35 (#12704)
v36 breaks worker loading in Electron's renderer
this use to work by turning off "PlzDedicatedWorker"
related to https://github.com/electron/electron/issues/43556

Before we know the root cause, revert back the electron version.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
  - Updated the version of Electron used in the application.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 15:17:51 +00:00
renovate
01aa6979eb chore: bump up @nestjs-cls/transactional-adapter-prisma version to v1.2.23 (#12680)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@nestjs-cls/transactional-adapter-prisma](https://papooch.github.io/nestjs-cls/) ([source](https://redirect.github.com/Papooch/nestjs-cls)) | [`1.2.21` -> `1.2.23`](https://renovatebot.com/diffs/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.21/1.2.23) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.23?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.23?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.21/1.2.23?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.21/1.2.23?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>Papooch/nestjs-cls (@&#8203;nestjs-cls/transactional-adapter-prisma)</summary>

### [`v1.2.23`](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.22...@nestjs-cls/transactional-adapter-prisma@1.2.23)

[Compare Source](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.22...@nestjs-cls/transactional-adapter-prisma@1.2.23)

### [`v1.2.22`](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.21...@nestjs-cls/transactional-adapter-prisma@1.2.22)

[Compare Source](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.21...@nestjs-cls/transactional-adapter-prisma@1.2.22)

</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:eyJjcmVhdGVkSW5WZXIiOiI0MC4zMy42IiwidXBkYXRlZEluVmVyIjoiNDAuNDAuMyIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-06-04 13:00:41 +00:00
akumatus
c32f7c7964 fix(core): read-only editor does not support code preview (#12700)
Close [AI-160](https://linear.app/affine-design/issue/AI-160)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved preview state management for code blocks, ensuring consistent behavior in both editable and readonly modes.

- **Refactor**
  - Streamlined the way preview state is toggled and displayed for code blocks, resulting in a more reliable and maintainable user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 12:41:10 +00:00
fengmk2
d219c92e98 chore(server): ignore never applied rolled back error (#12703)
closes #12701

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved error handling during migration rollbacks to prevent unnecessary errors when migrations were never applied.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 11:37:32 +00:00
darkskygit
063072457c fix(server): chat with image (#12699)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Improved the handling of attachments in chat messages for more efficient processing of images and files without impacting user experience.
- **Chores**
  - Added internal logging to enhance monitoring of AI model interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 08:51:02 +00:00
darkskygit
13fa4f922a fix(server): token calculate (#12667) 2025-06-04 07:09:33 +00:00
fengmk2
f54bc0c047 chore(server): auto roll back failed migrations (#12697) 2025-06-04 14:45:42 +08:00
akumatus
1f0cc51462 fix(core): ai retry missing reasoning and webSearch params (#12690)
Close [AI-165](https://linear.app/affine-design/issue/AI-165)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced chat components to support advanced reasoning and network search options, providing more control over AI-powered interactions.
  - Improved polling for context documents and files, now also triggered by additional chip types for more comprehensive updates.

- **Bug Fixes**
  - Ensured consistent application of configuration settings across all relevant chat components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 03:42:25 +00:00
EYHN
160e4c2a38 feat(core): add title order by (#12696)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for ordering documents by their title.
  - Introduced a new "title" system property type with an associated icon and display name.

- **Improvements**
  - Enhanced system property types to allow more flexible filtering options.
  - Improved filter condition handling to show an unknown filter UI when filtering methods or values are unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 02:41:55 +00:00
darkskygit
99198e246b fix: migration compatible for postgres (#12659)
fix AI-162

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Chores**
  - Improved database migration scripts to prevent errors by ensuring changes are only applied if relevant tables exist. No visible changes to user features or functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-04 02:21:57 +00:00
darkskygit
44e1eb503f feat(server): improve embedding & rerank speed (#12666)
fix AI-109
2025-06-03 11:12:35 +00:00
CatsJuice
2288cbe54d chore(core): remove calendar integration feature flag (#12689)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - All available integrations are now shown without restriction; calendar integration is always visible.

- **Chores**
  - Removed an obsolete feature flag related to calendar integration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 10:58:19 +00:00
JimmFly
23ff398994 feat(mobile): add delete account function (#12688)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a "Delete my account" option in mobile settings with role-based warnings and confirmation modals.
- **Enhancements**
  - Added flexible row and reverse row layout options for modal footers and action buttons on mobile.
- **Localization**
  - Added English translation for the "Delete my account" setting.
- **Style**
  - Updated styles for modal footers and action buttons on mobile.
  - Added styling for account deletion dialog descriptions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 10:17:45 +00:00
forehalo
ee931d546e fix(server): oauth should follow sign up restriction (#12683)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Enforced signup restrictions for OAuth login based on configuration settings. Users will not be able to sign up via OAuth if signup is disabled by the administrator.
- **Bug Fixes**
	- Improved error handling during OAuth login when signup is not permitted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 08:53:00 +00:00
aki-chang-dev
a02eed382d feat(android): chat base feature (#12684)
- **feat(android): chat send & receive**
- **[WIP] feat(android): markdown style for chat**
- **fix(android): fix auto scroll & ai message id replacement**
- **feat(android): replace icons**
- **refactor(android): design system**
- **feat(android): markdown style for chat**

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a comprehensive custom theme system with new color palettes, typography, and theme modes (Light, Dark, System).
  - Added support for rendering Markdown-formatted text in chat messages with custom styling.
  - Integrated new vector icons for UI elements such as lists, camera, image, send, close, and more.
  - Added composable icon and icon button components for consistent icon usage across the app.

- **Enhancements**
  - Updated chat UI to use the new theme, icons, and Markdown rendering for AI messages.
  - Improved chat message management and send button state handling with enhanced session retrieval and SSE stream processing.
  - Refined app bar and dropdown menu components with updated icons and theme integration.
  - Enhanced floating action button appearance with tinted vector drawable.
  - Unified UI components and styling under the AFFiNE design system in chat input and app bars.

- **Bug Fixes**
  - Corrected application and theme class naming for consistency.

- **Chores**
  - Added new dependencies for rich text and Markdown support.
  - Updated color and icon resources for a unified visual style.
  - Removed deprecated headers from authentication requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 07:35:48 +00:00
L-Sun
ab78b8e3ab fix(editor): playground init error (#12565)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved stability when observing document title changes by ensuring internal checks before updating.
	- Enhanced document initialization to reuse existing documents when available, reducing unnecessary duplication and improving performance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 07:18:29 +00:00
yoyoyohamapi
3fe2ac4e46 refactor(core): add to edgeless as note icon (#12656)
### TL;DR

refactor(core): add to edgeless as note icon

> CLOSE AI-152

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Style**
  - Updated the icon for the "Add to Edgeless as Note" chat action to improve visual representation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 07:02:04 +00:00
Brooooooklyn
d02aa8c7e0 fix(native): opt out napi-derive noop feature (#12686)
It would cause the napi-derive not work as expect in workspace level

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Improved internal handling and type definitions for document parsing, resulting in clearer and more maintainable data structures.
- **Chores**
  - Introduced a new feature flag for mobile native builds, enabling conditional compilation for enhanced flexibility across Android and iOS.
  - Updated build scripts to support the new feature flag for both Android and iOS platforms.
  - Updated iOS app dependencies to newer versions, including Apollo iOS, ChidoriMenu, and swift-collections, and removed SQLite.swift.
- **Tests**
  - Enhanced Rust linting and testing workflows to run selectively across workspace packages with the new feature flag enabled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 06:46:55 +00:00
akumatus
cce756365a feat(core): use claude 4 as default chat model (#12596)
Support [AI-59](https://linear.app/affine-design/issue/AI-59)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Updated the default AI model for chat prompts to use Claude Sonnet 4.
- **Bug Fixes**
  - Improved model selection logic to better support reasoning features across more AI models.
- **Tests**
  - Enhanced test cases with consistent instructions for response length.
  - Skipped certain chat-related tests to refine test suite stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 06:32:27 +00:00
yoyoyohamapi
a88dcc0951 fix(core): copy & paste ai message failed (#12655)
### TL;DR
* fix: ai message copy bug
  * Select a section of content in the Page
  * Choose the user's question from the AI chat conversation history and copy it
  * The copied (pasted) content will be the selected section from the Page

* fix: ai message paste bug
  * Select a section of content in the Page
  * Choose the user's question from the AI chat conversation history and copy it
  * Paste it into the AI Input, and the content will be pasted back into the original Page text

> CLOSE AF-2683

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved handling of copy and paste events in chat components to prevent unintended interactions with surrounding elements.
  - Enhanced test stability by adding error handling during embedding progress checks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 06:09:44 +00:00
darkskygit
57208a3de4 fix(server): lost context after merge template (#12682)
fix AI-163
fix AI-164

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Tests**
  - Added a new test to verify multi-turn chat interactions, ensuring accurate handling of chat history and correct responses for translation and explanation requests.
- **Bug Fixes**
  - Improved chat session logic to better merge user messages and attachments, enhancing the accuracy and continuity of multi-step conversations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 05:46:18 +00:00
L-Sun
d8cbeb1bb1 fix(editor): can move frame by dragging title (#12661)
Close [BS-3351](https://linear.app/affine-design/issue/BS-3351/无法通过拖拽frame-title来拖拽frame)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved rendering performance and consistency for widgets within frames.
  - Frame titles are now directly associated with individual frames and are draggable.

- **Bug Fixes**
  - Selection logic for frames has been refined to better handle locked states and title area interactions.

- **Refactor**
  - Frame title widget and related components have been simplified for clarity and maintainability.
  - Removed dynamic positioning and click toggling from frame titles for a cleaner interaction model.

- **Tests**
  - Added a test to verify that frame titles are draggable.
  - Temporarily disabled tests related to frame title stacking and selection due to ongoing changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 05:14:39 +00:00
yoyoyohamapi
418b38e8de test(core): support fast embedding progress (#12685)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Tests**
  - Improved test stability by handling potential errors during embedding progress checks in end-to-end tests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 05:00:21 +00:00
doouding
00ff373c01 fix: tuning drag and resize snapping (#12657)
### Changed
- Better snapping when resize elements

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved resizing behavior with enhanced alignment and snapping during element resizing, supporting rotation and multiple element selection.
  - Alignment lines now display more accurately when resizing elements.

- **Refactor**
  - Resizing logic updated to use scale factors instead of position deltas, enabling smoother and more precise resize operations.
  - Resize event data now includes richer details about handle positions, scaling, and original bounds.
  - Coordinate transformations and scaling now account for rotation and aspect ratio locking more robustly.
  - Cursor updates are disabled during active resize or rotate interactions for a smoother user experience.

- **Tests**
  - Updated resizing tests to use square shapes, ensuring consistent verification of aspect ratio maintenance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 04:12:57 +00:00
darkskygit
39830a410a feat(server): add metrics for copilot job event (#12575)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Chores**
	- Improved internal monitoring for AI embedding operations to enhance reliability and performance tracking. No changes to user-facing features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 03:37:02 +00:00
fundon
ef3be4a816 fix(editor): font weight of label on open doc menu (#12672)
Closes: [BS-3496](https://linear.app/affine-design/issue/BS-3496/toolbar-菜单字重)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated dropdown menu appearance by removing bold styling from button labels.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 03:22:18 +00:00
fundon
658393159b fix(editor): should check url origin and ip address url (#12663)
Closes: [BS-3578](https://linear.app/affine-design/issue/BS-3578/复制本的-url-无法识别)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved URL validation to recognize and allow IPv4 addresses when the origin matches the provided base URL.

- **Tests**
  - Added a test to ensure URLs with IP addresses (e.g., http://127.0.0.1) are considered valid when the origin matches.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-03 03:08:17 +00:00
renovate[bot]
ac3f247f01 chore: bump up apollographql/apollo-ios version to v1.22.0 (#12670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 10:32:12 +08:00
LongYinan
065d9c3b73 ci: fix release-mobile pipeline 2025-06-02 13:28:13 +08:00
LongYinan
2e58c11799 ci: do not use namespace runner anymore 2025-06-01 14:54:12 +08:00
L-Sun
10da3ad28e fix(editor): update card style after dragging it to note (#12660)
Close [BS-3148](https://linear.app/affine-design/issue/BS-3148/拖拽到note后,更新card样式)

### What Changes
- fix the style of card not updated after draggin it from canvas to note
- narrow type of specific card style by using `as const satisfies EmbedCardStyle[]`
- add type hint to the `props`, the second parameter  of `store.updateBlock`

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added middleware to automatically update card styles when dragging blocks into notes.
- **Bug Fixes**
  - Ensured that dragging a bookmark card into a note preserves its style.
- **Tests**
  - Introduced an end-to-end test to verify bookmark card style is retained after drag-and-drop.
- **Refactor**
  - Enhanced type safety and clarity for card style configurations and block properties.
- **Chores**
  - Refined type annotations and assertions across multiple block style constants and toolbar configurations.
  - Improved generic typing for block update methods to increase type precision.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 10:55:29 +00:00
darkskygit
887a496f8b feat(server): add attachment fallback for ai sdk (#12639)
fix AI-161
2025-05-30 08:39:32 +00:00
darkskygit
ada69c80f6 feat(server): only trigger embedding in workspace sync (#12634)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved handling of workspace embedding events to ensure they are only triggered for workspace-type spaces.

- **Chores**
  - Added additional debug logging for document embedding jobs to aid in monitoring and troubleshooting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 08:04:18 +00:00
doufa
7b82dd656b fix(editor): connector not added as frame child (#12611)
Co-authored-by: L-Sun <zover.v@gmail.com>
2025-05-30 13:29:42 +08:00
EYHN
5c96566dd8 feat(core): save all docs options by mode (#12654)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Improved state management for display preferences, view mode, and selected collection in the "All Docs" page, making the experience more modular and consistent, especially when using multiple views.
  - Updated the header component to handle view changes more directly, allowing smoother toggling between different document views.

- **New Features**
  - Enhanced support for independent display settings in split view or multiple "All Docs" instances.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 04:42:27 +00:00
zzj3720
a35e1b1882 feat(editor): add database filter event tracking (#12645)
close: BS-3568

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Added event tracking for filter creation in database views to improve activity monitoring and analytics.

- **Chores**
	- Updated internal event types to support new database view tracking.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 04:27:31 +00:00
yoyoyohamapi
756847d3cb fix(core): prevent ai input tip loop-play (#12600)
### TL;DR

* fix(core): prevent ai input tip loop-play

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added an option to control whether tips in the AI chat composer scroll continuously or stop after the last tip.

- **Style**
  - Improved layout and spacing in the embedding status tooltip for better readability and alignment.

- **Refactor**
  - Updated the structure of elements in the embedding status tooltip for more consistent formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 03:40:11 +00:00
forehalo
3c3a8bb107 feat(server): time duration helper (#12562)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced support for parsing and converting duration strings (e.g., "1h30m") into milliseconds and seconds.
  - Added utility methods to handle a wide range of time units and their combinations.
  - Added functions to calculate dates offset before or after a given date by specified durations.
- **Tests**
  - Implemented comprehensive automated tests to ensure accurate parsing and conversion of duration strings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 03:21:31 +00:00
forehalo
88eec2cdfb chore(server): disable version check for oauth callback (#12640)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved Apple OAuth login reliability by ensuring client version checks do not block the callback process.

- **New Features**
  - Enhanced OAuth account information by including an optional display name field.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 03:05:38 +00:00
renovate
52777b0064 chore: bump up @types/mime-types version to v3 (#12653)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@types/mime-types](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mime-types) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mime-types)) | [`^2.1.4` -> `^3.0.0`](https://renovatebot.com/diffs/npm/@types%2fmime-types/2.1.4/3.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fmime-types/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2fmime-types/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2fmime-types/2.1.4/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fmime-types/2.1.4/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0MC4zMy42IiwidXBkYXRlZEluVmVyIjoiNDAuMzMuNiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-05-30 02:49:07 +00:00
JimmFly
00ccd2d865 chore: display join button text based on invitation type (#12650)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - The button on the Request to Join page now dynamically updates its label to show "accept invitation" when an invitation is pending, improving clarity for users responding to workspace invites.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 02:18:25 +00:00
doodlewind
5d94bd41a4 feat(editor): support triangle and diamond shape in shape dom renderer (#12331)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/ebfcee12-cebb-4b98-81e2-f9f670b4de96.png)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved rendering for diamond and triangle shapes using SVG, resulting in more accurate stroke and fill display.
- **Bug Fixes**
  - Ensured background and border styles do not interfere with SVG-based shapes.
- **Tests**
  - Added tests to verify correct DOM rendering for diamond and triangle shapes.
- **Refactor**
  - Streamlined and clarified the rendering logic for polygonal shapes, separating SVG and CSS rendering paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 02:04:09 +00:00
fundon
20d8d6131a fix(editor): text color of buttons on toolbar (#12642)
Closes: [BS-3574](https://linear.app/affine-design/issue/BS-3574/affine-light-模式,画板dark-模式,toolbar配色崩坏)

<img width="1068" alt="Screenshot 2025-05-29 at 17 46 38" src="https://github.com/user-attachments/assets/66a731dc-0bc6-4b0c-9712-787a78525ddf" />
<img width="1095" alt="Screenshot 2025-05-29 at 17 46 17" src="https://github.com/user-attachments/assets/3317ea83-837f-4c50-abee-ebb859fce3d9" />
<img width="1075" alt="Screenshot 2025-05-29 at 17 46 05" src="https://github.com/user-attachments/assets/3291810b-3aa1-4fce-aa8b-415be5e10c46" />
<img width="1096" alt="Screenshot 2025-05-29 at 17 45 54" src="https://github.com/user-attachments/assets/a5ad5e41-4eb9-4578-85a6-c6b773a03da9" />

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated toolbar theme styles to include an additional color variable for improved customization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 01:47:50 +00:00
doodlewind
94539ac0d0 perf(editor): lazy rendering for dom renderer (#12638)
Before (brush updated even when it's not being dragged):

https://github.com/user-attachments/assets/e56ce326-56ae-4cac-a5f8-86be35fd8fcd

After (fine-grained element level update):

https://github.com/user-attachments/assets/712f4e22-0830-455d-bbe1-0f575e8920ac

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved rendering performance by introducing incremental updates, ensuring only changed elements are updated instead of re-rendering everything.
  - Enhanced responsiveness when elements are added, removed, or updated, as well as during viewport, size, or zoom changes.

- **Bug Fixes**
  - Reduced unnecessary full re-renders, leading to smoother and more efficient user interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-30 01:32:36 +00:00
Brooooooklyn
e1ce42a6fc feat(native): upgrade NAPI-RS to 3.0.0 beta (#12652)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Added a default export for the native binding in the frontend native module, allowing easier imports.

- **Refactor**
	- Streamlined and updated Rust-to-JavaScript type conversions and lifetime handling for improved safety and consistency.
	- Improved object and array construction in Rust modules for more idiomatic usage.
	- Simplified boolean and null value handling in JavaScript interop layers.

- **Chores**
	- Upgraded several dependencies and development tools to newer versions across backend, frontend, and common packages.
	- Updated build scripts for the frontend native package to simplify commands.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 16:09:32 +00:00
renovate
2a7f0162cf chore: bump up nestjs-cls version to v6 (#12648)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [nestjs-cls](https://papooch.github.io/nestjs-cls/) ([source](https://redirect.github.com/Papooch/nestjs-cls)) | [`^5.0.0` -> `^6.0.0`](https://renovatebot.com/diffs/npm/nestjs-cls/5.4.3/6.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nestjs-cls/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nestjs-cls/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nestjs-cls/5.4.3/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nestjs-cls/5.4.3/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>Papooch/nestjs-cls (nestjs-cls)</summary>

### [`v6.0.0`](https://redirect.github.com/Papooch/nestjs-cls/releases/tag/nestjs-cls%406.0.0)

[Compare Source](https://redirect.github.com/Papooch/nestjs-cls/compare/nestjs-cls@5.4.3...nestjs-cls@6.0.0)

##### Breaking Changes

-   The experimental Plugin API has been changed ([4623607](https://redirect.github.com/Papooch/nestjs-cls/commits/4623607))
-   Access to Proxy providers moved to a dedicated `proxy` property on the ClsService ([82cdeef](https://redirect.github.com/Papooch/nestjs-cls/commits/82cdeef))

##### Features

-   **core**: introduce hooks for the Plugin API ([#&#8203;283](https://redirect.github.com/Papooch/nestjs-cls/issues/283)) ([4623607](https://redirect.github.com/Papooch/nestjs-cls/commits/4623607))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MC4zMy42IiwidXBkYXRlZEluVmVyIjoiNDAuMzMuNiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-05-29 15:45:33 +00:00
renovate
34a5d9dec3 chore: bump up @nestjs-cls/transactional-adapter-prisma version to v1.2.21 (#12643)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@nestjs-cls/transactional-adapter-prisma](https://papooch.github.io/nestjs-cls/) ([source](https://redirect.github.com/Papooch/nestjs-cls)) | [`1.2.20` -> `1.2.21`](https://renovatebot.com/diffs/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.20/1.2.21) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.21?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.21?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.20/1.2.21?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.20/1.2.21?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>Papooch/nestjs-cls (@&#8203;nestjs-cls/transactional-adapter-prisma)</summary>

### [`v1.2.21`](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.20...@nestjs-cls/transactional-adapter-prisma@1.2.21)

[Compare Source](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.20...@nestjs-cls/transactional-adapter-prisma@1.2.21)

</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:eyJjcmVhdGVkSW5WZXIiOiI0MC4zMy42IiwidXBkYXRlZEluVmVyIjoiNDAuMzMuNiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-05-29 15:29:07 +00:00
renovate
c68598c0e0 chore: bump up opentelemetry (#12183)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@opentelemetry/exporter-prometheus](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js)) | [`^0.57.0` -> `^0.201.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fexporter-prometheus/0.57.2/0.201.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fexporter-prometheus/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fexporter-prometheus/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fexporter-prometheus/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fexporter-prometheus/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/host-metrics](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/opentelemetry-host-metrics#readme) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.35.4` -> `^0.36.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fhost-metrics/0.35.5/0.36.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fhost-metrics/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fhost-metrics/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fhost-metrics/0.35.5/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fhost-metrics/0.35.5/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/instrumentation](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js)) | [`^0.57.0` -> `^0.201.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation/0.57.2/0.201.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/instrumentation-graphql](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-graphql#readme) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.47.0` -> `^0.49.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-graphql/0.47.1/0.49.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-graphql/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-graphql/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-graphql/0.47.1/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-graphql/0.47.1/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/instrumentation-http](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js)) | [`^0.57.0` -> `^0.201.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-http/0.57.2/0.201.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-http/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-http/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-http/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-http/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/instrumentation-ioredis](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-ioredis#readme) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.47.0` -> `^0.49.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-ioredis/0.47.1/0.49.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-ioredis/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-ioredis/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-ioredis/0.47.1/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-ioredis/0.47.1/0.49.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/instrumentation-nestjs-core](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-nestjs-core#readme) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.44.0` -> `^0.47.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-nestjs-core/0.44.1/0.47.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-nestjs-core/0.47.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-nestjs-core/0.47.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-nestjs-core/0.44.1/0.47.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-nestjs-core/0.44.1/0.47.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/instrumentation-socket.io](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-socket.io#readme) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.46.0` -> `^0.48.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-socket.io/0.46.1/0.48.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-socket.io/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-socket.io/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-socket.io/0.46.1/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-socket.io/0.46.1/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/sdk-node](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js)) | [`^0.57.0` -> `^0.201.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsdk-node/0.57.2/0.201.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsdk-node/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fsdk-node/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fsdk-node/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsdk-node/0.57.2/0.201.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@opentelemetry/semantic-conventions](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions) ([source](https://redirect.github.com/open-telemetry/opentelemetry-js)) | [`1.33.0` -> `1.34.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsemantic-conventions/1.33.0/1.34.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsemantic-conventions/1.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fsemantic-conventions/1.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fsemantic-conventions/1.33.0/1.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsemantic-conventions/1.33.0/1.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>open-telemetry/opentelemetry-js (@&#8203;opentelemetry/exporter-prometheus)</summary>

### [`v0.201.1`](4ce5bd1651...9dbd1e446b)

[Compare Source](4ce5bd1651...9dbd1e446b)

### [`v0.201.0`](7fde94081e...4ce5bd1651)

[Compare Source](7fde94081e...4ce5bd1651)

### [`v0.200.0`](ac8641a5db...7fde94081e)

[Compare Source](ac8641a5db...7fde94081e)

</details>

<details>
<summary>open-telemetry/opentelemetry-js-contrib (@&#8203;opentelemetry/host-metrics)</summary>

### [`v0.36.0`](d4d3c4f14f...32abc4c3c0)

[Compare Source](d4d3c4f14f...32abc4c3c0)

</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:eyJjcmVhdGVkSW5WZXIiOiI0MC43LjEiLCJ1cGRhdGVkSW5WZXIiOiI0MC4zMy42IiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-05-29 15:10:07 +00:00
Flrande
9c81c24fbe fix(editor): clear selection after toggle latex editor (#12637)
- **fix(editor): clear selection after toggle latex editor**
- **chore: remove useless test**

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - LaTeX rendering now outputs MathML format for improved accessibility and compatibility.
  - Added support for KaTeX styling to enhance LaTeX display in the playground.

- **Bug Fixes**
  - Improved editor behavior by resetting the selection group before opening the LaTeX editor.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 14:37:41 +00:00
congzhou09
517aec79ba fix(editor): invoke subscriber.unsubscribe() during cleanup (#12628) 2025-05-29 22:30:55 +08:00
fengmk2
31a1841e25 chore(server): log removed job id (#12646)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
	- Improved log messages to include job IDs when jobs are removed from the queue, enhancing traceability for users monitoring job activity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 11:48:16 +00:00
L-Sun
625e8392a6 fix(editor): missing block in select-all set (#12627)
This PR fixed that the block is missing in the selecte-all set after undo a dragging from canvas to note. Related to #12473

### Before

https://github.com/user-attachments/assets/828b4f48-689a-4975-bba6-f380f324de3c

### After

https://github.com/user-attachments/assets/9996c1ca-c3ea-415c-ab2b-359d826a1ffa

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved handling of changes to child elements, ensuring more accurate updates when items are added or removed. This results in more reliable display and interaction with nested components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 11:04:01 +00:00
pengx17
f616bd29d3 fix(core): adjust some uis for sharing (#12486)
fix AF-2660

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Improved visual appearance of sidebar buttons and quick search input, including reduced sizes, updated padding, and enhanced hover effects.
  - Adjusted layout spacing for quick search and new page elements in the sidebar.
  - Updated share button styling to use the primary variant.

- **New Features**
  - Notification cards now only display messages and action footers when relevant, providing a cleaner interface.

- **Refactor**
  - Removed shortcut hint and spotlight elements from the quick search input for a simplified user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 09:40:11 +00:00
CatsJuice
d6b9e9c60a feat(mobile): share page support (#12351)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved server context awareness for workspaces on mobile web.
  - Enhanced handling for missing workspaces by displaying a share page when accessing a document detail route in mobile web environments.

- **Bug Fixes**
  - Workspace list now refreshes automatically when switching workspace IDs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 09:16:13 +00:00
donteatfriedrice
bc67766bb9 fix(editor): cleanup transformer middleware slot subscriptions (#12630)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved resource management by introducing explicit cleanup for various middleware components, ensuring that resources are properly released when no longer needed.

- **Refactor**
  - Updated middleware logic to support cleanup functions, enhancing the stability and performance of the application by preventing potential memory leaks.

- **Chores**
  - Enhanced lifecycle management in core systems to automatically dispose of resources when appropriate.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 08:33:30 +00:00
L-Sun
9a96cfded0 fix(editor): viewportElement is undefined in edgeless root block (#12626)
This PR fixed that `rootComponent.viewportElement` is undefeined in edgeless mode, which leads that toast can not be render in playground.

388641bc89/blocksuite/affine/components/src/toast/create.ts (L23-L35)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Improved internal code organization for better maintainability. No changes to visible features or functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 08:10:54 +00:00
239 changed files with 4898 additions and 1966 deletions

View File

@@ -886,8 +886,8 @@
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable indexer plugin\n@default true\n@environment `AFFINE_INDEXER_ENABLED`",
"default": true
"description": "Enable indexer plugin\n@default false\n@environment `AFFINE_INDEXER_ENABLED`",
"default": false
},
"provider.type": {
"type": "string",

View File

@@ -113,6 +113,7 @@ jobs:
build-server-native:
name: Build Server native - ${{ matrix.targets.name }}
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
strategy:
fail-fast: false
matrix:

View File

@@ -20,6 +20,7 @@ env:
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
DEPLOYMENT_TYPE: affine
AFFINE_INDEXER_ENABLED: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -151,7 +152,8 @@ jobs:
- name: Clippy
run: |
rustup component add clippy
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
check-git-status:
name: Check Git Status
@@ -923,7 +925,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo nextest run --release --no-fail-fast
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
copilot-api-test:
name: Server Copilot Api Test

View File

@@ -117,31 +117,10 @@ jobs:
name: android
path: packages/frontend/apps/android/dist
determine-ios-runner:
runs-on: ubuntu-latest
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
needs:
- build-ios-web
outputs:
RUNNER: ${{ steps.runner.outputs.RUNNER }}
steps:
- name: Determine Runner
id: runner
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
# blaze/macos-14 is free but has limited concurrency
run: |
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
if [ $RANDOM_NUMBER -le 20 ]; then
echo "Selected namespace-profile-macos (20% probability)"
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
else
echo "Selected blaze/macos-14 (80% probability)"
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
fi
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
needs:
- determine-ios-runner
steps:
- uses: actions/checkout@v4
- name: Download mobile artifact

611
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,9 +47,9 @@ log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1"
nanoid = "0.4"
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.28" }
napi-derive = { version = "3.0.0-beta.3" }
nom = "8"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
@@ -77,12 +77,12 @@ smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
strum_macros = "0.27.0"
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
text-splitter = "0.25"
text-splitter = "0.27"
thiserror = "2"
tiktoken-rs = "0.6"
tokio = "1.37"
tiktoken-rs = "0.7"
tokio = "1.45"
tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.23" }
tree-sitter-c = { version = "0.24" }
tree-sitter-c-sharp = { version = "0.23" }
tree-sitter-cpp = { version = "0.23" }
tree-sitter-go = { version = "0.23" }

View File

@@ -17,6 +17,7 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
CitationProvider,
DocModeProvider,
FileSizeLimitProvider,
TelemetryProvider,
@@ -37,6 +38,7 @@ import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { filter } from 'rxjs/operators';
import { AttachmentEmbedProvider } from './embed';
import { styles } from './styles';
@@ -79,8 +81,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.std.get(FileSizeLimitProvider).maxFileSize;
}
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return !!this.model.props.footnoteIdentifier;
return this.citationService.isCitationModel(this.model);
}
convertTo = () => {
@@ -139,6 +145,34 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
selectionManager.setGroup('note', [blockSelection]);
}
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -162,6 +196,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
});
});
}
this._trackCitationDeleteEvent();
}
override firstUpdated() {

View File

@@ -1,6 +1,7 @@
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
import { toast } from '@blocksuite/affine-components/toast';
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
import { CitationProvider } from '@blocksuite/affine-shared/services';
import type { EditorHost } from '@blocksuite/std';
import { html } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
@@ -33,6 +34,7 @@ export const RenameModal = ({
let fileName = includeExtension ? nameWithoutExtension : originalName;
const extension = includeExtension ? originalExtension : '';
const citationService = editorHost.std.get(CitationProvider);
const abort = () => abortController.abort();
const onConfirm = () => {
@@ -44,6 +46,9 @@ export const RenameModal = ({
model.store.updateBlock(model, {
name: newFileName,
});
if (citationService.isCitationModel(model)) {
citationService.trackEvent('Edit');
}
abort();
};
const onInput = (e: InputEvent) => {

View File

@@ -8,6 +8,7 @@ import type {
} from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import {
CitationProvider,
DocModeProvider,
LinkPreviewServiceIdentifier,
} from '@blocksuite/affine-shared/services';
@@ -18,6 +19,7 @@ import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { filter } from 'rxjs/operators';
import { refreshBookmarkUrlData } from './utils.js';
@@ -114,11 +116,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
);
};
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
return this.citationService.isCitationModel(this.model);
}
get imageProxyService() {
@@ -166,6 +169,31 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
></bookmark-card>`;
};
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -203,6 +231,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
})
);
this._trackCitationDeleteEvent();
}
override disconnectedCallback(): void {

View File

@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let { style } = model.props;
let style: EmbedCardStyle = model.props.style;
if (!styles.includes(style)) {
style = styles[0];
@@ -482,24 +482,26 @@ const builtinSurfaceToolbarConfig = {
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'b.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => BookmarkStyles.includes(action.id)),
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null;

View File

@@ -40,6 +40,16 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
private _inlineRangeProvider: InlineRangeProvider | null = null;
private readonly _localPreview$ = signal<boolean | null>(null);
preview$: Signal<boolean> = computed(() => {
const modelPreview = !!this.model.props.preview$.value;
if (this.store.readonly) {
return this._localPreview$.value ?? modelPreview;
}
return modelPreview;
});
highlightTokens$: Signal<ThemedToken[][]> = signal([]);
languageName$: Signal<string> = computed(() => {
@@ -393,7 +403,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
true) &&
(this.model.props.lineNumber ?? true);
const preview = !!this.model.props.preview;
const preview = this.preview$.value;
const previewContext = this.std.getOptional(
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
);
@@ -461,6 +471,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
override accessor useCaptionEditor = true;
override accessor useZeroWidth = true;
setPreviewState(preview: boolean) {
if (this.store.readonly) {
this._localPreview$.value = preview;
} else {
this.store.updateBlock(this.model, { preview });
}
}
}
declare global {

View File

@@ -58,11 +58,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
`;
private readonly _toggle = (value: boolean) => {
if (this.blockComponent.store.readonly) return;
this.blockComponent.store.updateBlock(this.blockComponent.model, {
preview: value,
});
this.blockComponent.setPreviewState(value);
const std = this.blockComponent.std;
const mode = std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
@@ -77,7 +73,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
};
get preview() {
return !!this.blockComponent.model.props.preview$.value;
return this.blockComponent.preview$.value;
}
override render() {

View File

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

View File

@@ -17,6 +17,7 @@ import {
REFERENCE_NODE,
} from '@blocksuite/affine-shared/consts';
import {
CitationProvider,
DocDisplayMetaProvider,
DocModeProvider,
OpenDocExtensionIdentifier,
@@ -43,6 +44,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import throttle from 'lodash-es/throttle';
import { filter } from 'rxjs/operators';
import * as Y from 'yjs';
import { renderLinkedDocInCard } from '../common/render-linked-doc';
@@ -254,11 +256,12 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
return this.store.readonly;
}
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
return this.citationService.isCitationModel(this.model);
}
private readonly _handleDoubleClick = (event: MouseEvent) => {
@@ -454,6 +457,31 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
);
};
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -532,6 +560,8 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
}
})
);
this._trackCitationDeleteEvent();
}
getInitialState(): {

View File

@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
let { style } = model.props;
let style: EmbedCardStyle = model.props.style;
let flavour = 'affine:bookmark';
if (options?.viewType === 'card') {
@@ -227,7 +227,7 @@ function createBuiltinToolbarConfigForExternal(
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let { style } = model.props;
let style: EmbedCardStyle = model.props.style;
if (!styles.includes(style)) {
style =
@@ -441,7 +441,11 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
let { style } = model.props;
let flavour = 'affine:bookmark';
if (!BookmarkStyles.includes(style)) {
if (
!BookmarkStyles.includes(
style as (typeof BookmarkStyles)[number]
)
) {
style = BookmarkStyles[0];
}
@@ -517,26 +521,26 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'c.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action =>
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
),
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => EmbedGithubStyles.includes(action.id)),
when(ctx) {
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
},

View File

@@ -16,6 +16,7 @@ import {
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import {
@@ -87,6 +88,12 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
this.gfx.tool.currentToolName$.value === 'frameNavigator';
const frameIndex = this.gfx.layer.getZIndex(model);
const widgets = html`${repeat(
Object.entries(this.widgets),
([id]) => id,
([_, widget]) => widget
)}`;
return html`
<div
class="affine-frame-container"
@@ -102,6 +109,7 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
})}
></div>
${widgets}
`;
}
@@ -178,11 +186,22 @@ export const FrameBlockInteraction =
selectable(context) {
const { model } = context;
const onTitle =
model.externalBound?.containsPoint([
context.position.x,
context.position.y,
]) ?? false;
return (
context.default(context) &&
(model.isLocked() || !isTransparent(model.props.background))
(model.isLocked() ||
!isTransparent(model.props.background) ||
onTitle)
);
},
onSelect(context) {
return context.default(context);
},
};
},
}

View File

@@ -241,20 +241,35 @@ export class EdgelessFrameManager extends GfxExtension {
surfaceModel.elementAdded.subscribe(({ id, local }) => {
const element = surfaceModel.getElementById(id);
if (element && local) {
const frame = this.getFrameFromPoint(element.elementBound.center);
// if the container created with a frame, skip it.
if (
isGfxGroupCompatibleModel(element) &&
frame &&
element.hasChild(frame)
) {
return;
}
// new element may intended to be added to other group
// so we need to wait for the next microtask to check if the element can be added to the frame
// The entire frame detection logic must be in microtask for timing reasons:
//
// 1. For connectors: When elementAdded fires, connectors have invalid bounds [0,0,0,0]
// because their path/bounds are calculated in a separate microtask of updateConnectorPath by connector-watcher.
// We need to wait for that calculation to complete before frame detection.
//
// 2. For shapes: Although they have valid bounds immediately, processing them in microtask
// ensures consistent timing and allows other initialization to complete first.
//
// 3. Group compatibility: Some elements may need to establish their group relationships
// before being considered for frame membership.
//
// By embedding the entire logic in microtask, we ensure:
// - Connectors have proper bounds calculated (not [0,0,0,0])
// - getFrameFromPoint() works correctly with valid element centers
// - All element initialization is complete before frame detection
queueMicrotask(() => {
const frame = this.getFrameFromPoint(element.elementBound.center);
// if the container created with a frame, skip it.
if (
isGfxGroupCompatibleModel(element) &&
frame &&
element.hasChild(frame)
) {
return;
}
// Only add elements that aren't already grouped and have a valid frame
if (!element.group && frame) {
this.addElementsToFrame(frame, [element]);
}

View File

@@ -7,7 +7,10 @@ import {
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
CitationProvider,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
import {
calculateCollapsedSiblings,
getNearestHeadingBefore,
@@ -63,6 +66,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
?.getPlaceholder(this.model);
}
get citationService() {
return this.std.get(CitationProvider);
}
get attributeRenderer() {
return this.inlineManager.getRenderer();
}
@@ -94,6 +101,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
return this.std.get(DefaultInlineManagerExtension.identifier);
}
get hasCitationSiblings() {
return this.collapsedSiblings.some(sibling =>
this.citationService.isCitationModel(sibling)
);
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
@@ -286,6 +299,13 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
collapsed: value,
});
}
if (this.hasCitationSiblings) {
this.citationService.trackEvent('Expand', {
control: 'Source Button',
type: value ? 'Hide' : 'Show',
});
}
}}
></blocksuite-toggle-button>
`

View File

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

View File

@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
) as SurfaceBlockModel;
}
private get _viewportElement(): HTMLElement {
get viewportElement(): HTMLElement {
return this.std.get(ViewportElementProvider).viewportElement;
}
@@ -267,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
this.gfx.viewport.onResize();
});
resizeObserver.observe(this._viewportElement);
resizeObserver.observe(this.viewportElement);
this._resizeObserver = resizeObserver;
}

View File

@@ -43,6 +43,25 @@ type RendererOptions = {
surfaceModel: SurfaceBlockModel;
};
const UpdateType = {
ELEMENT_ADDED: 'element-added',
ELEMENT_REMOVED: 'element-removed',
ELEMENT_UPDATED: 'element-updated',
VIEWPORT_CHANGED: 'viewport-changed',
SIZE_CHANGED: 'size-changed',
ZOOM_STATE_CHANGED: 'zoom-state-changed',
} as const;
type UpdateType = (typeof UpdateType)[keyof typeof UpdateType];
interface IncrementalUpdateState {
dirtyElementIds: Set<string>;
viewportDirty: boolean;
sizeDirty: boolean;
usePlaceholderDirty: boolean;
pendingUpdates: Map<string, UpdateType[]>;
}
const PLACEHOLDER_RESET_STYLES = {
border: 'none',
borderRadius: '0',
@@ -141,6 +160,18 @@ export class DomRenderer {
private _sizeUpdatedRafId: number | null = null;
private readonly _updateState: IncrementalUpdateState = {
dirtyElementIds: new Set(),
viewportDirty: false,
sizeDirty: false,
usePlaceholderDirty: false,
pendingUpdates: new Map(),
};
private _lastViewportBounds: Bound | null = null;
private _lastZoom: number | null = null;
private _lastUsePlaceholder: boolean = false;
rootElement: HTMLElement;
private readonly _elementsMap = new Map<string, HTMLElement>();
@@ -186,6 +217,7 @@ export class DomRenderer {
private _initViewport() {
this._disposables.add(
this.viewport.viewportUpdated.subscribe(() => {
this._markViewportDirty();
this.refresh();
})
);
@@ -195,6 +227,7 @@ export class DomRenderer {
if (this._sizeUpdatedRafId) return;
this._sizeUpdatedRafId = requestConnectedFrame(() => {
this._sizeUpdatedRafId = null;
this._markSizeDirty();
this._resetSize();
this._render();
this.refresh();
@@ -208,6 +241,7 @@ export class DomRenderer {
if (this.usePlaceholder !== shouldRenderPlaceholders) {
this.usePlaceholder = shouldRenderPlaceholders;
this._markUsePlaceholderDirty();
this.refresh();
}
})
@@ -307,6 +341,292 @@ export class DomRenderer {
}
private _render() {
this._renderIncremental();
}
private _watchSurface(surfaceModel: SurfaceBlockModel) {
this._disposables.add(
surfaceModel.elementAdded.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
this.refresh();
})
);
this._disposables.add(
surfaceModel.elementRemoved.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
this.refresh();
})
);
this._disposables.add(
surfaceModel.localElementAdded.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
this.refresh();
})
);
this._disposables.add(
surfaceModel.localElementDeleted.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
this.refresh();
})
);
this._disposables.add(
surfaceModel.localElementUpdated.subscribe(payload => {
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
this.refresh();
})
);
this._disposables.add(
surfaceModel.elementUpdated.subscribe(payload => {
// ignore externalXYWH update cause it's updated by the renderer
if (payload.props['externalXYWH']) return;
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
this.refresh();
})
);
}
addOverlay = (overlay: Overlay) => {
overlay.setRenderer(null);
this._overlays.add(overlay);
this.refresh();
};
attach = (container: HTMLElement) => {
this._container = container;
container.append(this.rootElement);
this._resetSize();
this.refresh();
};
dispose = () => {
this._overlays.forEach(overlay => overlay.dispose());
this._overlays.clear();
this._disposables.dispose();
if (this._refreshRafId) {
cancelAnimationFrame(this._refreshRafId);
this._refreshRafId = null;
}
if (this._sizeUpdatedRafId) {
cancelAnimationFrame(this._sizeUpdatedRafId);
this._sizeUpdatedRafId = null;
}
this.rootElement.remove();
this._elementsMap.clear();
};
generateColorProperty = (color: Color, fallback?: Color) => {
return (
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
);
};
getColorScheme = () => {
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
};
getColorValue = (color: Color, fallback?: Color, real?: boolean) => {
return (
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
);
};
getPropertyValue = (property: string) => {
return this.provider.getPropertyValue?.(property) ?? '';
};
refresh = () => {
if (this._refreshRafId !== null) return;
this._refreshRafId = requestConnectedFrame(() => {
this._refreshRafId = null;
this._render();
}, this._container);
};
removeOverlay = (overlay: Overlay) => {
if (!this._overlays.has(overlay)) {
return;
}
this._overlays.delete(overlay);
this.refresh();
};
/**
* Mark a specific element as dirty for incremental updates
* @param elementId - The ID of the element to mark as dirty
* @param updateType - The type of update (optional, defaults to ELEMENT_UPDATED)
*/
markElementDirty = (
elementId: string,
updateType: UpdateType = UpdateType.ELEMENT_UPDATED
) => {
this._markElementDirty(elementId, updateType);
};
/**
* Force a full re-render of all elements
*/
forceFullRender = () => {
this._updateState.viewportDirty = true;
this.refresh();
};
private _markElementDirty(elementId: string, updateType: UpdateType) {
this._updateState.dirtyElementIds.add(elementId);
const currentUpdates =
this._updateState.pendingUpdates.get(elementId) || [];
if (!currentUpdates.includes(updateType)) {
currentUpdates.push(updateType);
this._updateState.pendingUpdates.set(elementId, currentUpdates);
}
}
private _markViewportDirty() {
this._updateState.viewportDirty = true;
}
private _markSizeDirty() {
this._updateState.sizeDirty = true;
}
private _markUsePlaceholderDirty() {
this._updateState.usePlaceholderDirty = true;
}
private _clearUpdateState() {
this._updateState.dirtyElementIds.clear();
this._updateState.viewportDirty = false;
this._updateState.sizeDirty = false;
this._updateState.usePlaceholderDirty = false;
this._updateState.pendingUpdates.clear();
}
private _isViewportChanged(): boolean {
const { viewportBounds, zoom } = this.viewport;
if (!this._lastViewportBounds || !this._lastZoom) {
return true;
}
return (
this._lastViewportBounds.x !== viewportBounds.x ||
this._lastViewportBounds.y !== viewportBounds.y ||
this._lastViewportBounds.w !== viewportBounds.w ||
this._lastViewportBounds.h !== viewportBounds.h ||
this._lastZoom !== zoom
);
}
private _isUsePlaceholderChanged(): boolean {
return this._lastUsePlaceholder !== this.usePlaceholder;
}
private _updateLastState() {
const { viewportBounds, zoom } = this.viewport;
this._lastViewportBounds = {
x: viewportBounds.x,
y: viewportBounds.y,
w: viewportBounds.w,
h: viewportBounds.h,
} as Bound;
this._lastZoom = zoom;
this._lastUsePlaceholder = this.usePlaceholder;
}
private _renderIncremental() {
const { viewportBounds, zoom } = this.viewport;
const addedElements: HTMLElement[] = [];
const elementsToRemove: HTMLElement[] = [];
const needsFullRender =
this._isViewportChanged() ||
this._isUsePlaceholderChanged() ||
this._updateState.sizeDirty ||
this._updateState.viewportDirty ||
this._updateState.usePlaceholderDirty;
if (needsFullRender) {
this._renderFull();
this._updateLastState();
this._clearUpdateState();
return;
}
// Only update dirty elements
const elementsFromGrid = this.grid.search(viewportBounds, {
filter: ['canvas', 'local'],
}) as SurfaceElementModel[];
const visibleElementIds = new Set<string>();
// 1. Update dirty elements
for (const elementModel of elementsFromGrid) {
const display = (elementModel.display ?? true) && !elementModel.hidden;
if (
display &&
intersects(getBoundWithRotation(elementModel), viewportBounds)
) {
visibleElementIds.add(elementModel.id);
// Only update dirty elements
if (this._updateState.dirtyElementIds.has(elementModel.id)) {
if (
this.usePlaceholder &&
!(elementModel as GfxCompatibleInterface).forceFullRender
) {
this._renderOrUpdatePlaceholder(
elementModel,
viewportBounds,
zoom,
addedElements
);
} else {
this._renderOrUpdateFullElement(
elementModel,
viewportBounds,
zoom,
addedElements
);
}
}
}
}
// 2. Remove elements that are no longer in the grid
for (const elementId of this._updateState.dirtyElementIds) {
const updateTypes = this._updateState.pendingUpdates.get(elementId) || [];
if (
updateTypes.includes(UpdateType.ELEMENT_REMOVED) ||
!visibleElementIds.has(elementId)
) {
const domElem = this._elementsMap.get(elementId);
if (domElem) {
domElem.remove();
this._elementsMap.delete(elementId);
elementsToRemove.push(domElem);
}
}
}
// 3. Notify changes
if (addedElements.length > 0 || elementsToRemove.length > 0) {
this.elementsUpdated.next({
elements: Array.from(this._elementsMap.values()),
added: addedElements,
removed: elementsToRemove,
});
}
this._updateLastState();
this._clearUpdateState();
}
private _renderFull() {
const { viewportBounds, zoom } = this.viewport;
const addedElements: HTMLElement[] = [];
const elementsToRemove: HTMLElement[] = [];
@@ -387,100 +707,4 @@ export class DomRenderer {
});
}
}
private _watchSurface(surfaceModel: SurfaceBlockModel) {
this._disposables.add(
surfaceModel.elementAdded.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.elementRemoved.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.localElementAdded.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.elementUpdated.subscribe(payload => {
// ignore externalXYWH update cause it's updated by the renderer
if (payload.props['externalXYWH']) return;
this.refresh();
})
);
}
addOverlay(overlay: Overlay) {
overlay.setRenderer(null);
this._overlays.add(overlay);
this.refresh();
}
attach(container: HTMLElement) {
this._container = container;
container.append(this.rootElement);
this._resetSize();
this.refresh();
}
dispose(): void {
this._overlays.forEach(overlay => overlay.dispose());
this._overlays.clear();
this._disposables.dispose();
if (this._refreshRafId) {
cancelAnimationFrame(this._refreshRafId);
this._refreshRafId = null;
}
if (this._sizeUpdatedRafId) {
cancelAnimationFrame(this._sizeUpdatedRafId);
this._sizeUpdatedRafId = null;
}
this.rootElement.remove();
this._elementsMap.clear();
}
generateColorProperty(color: Color, fallback?: Color) {
return (
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
);
}
getColorScheme() {
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
}
getColorValue(color: Color, fallback?: Color, real?: boolean) {
return (
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
);
}
getPropertyValue(property: string) {
return this.provider.getPropertyValue?.(property) ?? '';
}
refresh() {
if (this._refreshRafId !== null) return;
this._refreshRafId = requestConnectedFrame(() => {
this._refreshRafId = null;
this._render();
}, this._container);
}
removeOverlay(overlay: Overlay) {
if (!this._overlays.has(overlay)) {
return;
}
this._overlays.delete(overlay);
this.refresh();
}
}

View File

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

View File

@@ -9,6 +9,7 @@ const toolbarColorKeys: Array<keyof AffineCssVariables> = [
'--affine-background-overlay-panel-color',
'--affine-v2-layer-background-overlayPanel' as never,
'--affine-v2-layer-insideBorder-blackBorder' as never,
'--affine-v2-icon-primary' as never,
'--affine-background-error-color',
'--affine-background-primary-color',
'--affine-background-tertiary-color',

View File

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

View File

@@ -16,6 +16,7 @@ import { property } from 'lit/decorators.js';
import type { Variable } from '../../../core/expression/types.js';
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
import { popCreateFilter } from '../../../core/index.js';
import type { DataViewUILogicBase } from '../../../core/view/data-view-base.js';
import { popFilterGroup } from './group-panel-view.js';
export class FilterBar extends SignalWatcher(ShadowlessElement) {
@@ -99,6 +100,7 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
requestAnimationFrame(() => {
this.expandGroup(element, index);
});
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
});
};
@@ -206,6 +208,9 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
@property({ attribute: false })
accessor vars!: ReadonlySignal<Variable[]>;
@property({ attribute: false })
accessor dataViewLogic!: DataViewUILogicBase;
}
declare global {

View File

@@ -26,7 +26,10 @@ import { repeat } from 'lit/directives/repeat.js';
import type { Variable } from '../../../core/expression/types.js';
import type { FilterTrait } from '../../../core/filter/trait.js';
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
import { popCreateFilter } from '../../../core/index.js';
import {
type DataViewUILogicBase,
popCreateFilter,
} from '../../../core/index.js';
import {
type FilterGroupView,
getDepth,
@@ -375,6 +378,7 @@ export const popFilterRoot = (
props: {
filterTrait: FilterTrait;
onBack: () => void;
dataViewLogic: DataViewUILogicBase;
}
) => {
const filterTrait = props.filterTrait;
@@ -414,6 +418,10 @@ export const popFilterRoot = (
...value,
conditions: [...value.conditions, filter],
});
props.dataViewLogic.eventTrace(
'CreateDatabaseFilter',
{}
);
},
},
{ middleware: subMenuMiddleware }

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import {
} from '@blocksuite/affine-ext-loader';
import {
AutoClearSelectionService,
CitationService,
DefaultOpenDocExtension,
DNDAPIExtension,
DocDisplayMetaService,
@@ -76,6 +77,7 @@ export class FoundationViewExtension extends ViewExtensionProvider<FoundationVie
FileSizeLimitService,
LinkPreviewCache,
LinkPreviewService,
CitationService,
]);
context.register(clipboardConfigs);
if (this.isEdgeless(context.scope)) {

View File

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

View File

@@ -1,6 +1,6 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import { MindmapElementModel } from '@blocksuite/affine-model';
import { Bound } from '@blocksuite/global/gfx';
import { type Bound } from '@blocksuite/global/gfx';
import {
type DragExtensionInitializeContext,
type ExtensionDragMoveContext,
@@ -74,47 +74,63 @@ export class SnapExtension extends InteractivityExtension {
return {};
}
let alignBound: Bound | null = null;
return {
onResizeStart(context) {
alignBound = snapOverlay.setMovingElements(context.elements);
snapOverlay.setMovingElements(context.elements);
},
onResizeMove(context) {
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
return;
const {
handle,
originalBound,
scaleX,
scaleY,
handleSign,
currentHandlePos,
elements,
} = context;
const rotate = elements.length > 1 ? 0 : elements[0].rotate;
const alignDirection: ('vertical' | 'horizontal')[] = [];
let switchDirection = false;
let nx = handleSign.x;
let ny = handleSign.y;
if (handle.length > 6) {
alignDirection.push('vertical', 'horizontal');
} else if (rotate % 90 === 0) {
nx =
handleSign.x * Math.cos((rotate / 180) * Math.PI) -
handleSign.y * Math.sin((rotate / 180) * Math.PI);
ny =
handleSign.x * Math.sin((rotate / 180) * Math.PI) +
handleSign.y * Math.cos((rotate / 180) * Math.PI);
if (Math.abs(nx) > Math.abs(ny)) {
alignDirection.push('horizontal');
} else {
alignDirection.push('vertical');
}
if (rotate % 180 !== 0) {
switchDirection = true;
}
}
const { handle, handleSign, lockRatio } = context;
let { dx, dy } = context;
if (lockRatio) {
const min = Math.min(
Math.abs(dx / alignBound.w),
Math.abs(dy / alignBound.h)
if (alignDirection.length > 0) {
const rst = snapOverlay.alignResize(
currentHandlePos,
alignDirection
);
dx = min * Math.sign(dx) * alignBound.w;
dy = min * Math.sign(dy) * alignBound.h;
const dx = switchDirection ? ny * rst.dy : nx * rst.dx;
const dy = switchDirection ? nx * rst.dx : ny * rst.dy;
context.suggest({
scaleX: scaleX + dx / originalBound.w,
scaleY: scaleY + dy / originalBound.h,
});
}
const currentBound = new Bound(
alignBound.x +
(handle.includes('left') ? -dx * handleSign.xSign : 0),
alignBound.y +
(handle.includes('top') ? -dy * handleSign.ySign : 0),
Math.abs(alignBound.w + dx * handleSign.xSign),
Math.abs(alignBound.h + dy * handleSign.ySign)
);
const alignRst = snapOverlay.align(currentBound);
context.suggest({
dx: alignRst.dx + context.dx,
dy: alignRst.dy + context.dy,
});
},
onResizeEnd() {
alignBound = null;
snapOverlay.clear();
},
};

View File

@@ -3,7 +3,7 @@ import {
ConnectorElementModel,
MindmapElementModel,
} from '@blocksuite/affine-model';
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
import type { GfxModel } from '@blocksuite/std/gfx';
interface Distance {
@@ -586,6 +586,60 @@ export class SnapOverlay extends Overlay {
);
}
alignResize(position: IVec, direction: ('vertical' | 'horizontal')[]) {
const rst = { dx: 0, dy: 0 };
const { viewport } = this.gfx;
const threshold = ALIGN_THRESHOLD / viewport.zoom;
const searchBound = new Bound(
position[0] - threshold / 2,
position[1] - threshold / 2,
threshold,
threshold
);
const alignBound = new Bound(position[0], position[1], 0, 0);
this._intraGraphicAlignLines = {
horizontal: [],
vertical: [],
};
this._distributedAlignLines = [];
this._updateAlignCandidates(searchBound);
for (const other of this._referenceBounds.all) {
const closestDistances = this._calculateClosestDistances(
alignBound,
other
);
if (
direction.includes('horizontal') &&
closestDistances.horiz &&
(!this._intraGraphicAlignLines.horizontal.length ||
Math.abs(closestDistances.horiz.distance) < Math.abs(rst.dx))
) {
this._updateXAlignPoint(rst, alignBound, other, closestDistances);
}
if (
direction.includes('vertical') &&
closestDistances.vert &&
(!this._intraGraphicAlignLines.vertical.length ||
Math.abs(closestDistances.vert.distance) < Math.abs(rst.dy))
) {
this._updateYAlignPoint(rst, alignBound, other, closestDistances);
}
}
this._intraGraphicAlignLines.horizontal =
this._intraGraphicAlignLines.horizontal.slice(0, 1);
this._intraGraphicAlignLines.vertical =
this._intraGraphicAlignLines.vertical.slice(0, 1);
this._renderer?.refresh();
return rst;
}
align(bound: Bound): { dx: number; dy: number } {
const rst = { dx: 0, dy: 0 };
const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom;

View File

@@ -9,18 +9,35 @@ function applyShapeSpecificStyles(
element: HTMLElement,
zoom: number
) {
if (model.shapeType === 'rect') {
const w = model.w * zoom;
const h = model.h * zoom;
const r = model.radius ?? 0;
const borderRadius =
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
element.style.borderRadius = borderRadius;
} else if (model.shapeType === 'ellipse') {
element.style.borderRadius = '50%';
} else {
element.style.borderRadius = '';
// Reset properties that might be set by different shape types
element.style.removeProperty('clip-path');
element.style.removeProperty('border-radius');
// Clear DOM for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based
if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') {
while (element.firstChild) element.firstChild.remove();
}
switch (model.shapeType) {
case 'rect': {
const w = model.w * zoom;
const h = model.h * zoom;
const r = model.radius ?? 0;
const borderRadius =
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
element.style.borderRadius = borderRadius;
break;
}
case 'ellipse':
element.style.borderRadius = '50%';
break;
case 'diamond':
element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
break;
case 'triangle':
element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)';
break;
}
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
}
function applyBorderStyles(
@@ -78,6 +95,9 @@ export const shapeDomRenderer = (
renderer: DomRenderer
): void => {
const { zoom } = renderer.viewport;
const unscaledWidth = model.w;
const unscaledHeight = model.h;
const fillColor = renderer.getColorValue(
model.fillColor,
DefaultTheme.shapeFillColor,
@@ -89,17 +109,80 @@ export const shapeDomRenderer = (
true
);
element.style.width = `${model.w * zoom}px`;
element.style.height = `${model.h * zoom}px`;
element.style.width = `${unscaledWidth * zoom}px`;
element.style.height = `${unscaledHeight * zoom}px`;
element.style.boxSizing = 'border-box';
// Apply shape-specific clipping, border-radius, and potentially clear innerHTML
applyShapeSpecificStyles(model, element, zoom);
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
if (model.shapeType === 'diamond' || model.shapeType === 'triangle') {
// For diamond and triangle, fill and border are handled by inline SVG
element.style.border = 'none'; // Ensure no standard CSS border interferes
element.style.backgroundColor = 'transparent'; // Host element is transparent
const strokeW = model.strokeWidth;
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
let svgPoints = '';
if (model.shapeType === 'diamond') {
// Adjusted points for diamond
svgPoints = [
`${unscaledWidth / 2},${halfStroke}`,
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
`${halfStroke},${unscaledHeight / 2}`,
].join(' ');
} else {
// triangle
// Adjusted points for triangle
svgPoints = [
`${unscaledWidth / 2},${halfStroke}`,
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
`${halfStroke},${unscaledHeight - halfStroke}`,
].join(' ');
}
// Determine if stroke should be visible and its color
const finalStrokeColor =
model.strokeStyle !== 'none' && strokeW > 0 ? strokeColor : 'transparent';
// Determine dash array, only if stroke is visible and style is 'dash'
const finalStrokeDasharray =
model.strokeStyle === 'dash' && finalStrokeColor !== 'transparent'
? '12, 12'
: 'none';
// Determine fill color
const finalFillColor = model.filled ? fillColor : 'transparent';
// Build SVG safely with DOM-API
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
svg.setAttribute('preserveAspectRatio', 'none');
const polygon = document.createElementNS(SVG_NS, 'polygon');
polygon.setAttribute('points', svgPoints);
polygon.setAttribute('fill', finalFillColor);
polygon.setAttribute('stroke', finalStrokeColor);
polygon.setAttribute('stroke-width', String(strokeW));
if (finalStrokeDasharray !== 'none') {
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
}
svg.append(polygon);
// Replace existing children to avoid memory leaks
element.replaceChildren(svg);
} else {
// Standard rendering for other shapes (e.g., rect, ellipse)
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
}
applyBorderStyles(model, element, strokeColor, zoom);
applyTransformStyles(model, element);
element.style.boxSizing = 'border-box';
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
manageClassNames(model, element);

View File

@@ -1,6 +1,7 @@
import { HoverController } from '@blocksuite/affine-components/hover';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import type { FootNote } from '@blocksuite/affine-model';
import { CitationProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { WithDisposable } from '@blocksuite/global/lit';
@@ -117,6 +118,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
return this.std.store.readonly;
}
get citationService() {
return this.std.get(CitationProvider);
}
onFootnoteClick = () => {
if (!this.footnote) {
return;
@@ -215,6 +220,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
return null;
}
this.citationService.trackEvent('Hover', {
control: 'Source Footnote',
});
return {
template: this._FootNotePopup(footnote, abortController),
container: this.std.host,

View File

@@ -188,6 +188,8 @@ export class AffineLatexNode extends SignalWatcher(
this._editorAbortController?.abort();
this._editorAbortController = new AbortController();
blockComponent.selection.setGroup('note', []);
const portal = createLitPortal({
template: html`<latex-editor-menu
.std=${this.std}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { BlockModel } from '@blocksuite/store';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
export const EmbedHtmlStyles = ['html'] as const satisfies EmbedCardStyle[];
export type EmbedHtmlBlockProps = {
style: (typeof EmbedHtmlStyles)[number];

View File

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

View File

@@ -4,17 +4,17 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
export const EmbedLinkedDocStyles = [
'vertical',
'horizontal',
'list',
'cube',
'horizontalThin',
'citation',
];
] as const satisfies EmbedCardStyle[];
export type EmbedLinkedDocBlockProps = {
style: EmbedCardStyle;
style: (typeof EmbedLinkedDocStyles)[number];
caption: string | null;
footnoteIdentifier: string | null;
} & ReferenceInfo;

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import type { GfxModel } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { BookmarkBlockModel } from '../bookmark';
import { EmbedFigmaModel } from './figma';
import { EmbedGithubModel } from './github';
import type { EmbedHtmlModel } from './html';
@@ -30,7 +31,10 @@ export type EmbedCardModel = InstanceType<
ExternalEmbedModel | InternalEmbedModel
>;
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
export type LinkableEmbedModel =
| EmbedCardModel
| EmbedIframeBlockModel
| BookmarkBlockModel;
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;

View File

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

View File

@@ -80,4 +80,8 @@ describe('isValidUrl: determining whether a URL is valid is very complicated', (
// See also https://stackoverflow.com/questions/9238640/how-long-can-a-tld-possibly-be#:~:text=Longest%20TLD%20up%20to%20date,17%20when%20decoded%20%5Bverm%C3%B6gensberatung%5D.
expect(isValidUrl('example.xn--vermgensberatung-pwb')).toEqual(false);
});
test('should allow ip address url when origin is same', () => {
expect(isValidUrl('http://127.0.0.1', 'http://127.0.0.1')).toEqual(true);
});
});

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
map(({ model }) => model)
);
afterImportBlock$
const afterImportBlockSubscription = afterImportBlock$
.pipe(filter(model => matchModels(model, [DatabaseBlockModel])))
.subscribe(model => {
Object.keys(model.props.cells).forEach(cellId => {
@@ -44,7 +44,7 @@ export const replaceIdMiddleware =
});
// replace LinkedPage pageId with new id in paragraph blocks
afterImportBlock$
const replaceLinkedPageIdSubscription = afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [ParagraphBlockModel, ListBlockModel])
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
}
});
afterImportBlock$
const replaceSurfaceRefIdSubscription = afterImportBlock$
.pipe(filter(model => matchModels(model, [SurfaceRefBlockModel])))
.subscribe(model => {
const original = model.props.reference;
@@ -105,7 +105,7 @@ export const replaceIdMiddleware =
});
// TODO(@fundon): process linked block/element
afterImportBlock$
const replaceLinkedDocIdSubscription = afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
// Before Import
slots.beforeImport
const beforeImportPageSubscription = slots.beforeImport
.pipe(filter(payload => payload.type === 'page'))
.subscribe(payload => {
if (idMap.has(payload.snapshot.meta.id)) {
@@ -140,7 +140,7 @@ export const replaceIdMiddleware =
payload.snapshot.meta.id = newId;
});
slots.beforeImport
const beforeImportBlockSubscription = slots.beforeImport
.pipe(
filter(
(payload): payload is BeforeImportBlockPayload =>
@@ -244,4 +244,13 @@ export const replaceIdMiddleware =
});
}
});
return () => {
afterImportBlockSubscription.unsubscribe();
replaceLinkedPageIdSubscription.unsubscribe();
replaceSurfaceRefIdSubscription.unsubscribe();
replaceLinkedDocIdSubscription.unsubscribe();
beforeImportPageSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
};

View File

@@ -5,33 +5,42 @@ export const surfaceRefToEmbed =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
let pageId: string | null = null;
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
}
}
});
slots.beforeImport.subscribe(payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
);
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
const isNotSameDoc = pageId !== std.store.doc.id;
if (pageId && isNotSameDoc) {
// The blockId of the original surface-ref block
const blockId = payload.snapshot.id;
payload.snapshot.id = std.workspace.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
pageId,
params: {
mode: 'page',
blockIds: [blockId],
},
};
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
const isNotSameDoc = pageId !== std.store.doc.id;
if (pageId && isNotSameDoc) {
// The blockId of the original surface-ref block
const blockId = payload.snapshot.id;
payload.snapshot.id = std.workspace.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
pageId,
params: {
mode: 'page',
blockIds: [blockId],
},
};
}
}
});
);
return () => {
beforeImportSliceSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
};

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/std';
import { type BlockModel, Extension } from '@blocksuite/store';
import { DocModeProvider } from '../doc-mode-service';
import type {
CitationEvents,
CitationEventType,
} from '../telemetry-service/citation';
import { TelemetryProvider } from '../telemetry-service/telemetry-service';
const CitationEventTypeMap = {
Hover: 'AICitationHoverSource',
Expand: 'AICitationExpandSource',
Delete: 'AICitationDelete',
Edit: 'AICitationEdit',
} as const;
type EventType = keyof typeof CitationEventTypeMap;
type EventTypeMapping = {
[K in EventType]: CitationEventType;
};
export interface CitationViewService {
/**
* Tracks citation-related events
* @param type - The type of citation event to track
* @param properties - The properties of the event
*/
trackEvent<T extends EventType>(
type: T,
properties?: CitationEvents[EventTypeMapping[T]]
): void;
/**
* Checks if the model is a citation model
* @param model - The model to check
* @returns True if the model is a citation model, false otherwise
*/
isCitationModel(model: BlockModel): boolean;
}
export const CitationProvider =
createIdentifier<CitationViewService>('CitationService');
export class CitationService extends Extension implements CitationViewService {
constructor(private readonly std: BlockStdScope) {
super();
}
static override setup(di: Container) {
di.addImpl(CitationProvider, CitationService, [StdIdentifier]);
}
get docModeService() {
return this.std.getOptional(DocModeProvider);
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
isCitationModel = (model: BlockModel) => {
return (
'footnoteIdentifier' in model.props &&
!!model.props.footnoteIdentifier &&
'style' in model.props &&
model.props.style === 'citation'
);
};
trackEvent<T extends EventType>(
type: T,
properties?: CitationEvents[EventTypeMapping[T]]
) {
const editorMode = this.docModeService?.getEditorMode() ?? 'page';
this.telemetryService?.track(CitationEventTypeMap[type], {
page: editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
module: 'AI Result',
control: 'Source',
...properties,
});
}
}

View File

@@ -0,0 +1 @@
export * from './citation-service';

View File

@@ -1,5 +1,6 @@
export * from './auto-clear-selection-service';
export * from './block-meta-service';
export * from './citation-service';
export * from './doc-display-meta-service';
export * from './doc-mode-service';
export * from './drag-handle-config';

View File

@@ -0,0 +1,8 @@
import type { TelemetryEvent } from './types';
export type CitationEventType =
| 'AICitationHoverSource'
| 'AICitationExpandSource'
| 'AICitationDelete'
| 'AICitationEdit';
export type CitationEvents = Record<CitationEventType, TelemetryEvent>;

View File

@@ -1,3 +1,4 @@
export * from './citation.js';
export * from './database.js';
export * from './link.js';
export * from './telemetry-service.js';

View File

@@ -1,6 +1,7 @@
import { createIdentifier } from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import type { CitationEvents } from './citation.js';
import type { CodeBlockEvents } from './code-block.js';
import type { OutDatabaseAllEvents } from './database.js';
import type { LinkToolbarEvents } from './link.js';
@@ -28,7 +29,8 @@ export type TelemetryEventMap = OutDatabaseAllEvents &
LinkToolbarEvents &
SlashMenuEvents &
CodeBlockEvents &
NoteEvents & {
NoteEvents &
CitationEvents & {
DocCreated: DocCreatedEvent;
Link: TelemetryEvent;
LinkedDocCreated: LinkedDocCreatedEvent;

View File

@@ -11,6 +11,9 @@ const ALLOWED_SCHEMES = new Set([
// https://publicsuffix.org/
const TLD_REGEXP = /(?:\.[a-zA-Z]+)?(\.[a-zA-Z]{2,})$/;
const IPV4_ADDR_REGEXP =
/^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
const toURL = (str: string) => {
try {
if (!URL.canParse(str)) return null;
@@ -21,16 +24,20 @@ const toURL = (str: string) => {
}
};
function resolveURL(str: string) {
function resolveURL(str: string, baseUrl: string, padded = false) {
const url = toURL(str);
if (!url) return null;
const protocol = url.protocol.substring(0, url.protocol.length - 1);
const hostname = url.hostname;
const origin = url.origin;
let allowed = ALLOWED_SCHEMES.has(protocol);
if (allowed && hostname.includes('.')) {
allowed = TLD_REGEXP.test(hostname);
allowed =
origin === baseUrl ||
TLD_REGEXP.test(hostname) ||
(padded ? false : IPV4_ADDR_REGEXP.test(hostname));
}
return { url, allowed };
@@ -68,10 +75,10 @@ export function normalizeUrl(str: string) {
*
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
*/
export function isValidUrl(str: string) {
export function isValidUrl(str: string, baseUrl = location.origin) {
str = str.trim();
let result = resolveURL(str);
let result = resolveURL(str, baseUrl);
if (result && !result.allowed) return false;
@@ -80,7 +87,7 @@ export function isValidUrl(str: string) {
if (!hasScheme) {
const dotIdx = str.indexOf('.');
if (dotIdx > 0 && dotIdx < str.length - 1) {
result = resolveURL(`https://${str}`);
result = resolveURL(`https://${str}`, baseUrl, true);
}
}
}

View File

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

View File

@@ -0,0 +1,56 @@
import {
AttachmentBlockModel,
BookmarkBlockModel,
EmbedGithubModel,
EmbedLinkedDocModel,
NoteBlockModel,
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { TransformerMiddleware } from '@blocksuite/store';
export const cardStyleUpdater =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
slots.beforeImport.subscribe(payload => {
if (payload.type !== 'block' || !payload.parent) return;
const parentModel = std.store.getModelById(payload.parent);
if (!matchModels(parentModel, [NoteBlockModel])) return;
// TODO(@L-Sun): Refactor this after refactor `store.moveBlocks`
// Currently, drag a block will use store.moveBlocks to update the tree of blocks
// but the instance of it is not changed.
// So change the style of snapshot.props in the middleware is not working.
// Instead, we can change the style of the model instance in the middleware,
const model = std.store.getModelById(payload.snapshot.id);
if (!model) return;
if (model instanceof AttachmentBlockModel) {
std.store.updateBlock(model, {
style: 'horizontalThin',
});
return;
}
if (model instanceof BookmarkBlockModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
if (model instanceof EmbedGithubModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
if (model instanceof EmbedLinkedDocModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
});
};

View File

@@ -9,36 +9,45 @@ export const newIdCrossDoc =
let samePage = false;
const oldToNewIdMap = new Map<string, string>();
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.store.id;
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.store.id;
}
if (payload.type === 'block' && !samePage) {
const newId = std.workspace.idGenerator();
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
}
}
if (payload.type === 'block' && !samePage) {
const newId = std.workspace.idGenerator();
);
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
const afterImportBlockSubscription = slots.afterImport.subscribe(
payload => {
if (
!samePage &&
payload.type === 'block' &&
matchModels(payload.model, [DatabaseBlockModel])
) {
const originalCells = payload.model.props.cells;
const newCells = {
...originalCells,
};
Object.keys(originalCells).forEach(cellId => {
if (oldToNewIdMap.has(cellId)) {
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
}
});
payload.model.props.cells$.value = newCells;
}
}
});
);
slots.afterImport.subscribe(payload => {
if (
!samePage &&
payload.type === 'block' &&
matchModels(payload.model, [DatabaseBlockModel])
) {
const originalCells = payload.model.props.cells;
const newCells = {
...originalCells,
};
Object.keys(originalCells).forEach(cellId => {
if (oldToNewIdMap.has(cellId)) {
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
}
});
payload.model.props.cells$.value = newCells;
}
});
return () => {
beforeImportSliceSubscription.unsubscribe();
afterImportBlockSubscription.unsubscribe();
};
};

View File

@@ -7,19 +7,25 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const reorderList =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
slots.afterImport.subscribe(payload => {
if (payload.type === 'block') {
const model = payload.model;
if (
matchModels(model, [ListBlockModel]) &&
model.props.type === 'numbered'
) {
const next = std.store.getNext(model);
correctNumberedListsOrderToPrev(std.store, model);
if (next) {
correctNumberedListsOrderToPrev(std.store, next);
const afterImportBlockSubscription = slots.afterImport.subscribe(
payload => {
if (payload.type === 'block') {
const model = payload.model;
if (
matchModels(model, [ListBlockModel]) &&
model.props.type === 'numbered'
) {
const next = std.store.getNext(model);
correctNumberedListsOrderToPrev(std.store, model);
if (next) {
correctNumberedListsOrderToPrev(std.store, next);
}
}
}
}
});
);
return () => {
afterImportBlockSubscription.unsubscribe();
};
};

View File

@@ -76,6 +76,7 @@ import last from 'lodash-es/last';
import type { AffineDragHandleWidget } from '../drag-handle.js';
import { PreviewHelper } from '../helpers/preview-helper.js';
import { gfxBlocksFilter } from '../middleware/blocks-filter.js';
import { cardStyleUpdater } from '../middleware/card-style-updater.js';
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
import { reorderList } from '../middleware/reorder-list';
import {
@@ -1433,6 +1434,7 @@ export class DragEventWatcher {
newIdCrossDoc(std),
reorderList(std),
surfaceRefToEmbed(std),
cardStyleUpdater(std),
];
if (selectedIds) {

View File

@@ -374,6 +374,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
type: 'resize' | 'rotate';
angle: number;
handle: ResizeHandle;
flipX?: boolean;
flipY?: boolean;
pure?: boolean;
}) => {
if (!options) {
@@ -381,8 +383,25 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
return 'default';
}
const { type, angle, handle } = options;
const { type, angle, flipX, flipY } = options;
let cursor: CursorType = 'default';
let handle: ResizeHandle = options.handle;
if (flipX) {
handle = (
handle.includes('left')
? handle.replace('left', 'right')
: handle.replace('right', 'left')
) as ResizeHandle;
}
if (flipY) {
handle = (
handle.includes('top')
? handle.replace('top', 'bottom')
: handle.replace('bottom', 'top')
) as ResizeHandle;
}
if (type === 'rotate') {
cursor = generateCursorUrl(angle, handle);
@@ -626,7 +645,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
onResizeStart: () => {
this._mode = 'resize';
},
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
if (lockRatio) {
this._scaleDirection = handle;
this._scalePercent = `${Math.round(scaleX * 100)}%`;
@@ -642,6 +661,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
type: 'resize',
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
handle,
flipX: scaleX < 0,
flipY: scaleY < 0,
});
},
onResizeEnd: () => {
@@ -652,6 +673,14 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
}
},
option => {
if (
['resize', 'rotate'].includes(
interaction.activeInteraction$.value?.type ?? ''
)
) {
return '';
}
return this._updateCursor({
...option,
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),

View File

@@ -1,43 +1,21 @@
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
import { type FrameBlockModel } from '@blocksuite/affine-model';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import type { AffineFrameTitle } from './frame-title.js';
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
private get _frames() {
return Object.values(this.store.blocks.value)
.map(({ model }) => model)
.filter(model => model instanceof FrameBlockModel);
}
getFrameTitle(frame: FrameBlockModel | string) {
const id = typeof frame === 'string' ? frame : frame.id;
const frameTitle = this.shadowRoot?.querySelector(
`affine-frame-title[data-id="${id}"]`
) as AffineFrameTitle | null;
return frameTitle;
}
export class AffineFrameTitleWidget extends WidgetComponent<FrameBlockModel> {
override render() {
return repeat(
this._frames,
({ id }) => id,
frame =>
html`<affine-frame-title
.model=${frame}
data-id=${frame.id}
></affine-frame-title>`
);
return html`<affine-frame-title
.model=${this.model}
data-id=${this.model.id}
></affine-frame-title>`;
}
}
export const frameTitleWidget = WidgetViewExtension(
'affine:page',
'affine:frame',
AFFINE_FRAME_TITLE_WIDGET,
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
);

View File

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

View File

@@ -142,12 +142,10 @@ export class AffineFrameTitle extends SignalWatcher(
}px)`,
];
const anchor = this.gfx.viewport.toViewCoord(bound.x, bound.y);
this.style.display = '';
this.style.setProperty('--bg-color', this.colors.background);
this.style.left = `${anchor[0]}px`;
this.style.top = `${anchor[1]}px`;
this.style.left = '0px';
this.style.top = '0px';
this.style.display = hidden ? 'none' : 'flex';
this.style.transform = transformOperation.join(' ');
this.style.maxWidth = `${maxWidth}px`;
@@ -205,18 +203,6 @@ export class AffineFrameTitle extends SignalWatcher(
})
);
_disposables.add(
on(this, 'click', evt => {
if (evt.shiftKey) {
this.gfx.selection.toggle(this.model);
} else {
this.gfx.selection.set({
elements: [this.model.id],
});
}
})
);
_disposables.add(
on(this, 'dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');

View File

@@ -566,23 +566,29 @@ Optional flag to insert before sibling
### updateBlock()
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
Updates a block's properties or executes a callback in a transaction
#### Type Parameters
##### T
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
#### Parameters
##### modelOrId
The block model or block ID to update
`string` | `BlockModel`\<`object`\>
`string` | `T`
##### callBackOrProps
Either a callback function to execute or properties to update
`Partial`\<`BlockProps`\> | () => `void`
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
#### Returns

View File

@@ -220,6 +220,12 @@ export class UIEventDispatcher extends LifeCycleWatcher {
this._setActive(false);
});
// When the document is hidden, the event dispatcher should be inactive
this.disposables.addFromEvent(document, 'visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this._setActive(false);
}
});
}
private _buildEventScopeBySelection(name: EventName) {

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
import type { GfxModel } from '../../model/model';
import type { ResizeHandle } from '../resize/manager';
@@ -8,6 +10,16 @@ export type ExtensionElementResizeContext = {
export type ExtensionElementResizeStartContext = {
elements: GfxModel[];
/**
* The position of the handle in the browser coordinate space.
*/
handlePos: IVec;
/**
* The sign (or normal vector) of the handle.
*/
handleSign: IPoint;
handle: ResizeHandle;
};
@@ -16,15 +28,14 @@ export type ExtensionElementResizeEndContext =
export type ExtensionElementResizeMoveContext =
ExtensionElementResizeStartContext & {
dx: number;
dy: number;
scaleX: number;
scaleY: number;
originalBound: IBound;
currentHandlePos: IVec;
lockRatio: boolean;
handleSign: {
xSign: number;
ySign: number;
};
suggest: (distance: { dx: number; dy: number }) => void;
suggest: (distance: { scaleX: number; scaleY: number }) => void;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ describe('Shape rendering with DOM renderer', () => {
fill: '#ff0000',
stroke: '#000000',
};
const shapeId = surfaceModel.addElement(shapeProps as any);
const shapeId = surfaceModel.addElement(shapeProps);
await new Promise(resolve => setTimeout(resolve, 100));
const shapeElement = surfaceView?.renderRoot.querySelector(
@@ -73,7 +73,7 @@ describe('Shape rendering with DOM renderer', () => {
subType: 'ellipse',
xywh: '[200, 200, 50, 50]',
};
const shapeId = surfaceModel.addElement(shapeProps as any);
const shapeId = surfaceModel.addElement(shapeProps);
await new Promise(resolve => setTimeout(resolve, 100));
@@ -91,4 +91,48 @@ describe('Shape rendering with DOM renderer', () => {
);
expect(shapeElement).toBeNull();
});
test('should correctly render diamond shape', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const shapeProps = {
type: 'shape',
subType: 'diamond',
xywh: '[150, 150, 80, 60]',
fillColor: '#ff0000',
strokeColor: '#000000',
filled: true,
};
const shapeId = surfaceModel.addElement(shapeProps);
await wait(100);
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
`[data-element-id="${shapeId}"]`
);
expect(shapeElement).not.toBeNull();
expect(shapeElement?.style.width).toBe('80px');
expect(shapeElement?.style.height).toBe('60px');
});
test('should correctly render triangle shape', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const shapeProps = {
type: 'shape',
subType: 'triangle',
xywh: '[150, 150, 80, 60]',
fillColor: '#ff0000',
strokeColor: '#000000',
filled: true,
};
const shapeId = surfaceModel.addElement(shapeProps);
await wait(100);
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
`[data-element-id="${shapeId}"]`
);
expect(shapeElement).not.toBeNull();
expect(shapeElement?.style.width).toBe('80px');
expect(shapeElement?.style.height).toBe('60px');
});
});

View File

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

View File

@@ -7,30 +7,40 @@ import type { InitFn } from './utils.js';
const presetMarkdown = `Click the 🔁 button to switch between editors dynamically - they are fully compatible!`;
export const preset: InitFn = async (collection: Workspace, id: string) => {
const doc = collection.createDoc(id).getStore({ id });
doc.load();
// Add root block and surface block at root level
const rootId = doc.addBlock('affine:page', {
title: new Text('BlockSuite Playground'),
});
doc.addBlock('affine:surface', {}, rootId);
let doc = collection.getDoc(id);
const hasDoc = !!doc;
if (!doc) {
doc = collection.createDoc(id);
}
// Add note block inside root block
const noteId = doc.addBlock(
'affine:note',
{ xywh: '[0, 100, 800, 640]' },
rootId
);
const store = doc.getStore({ id });
store.load();
// Import preset markdown content inside note block
await MarkdownTransformer.importMarkdownToBlock({
doc,
blockId: noteId,
markdown: presetMarkdown,
extensions: getTestStoreManager().get('store'),
});
// Run only once on all clients.
let noteId: string;
if (!hasDoc) {
// Add root block and surface block at root level
const rootId = store.addBlock('affine:page', {
title: new Text('BlockSuite Playground'),
});
store.addBlock('affine:surface', {}, rootId);
doc.resetHistory();
// Add note block inside root block
noteId = store.addBlock(
'affine:note',
{ xywh: '[0, 100, 800, 640]' },
rootId
);
// Import preset markdown content inside note block
await MarkdownTransformer.importMarkdownToBlock({
doc: store,
blockId: noteId,
markdown: presetMarkdown,
extensions: getTestStoreManager().get('store'),
});
}
store.resetHistory();
};
preset.id = 'preset';

View File

@@ -24,6 +24,7 @@
"@types/katex": "^0.16.7",
"browser-fs-access": "^0.37.0",
"jszip": "^3.10.1",
"katex": "^0.16.11",
"lit": "^3.2.0",
"lz-string": "^1.5.0",
"rxjs": "^7.8.1",

View File

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

View File

@@ -68,7 +68,7 @@
"@vitest/coverage-istanbul": "3.1.3",
"@vitest/ui": "3.1.3",
"cross-env": "^7.0.3",
"electron": "^36.0.0",
"electron": "^35.0.0",
"eslint": "^9.16.0",
"eslint-config-prettier": "^10.0.0",
"eslint-import-resolver-typescript": "^4.0.0",

View File

@@ -8,6 +8,11 @@ export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
export interface Chunk {
index: number
content: string
}
export declare function fromModelName(modelName: string): Tokenizer | null
export declare function getMime(input: Uint8Array): string
@@ -22,6 +27,11 @@ export declare function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
export declare function parseDoc(filePath: string, doc: Buffer): Promise<{ name: string, chunks: Array<{index: number, content: string}> }>
export interface ParsedDoc {
name: string
chunks: Array<Chunk>
}
export declare function parseDoc(filePath: string, doc: Buffer): Promise<ParsedDoc>
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>

View File

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

View File

@@ -2,9 +2,21 @@ use affine_common::doc_loader::Doc;
use napi::{
anyhow::anyhow,
bindgen_prelude::{AsyncTask, Buffer},
Env, JsObject, Result, Task,
Env, Result, Task,
};
#[napi(object)]
pub struct Chunk {
pub index: i64,
pub content: String,
}
#[napi(object)]
pub struct ParsedDoc {
pub name: String,
pub chunks: Vec<Chunk>,
}
pub struct Document {
inner: Doc,
}
@@ -14,24 +26,20 @@ impl Document {
self.inner.name.clone()
}
fn chunks(&self, env: Env) -> Result<JsObject> {
let mut array = env.create_array_with_length(self.inner.chunks.len())?;
for (i, chunk) in self.inner.chunks.iter().enumerate() {
let content = crate::utils::clean_content(&chunk.content);
let mut obj = env.create_object()?;
obj.set_named_property("index", i as i64)?;
obj.set_named_property("content", content)?;
array.set_element(i as u32, obj)?;
}
Ok(array)
}
fn resolve(self, env: Env) -> Result<JsObject> {
let mut obj = env.create_object()?;
obj.set_named_property("name", self.name())?;
obj.set_named_property("chunks", self.chunks(env)?)?;
Ok(obj)
fn chunks(&self) -> Vec<Chunk> {
self
.inner
.chunks
.iter()
.enumerate()
.map(|(i, chunk)| {
let content = crate::utils::clean_content(&chunk.content);
Chunk {
index: i as i64,
content,
}
})
.collect::<Vec<Chunk>>()
}
}
@@ -43,21 +51,22 @@ pub struct AsyncParseDocResponse {
#[napi]
impl Task for AsyncParseDocResponse {
type Output = Document;
type JsValue = JsObject;
type JsValue = ParsedDoc;
fn compute(&mut self) -> Result<Self::Output> {
let doc = Doc::new(&self.file_path, &self.doc).map_err(|e| anyhow!(e))?;
Ok(Document { inner: doc })
}
fn resolve(&mut self, env: Env, doc: Document) -> Result<Self::JsValue> {
doc.resolve(env)
fn resolve(&mut self, _: Env, doc: Document) -> Result<Self::JsValue> {
Ok(ParsedDoc {
name: doc.name(),
chunks: doc.chunks(),
})
}
}
#[napi(
ts_return_type = "Promise<{ name: string, chunks: Array<{index: number, content: string}> }>"
)]
#[napi]
pub fn parse_doc(file_path: String, doc: Buffer) -> AsyncTask<AsyncParseDocResponse> {
AsyncTask::new(AsyncParseDocResponse {
file_path,

View File

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

View File

@@ -15,16 +15,25 @@ pub fn from_model_name(model_name: String) -> Option<Tokenizer> {
impl Tokenizer {
#[napi]
pub fn count(&self, content: String, allowed_special: Option<Vec<String>>) -> u32 {
self
.inner
.encode(
&content,
if let Some(allowed_special) = &allowed_special {
HashSet::from_iter(allowed_special.iter().map(|s| s.as_str()))
} else {
Default::default()
},
)
.len() as u32
let allowed_special = if let Some(allowed_special) = &allowed_special {
HashSet::from_iter(allowed_special.iter().map(|s| s.as_str()))
} else {
Default::default()
};
self.inner.encode(&content, &allowed_special).0.len() as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenizer() {
let tokenizer = from_model_name("gpt-4.1".to_string()).unwrap();
let content = "Hello, world!";
let count = tokenizer.count(content.to_string(), None);
assert!(count > 0);
}
}

View File

@@ -9,4 +9,6 @@
# MAILER_SENDER="noreply@toeverything.info"
# MAILER_USER="noreply@toeverything.info"
# MAILER_PASSWORD="affine"
# MAILER_SECURE=false
# MAILER_SECURE=false
# AFFINE_INDEXER_ENABLED=true

View File

@@ -1,2 +1,9 @@
-- AlterTable
ALTER TABLE "ai_workspace_files" ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_files') THEN
-- AlterTable
ALTER TABLE "ai_workspace_files"
ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';
END IF;
END
$$;

View File

@@ -5,16 +5,25 @@
- The primary key for the `ai_workspace_file_embeddings` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- DropIndex
DROP INDEX "ai_workspace_embeddings_workspace_id_doc_id_chunk_key";
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_embeddings') AND
EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_file_embeddings') THEN
-- DropIndex
DROP INDEX "ai_workspace_embeddings_workspace_id_doc_id_chunk_key";
-- DropIndex
DROP INDEX "ai_workspace_file_embeddings_workspace_id_file_id_chunk_key";
-- DropIndex
DROP INDEX "ai_workspace_file_embeddings_workspace_id_file_id_chunk_key";
-- AlterTable
ALTER TABLE "ai_workspace_embeddings" DROP CONSTRAINT "ai_workspace_embeddings_pkey",
ADD CONSTRAINT "ai_workspace_embeddings_pkey" PRIMARY KEY ("workspace_id", "doc_id", "chunk");
-- AlterTable
ALTER TABLE "ai_workspace_embeddings"
DROP CONSTRAINT "ai_workspace_embeddings_pkey",
ADD CONSTRAINT "ai_workspace_embeddings_pkey" PRIMARY KEY ("workspace_id", "doc_id", "chunk");
-- AlterTable
ALTER TABLE "ai_workspace_file_embeddings" DROP CONSTRAINT "ai_workspace_file_embeddings_pkey",
ADD CONSTRAINT "ai_workspace_file_embeddings_pkey" PRIMARY KEY ("workspace_id", "file_id", "chunk");
-- AlterTable
ALTER TABLE "ai_workspace_file_embeddings"
DROP CONSTRAINT "ai_workspace_file_embeddings_pkey",
ADD CONSTRAINT "ai_workspace_file_embeddings_pkey" PRIMARY KEY ("workspace_id", "file_id", "chunk");
END IF;
END
$$;

View File

@@ -28,11 +28,11 @@
"dependencies": {
"@affine/reader": "workspace:*",
"@affine/server-native": "workspace:*",
"@ai-sdk/anthropic": "^1.2.10",
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/google": "^1.2.18",
"@ai-sdk/google-vertex": "^2.2.22",
"@ai-sdk/openai": "^1.3.21",
"@ai-sdk/perplexity": "^1.1.6",
"@ai-sdk/google-vertex": "^2.2.23",
"@ai-sdk/openai": "^1.3.22",
"@ai-sdk/perplexity": "^1.1.9",
"@apollo/server": "^4.11.3",
"@aws-sdk/client-s3": "^3.779.0",
"@aws-sdk/s3-request-presigner": "^3.779.0",
@@ -100,7 +100,7 @@
"nanoid": "^5.0.9",
"nest-commander": "^3.15.0",
"nest-winston": "^1.9.7",
"nestjs-cls": "^5.0.0",
"nestjs-cls": "^6.0.0",
"nodemailer": "^7.0.0",
"on-headers": "^1.0.2",
"piscina": "^5.0.0-alpha.0",

View File

@@ -47,5 +47,42 @@ function runPredeployScript() {
});
}
function fixFailedMigrations() {
console.log('fixing failed migrations.');
const maybeFailedMigrations = [
'20250521083048_fix_workspace_embedding_chunk_primary_key',
];
for (const migration of maybeFailedMigrations) {
try {
execSync(`yarn prisma migrate resolve --rolled-back ${migration}`, {
encoding: 'utf-8',
env: process.env,
stdio: 'pipe',
});
console.log(`migration [${migration}] has been rolled back.`);
} catch (err) {
if (
err.message.includes(
'cannot be rolled back because it is not in a failed state'
) ||
err.message.includes(
'cannot be rolled back because it was never applied'
) ||
err.message.includes(
'called markMigrationRolledBack on a database without migrations table'
)
) {
// migration has been rolled back, skip it
continue;
}
// ignore other errors
console.log(
`migration [${migration}] rolled back failed. ${err.message}`
);
}
}
}
prepare();
fixFailedMigrations();
runPredeployScript();

View File

@@ -272,13 +272,68 @@ test('should validate markdown list', t => {
// ==================== action ====================
const actions = [
{
name: 'Should chat with histories',
promptName: ['Chat With AFFiNE AI'],
messages: [
{
role: 'user' as const,
content: `
Hi! Im going to send you a technical term related to real-time collaborative editing (e.g., CRDT, Operational Transformation, OT Composer, etc.). Whenever I send you a term:
1. Translate it into Chinese (send me the Chinese version).
2. Then translate that Chinese back into English (send me the retranslated English).
3. Provide a brief, English-language introduction and context for this concept.
4. In that English explanation, annotate any niche terms with their Chinese equivalents in parentheses (for example: “Conflict-Free Replicated Data Type (无冲突复制数据类型)”).
5. Finally, give the origin or “term history” (e.g., who introduced it, in which paper or year).
If you understand, please proceed by explaining the term “CRDT.”
`.trim(),
},
{
role: 'assistant' as const,
content: `
1. **Chinese Translation:**
“CRDT” → **无冲突复制数据类型**
2. **Back-Translation to English:**
无冲突复制数据类型 → **Conflict-Free Replicated Data Type**
3. **English Introduction & Context:**
A **Conflict-Free Replicated Data Type (无冲突复制数据类型)** is an abstract data type designed for distributed systems where replicas of shared state may be modified concurrently without requiring coordination. CRDTs allow multiple users or processes to update the same data structure (for example, a shared document in a collaborative editor) at the same time.
- **Key Terms (with Chinese equivalents):**
- **Replica (副本):** Each node or client maintains its own copy of the data.
- **State-based (状态型) vs. Operation-based (操作型):** Two main CRDT classes; state-based CRDTs exchange entire state snapshots occasionally, whereas operation-based CRDTs broadcast only incremental operations.
- **Merge Function (合并函数):** A deterministic function that resolves differences between two replicas without conflicts.
CRDTs enable **eventual consistency (最终一致性)** in real-time collaborative editors by ensuring that, after all updates propagate, every replica converges to the same state, even if operations arrive in different orders. This approach removes the need for a centralized server to resolve conflicts, making offline or peer-to-peer editing possible.
4. **Origin / Term History:**
The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Carlos Baquero, and Marek Zawirski in their 2011 paper titled “Conflict-free Replicated Data Types” (published in the _Stabilization, Safety, and Security of Distributed Systems (SSS)_ conference). They formalized two families of CRDTs—state-based (“Convergent Replicated Data Types” or CvRDTs) and operation-based (“Commutative Replicated Data Types” or CmRDTs)—and proved their convergence properties under asynchronous, unreliable networks.
`.trim(),
},
{
role: 'user' as const,
content: `Thanks! Now please just tell me the **Chinese translation** and the **back-translated English term** that you provided previously for “CRDT.” Do not reprint the full introduction—only those two lines.`,
},
],
verifier: (t: ExecutionContext<Tester>, result: string) => {
assertNotWrappedInCodeBlock(t, result);
const lower = result.toLowerCase();
t.assert(
lower.includes('无冲突复制数据类型') &&
lower.includes('conflict-free replicated data type'),
'The response should include “无冲突复制数据类型” and “Conflict-Free Replicated Data Type”'
);
},
type: 'text' as const,
},
{
name: 'Should not have citation',
promptName: ['Chat With AFFiNE AI'],
messages: [
{
role: 'user' as const,
content: 'what is ssot',
content: 'what is AFFiNE AI?',
params: {
files: [
{

View File

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

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