Compare commits

..

88 Commits

Author SHA1 Message Date
L-Sun
6f9c1554b7 fix(editor): keyboard can not open after closing input modal (#13041)
#### PR Dependency Tree


* **PR #13041** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of keyboard provider fallbacks to ensure more
reliable keyboard behavior when certain features are unavailable.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 12:22:09 +00:00
L-Sun
eb9652ed4c fix(editor): adjust highlght style of comment and comment editor flickering (#13040)
### Before


https://github.com/user-attachments/assets/6b98946b-d53c-42fb-b341-e09ba5204523

### After


https://github.com/user-attachments/assets/274341de-33c4-4fd3-b01b-a8f7c25bf2fe



#### PR Dependency Tree


* **PR #13040** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Improved comment highlighting: Clicking now cycles through and
highlights individual comments one at a time instead of highlighting all
at once.
* Highlighting behavior is now more flexible, allowing highlighting to
be toggled on or off in certain scenarios.

* **Bug Fixes**
* Prevented flickering in the comment editor when focusing on comments.

* **Refactor**
* Enhanced selection and anchoring logic to support the new highlight
flag and updated types for improved clarity and control.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13040** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-04 11:55:00 +00:00
L-Sun
ee8c7616bc chore(core): remove client comment feature flag (#13034)
#### PR Dependency Tree


* **PR #13034** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Refactor**
  * Removed the comment feature flag from the application.
* The ability to enable comments now depends solely on server
configuration, not on a feature flag.
* **Chores**
* Cleaned up related feature flag definitions and internal logic for
comment enablement.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-07-04 11:26:30 +00:00
Peng Xiao
1452f77c85 fix(core): list comment changes usage (#13036)
fix AF-2710

#### PR Dependency Tree


* **PR #13036** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Style**
* Limited the maximum width of the comment input container to 800 pixels
for improved layout consistency.

* **New Features**
* Enhanced comment change listings to include pagination information,
allowing users to navigate through comment changes more effectively.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-07-04 11:02:09 +00:00
Wu Yue
2f9a96f1c5 feat(core): support open doc in ai session history (#13035)
Close [AI-240
<img width="533" alt="截屏2025-07-04 18 04 39"
src="https://github.com/user-attachments/assets/726a54b6-3bdb-4e70-9cda-4671d83ae5bd"
/>
](https://linear.app/affine-design/issue/AI-240)

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

* **New Features**
* Enhanced chat toolbar and session history with the ability to open
specific documents directly from the chat interface.
* Added tooltips and improved click handling for clearer user
interactions in chat session and document lists.

* **Bug Fixes**
* Prevented redundant actions when attempting to open already active
sessions or documents.

* **Style**
* Improved tooltip formatting and visual styling for error messages and
tooltips.
* Refined hover effects and layout in chat session history for better
clarity.

* **Refactor**
* Updated tooltip configuration for more precise positioning and
behavior.

* **Chores**
* Minor updates to property defaults for tooltips and chat panel
components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-07-04 11:00:52 +00:00
德布劳外 · 贾贵
c882a8c5da feat(core): markdown-diff & patch apply (#12844)
## New Features
- **Markdown diff**: 
- Introduced block-level diffing for markdown content, enabling
detection of insertions, deletions, and replacements between document
versions.
  - Generate patch operations from markdown diff.
- **Patch Renderer**: Transfer patch operations to a render diff which
can be rendered into page body.
- **Patch apply**:Added functionality to apply patch operations to
documents, supporting block insertion, deletion, and content
replacement.

## Refactors
* Move `affine/shared/__tests__/utils` to
`blocksuite/affine-shared/test-utils`


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

* **New Features**
* Introduced utilities for declarative creation and testing of document
structures using template literals.
* Added new functions and types for block-level markdown diffing and
patch application.
* Provided a utility to generate structured render diffs for markdown
blocks.
* Added a unified test-utils entry point for easier access to testing
helpers.

* **Bug Fixes**
* Updated import paths in test files to use the new test-utils location.

* **Documentation**
* Improved example usage in documentation to reflect the new import
paths for test utilities.

* **Tests**
* Added comprehensive test suites for markdown diffing, patch
application, and render diff utilities.

* **Chores**
* Updated package dependencies and export maps to expose new test
utilities.
* Refactored internal test utilities organization for clarity and
maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

> CLOSE AI-271 AI-272 AI-273
2025-07-04 10:48:49 +00:00
fengmk2
5da56b5b04 chore(server): fix unstable test (#13037)
#### PR Dependency Tree


* **PR #13037** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Tests**
* Improved test reliability by generating unique user accounts and
prompt names for each test run.
  * Updated test setup to streamline database initialization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 18:46:07 +08:00
fengmk2
831da01432 fix(server): only send comment mention notification when comment author is doc owner (#13033)
close AF-2711



#### PR Dependency Tree


* **PR #13033** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Tests**
* Added an end-to-end test to verify that mention notifications are
correctly sent when replying to a comment authored by the document
owner.

* **Refactor**
* Improved the notification logic to streamline how mention and owner
notifications are sent, reducing redundancy and ensuring correct
recipients.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 16:54:16 +08:00
L-Sun
eb56adea46 fix(editor): time issues of comment initialization (#13031)
#### PR Dependency Tree


* **PR #13031** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Added the ability to filter comments by their resolution status
(resolved, unresolved, or all) when viewing or managing comments.

* **Refactor**
* Improved the way commented text is identified and retrieved, resulting
in more reliable comment selection and filtering.
* Enhanced comment retrieval to support asynchronous data loading,
ensuring up-to-date comment information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 08:19:00 +00:00
Hwang
a485ad5c45 feat(server): update tool descriptions and AI prompt (#13032)
update tools description & chat prompt

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

## Summary by CodeRabbit

* **New Features**
* Updated the AFFiNE AI copilot system prompt to reflect support for
multiple AI providers and a more concise, structured format with clearer
guidelines and modular tags.

* **Enhancements**
* Improved descriptions for document search and reading tools, providing
clearer guidance on when and how to use keyword search, semantic search,
and document reading features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 08:06:21 +00:00
fengmk2
296089efc9 feat(core): add comment notification settings (#13029)
![image](https://github.com/user-attachments/assets/1b239592-1c0d-4575-ad3b-bfb3d0c873c8)





#### PR Dependency Tree


* **PR #13029** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Added an option in user settings to enable or disable email
notifications for comments on your documents.
* Updated the user interface to include a toggle for comment email
notifications.
* Extended GraphQL queries and schema to support the new comment email
notification setting.

* **Localization**
* Added new English translations for comment email notification
settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 08:04:18 +00:00
Cats Juice
882d06b359 fix(core): re-layout ai-chat-content to display preview panel (#13030)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a split-view layout in the AI chat, allowing chat content
and a preview panel to be displayed side-by-side.
* Added responsive padding and layout adjustments for improved chat
panel appearance.

* **Refactor**
* Simplified the chat panel by removing the previous preview panel
feature and related state from the main chat component.
  * Updated internal logic to support the new split-view structure.

* **Style**
* Adjusted chat panel and workspace chat page styles for better layout
consistency and responsiveness.

* **Chores**
* Improved code organization and import statements for maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 08:00:24 +00:00
DarkSky
b9c4d7230e feat(server): update session after doc deletion (#13028)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Sessions associated with a deleted document are now automatically
updated to remove the document reference.

* **Improvements**
* Enhanced session management to better handle documents that have been
deleted.

No visible changes to the user interface; these updates improve backend
handling of session and document relationships.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 07:54:19 +00:00
Peng Xiao
d0beab9638 refactor(core): call copilot in tools (#13024)
fix AI-298

#### PR Dependency Tree


* **PR #13024** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Document and code artifact generation tools now use a single prompt
field for user input, enabling more flexible content creation powered by
AI.

* **Bug Fixes**
* Improved error handling for missing prompt templates or providers
during document and code artifact generation.

* **Refactor**
* Simplified input schemas for document and code artifact tools,
consolidating multiple input fields into a single prompt.
* Output schemas updated to remove metadata and other unused fields for
a cleaner result.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 07:23:09 +00:00
Wu Yue
24f1181069 feat(core): support ai recent session history (#13025)
Close [AI-239](https://linear.app/affine-design/issue/AI-239)
Close [AI-240](https://linear.app/affine-design/issue/AI-240)
Close [AI-242](https://linear.app/affine-design/issue/AI-242)

<img width="365" alt="截屏2025-07-04 13 49 25"
src="https://github.com/user-attachments/assets/d7c830f0-cc16-4a26-baf1-480c7d42838f"
/>


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

* **New Features**
* Introduced a floating chat history menu, allowing users to view and
switch between recent AI chat sessions grouped by recency.
* Added a new component for displaying recent chat sessions with
document icons and titles.
* Enhanced chat toolbar with asynchronous confirmation dialogs before
switching or creating sessions.
* Added notification support for chat-related actions and history
clearing.
* Added ability to fetch and display recent AI chat sessions per
workspace.

* **Improvements**
  * Streamlined session management and event handling in the chat panel.
* Improved embedding progress update and context change handling across
chat components.
  * Refined UI for chat history, session switching, and notifications.
* Updated chat components to use direct notification service injection
for better user prompts and toasts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 06:51:35 +00:00
Jakob
eb73c90b2e fix(server): allow MS Office365 / Azure compatibility by making OIDC.preferred_username optional (#13027)
> [!NOTE]
> **This is a reopened (already approved) PR**
> Needed to reopen https://github.com/toeverything/AFFiNE/pull/13011
because commit email was wrong and I could not sign the CLA

Make Office365 / Azure login possible by making preferred_username
optional.
This is NOT send in the token of MS.

To make this work you ALSO need to set the oidc.config.args.id to
"email" (there preferred_username is used as default)
Source:
https://github.com/toeverything/AFFiNE/blob/canary/packages/backend/server/src/plugins/oauth/providers/oidc.ts#L152

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved compatibility with OIDC providers by allowing the preferred
username field to be optional during user info validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 06:36:28 +00:00
EYHN
f961d9986f fix(core): fix migrate filter list error (#13022)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved error handling for collection filter migrations, reducing the
chance of failures affecting filter lists.
* **New Features**
* Expanded support for filter conditions on the "Tags" field, including
options like "is empty," "is not empty," "contains all," and more.
* **Enhancements**
* Improved handling of "Is Favourited" and "Is Public" filters for more
consistent results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 06:12:21 +00:00
DarkSky
5a49d5cd24 fix(server): abort behavior in sse stream (#12211)
fix AI-121
fix AI-118

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

- **Bug Fixes**
- Improved handling of connection closures and request abortion for
streaming and non-streaming chat endpoints, ensuring session data is
saved appropriately even if the connection is interrupted.
- **Refactor**
- Streamlined internal logic for managing request signals and connection
events, resulting in more robust and explicit session management during
streaming interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 06:07:45 +00:00
fengmk2
1b9ed2fb6d fix(server): send comment mention to comment author by default (#13018)
close AF-2708



#### PR Dependency Tree


* **PR #13018** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Added notifications for users when their comment receives a reply,
marking them with a mention.
* **Tests**
* Introduced an end-to-end test to verify that replying to a comment
sends a mention notification to the original comment author.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 05:38:32 +00:00
Lakr
ed6adcf4d9 feat: basic chat implementation completed (#13023)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a new chat list view with improved grouping of messages by
date and support for rich markdown rendering, including math
expressions.
* Added support for displaying user message attachments and hints within
the chat interface.

* **Improvements**
* Enhanced chat cell designs for user, assistant, and system messages,
providing clearer layouts and better text rendering.
* Streamlined chat message streaming with incremental markdown updates
and improved scrolling behavior.
* Updated chat view models to include timestamps and refined message
typing.

* **Bug Fixes**
* Improved handling of streaming responses and error reporting with more
accurate timestamps.

* **Refactor**
* Replaced the legacy table-based chat UI with a modern list-based
implementation.
* Simplified and unified chat cell view models and cell rendering logic.

* **Chores**
* Updated and added several third-party dependencies to support new UI
components and markdown features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 05:27:18 +00:00
Cats Juice
2b0b20cdd4 feat(core): add ai-chat-toolbar for independent chat (#13021)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced an AI chat toolbar for improved session management and
interaction.
  * Added the ability to pin chat sessions and reset chat content.
  * Enhanced chat header layout for better usability.

* **Improvements**
* Streamlined session creation and management within the AI chat
interface.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 05:16:20 +00:00
Peng Xiao
fe8cb6bb44 fix(core): some artifact styles (#13020)
fix AI-299, AI-296

#### PR Dependency Tree


* **PR #13020** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Style**
* Improved layout alignment in the artifact preview panel for better
visual consistency.
* Enforced a minimum width for linked document banners to ensure
consistent appearance.

* **Bug Fixes**
* Updated artifact and document compose tools so that clicking an
artifact result always opens the preview panel, instead of toggling or
closing it unexpectedly.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 04:15:50 +00:00
EYHN
d0d94066f7 feat(ios): hidden version variant (#13019)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* On iOS devices, the app and editor version numbers in the About
section now display only the main version (e.g., "0.23.0"), hiding any
additional suffixes.
* **Other**
* No visible changes for users on non-iOS platforms; full version
strings remain displayed.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 04:13:44 +00:00
Cats Juice
64fb3a7243 feat(core): add an independent AI panel (#13004)
close AI-246, AI-285
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Introduced an AI chat interface accessible from the sidebar with a
dedicated "/chat" route.
* Added "AFFiNE Intelligent" button with AI icon to the sidebar for
quick chat access.
* Enhanced chat components with an "independent mode" for improved
message display and layout.
* Improved chat input and content styling, including responsive layout
and onboarding offset support.

* **Improvements**
  * Expanded icon support to include an AI icon in the app.
* Updated utility and schema functions for greater flexibility and error
prevention.
* Added a new chat container style for consistent layout and max width.

* **Bug Fixes**
* Prevented potential errors when certain editor hosts are not provided.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 02:10:35 +00:00
fengmk2
e6b456330c chore(server): use localhost sub domain (#13012)
reduce dnsmasq service



#### PR Dependency Tree


* **PR #13012** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Documentation**
* Updated instructions to use the domain `affine.localhost` instead of
`dev.affine.fail`.
* Simplified setup steps by removing references to DNS service
configuration and custom DNS server entries.

* **Chores**
* Removed DNS server configuration and related files from the
development environment setup.
  * Updated example descriptions to reflect the new domain.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 00:23:19 +00:00
DarkSky
2b7a8dcd8a feat(server): use new content reader (#13007)
partial fix AI-280
2025-07-04 00:22:44 +00:00
Peng Xiao
8ed7dea823 feat(core): code artifact tool (#13015)
<img width="1272" alt="image"
src="https://github.com/user-attachments/assets/429ec60a-48a9-490b-b45f-3ce7150ef32a"
/>


#### PR Dependency Tree


* **PR #13015** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Introduced a new AI tool for generating self-contained HTML artifacts,
including a dedicated interface for previewing, copying, and downloading
generated HTML.
* Added syntax highlighting and preview capabilities for HTML artifacts
in chat and tool panels.
* Integrated the new HTML artifact tool into the AI chat prompt and
Copilot toolset.

* **Enhancements**
* Improved artifact preview panel layout and sizing for a better user
experience.
* Enhanced HTML preview components to support both model-based and raw
HTML rendering.

* **Dependency Updates**
  * Added the "shiki" library for advanced syntax highlighting.

* **Bug Fixes**
  * None.

* **Chores**
* Updated internal imports and code structure to support new features
and maintain consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 23:39:51 +00:00
DarkSky
53968f6f8c feat(server): edit tool intent collect (#12998)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Footnotes are now included in streamed AI responses, formatted as
markdown and appended at the end of the output when available.

* **Improvements**
* Enhanced handling of footnotes across multiple AI providers, ensuring
consistent display of additional information after the main response.

* **Refactor**
  * Removed citation parsing from one provider to streamline output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 23:32:30 +00:00
Peng Xiao
cfc108613c feat(core): support compose a doc tool (#13013)
#### PR Dependency Tree


* **PR #13013** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Introduced a document composition tool for AI chat, allowing users to
generate, preview, and save structured markdown documents directly from
chat interactions.
* Added an artifact preview panel for enhanced document previews within
the chat interface.
* Enabled dynamic content rendering in the chat panel's right section
for richer user experiences.

* **Improvements**
  * Sidebar maximum width increased for greater workspace flexibility.
* Enhanced chat message and split view styling for improved layout and
usability.

* **Bug Fixes**
  * None.

* **Other**
* Registered new custom elements for AI tools and artifact preview
functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 14:21:49 +00:00
L-Sun
558279da29 feat(editor): resolve unassociated comments on init (#13008)
#### PR Dependency Tree


* **PR #13008** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved synchronization between comment states in the editor and the
comment provider to ensure consistency of inline and block comments.

* **Refactor**
* Separated and modularized utility functions for finding commented
texts and blocks, enhancing maintainability and reusability.

* **Style**
* Updated styles to remove background color and border from inline
comments within embedded note content.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 12:30:07 +00:00
EYHN
c5b442225f chore(ios): allow stable ios release (#13010)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated workflow so the "Testflight" step always runs for iOS builds,
regardless of build type.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 12:29:44 +00:00
Wu Yue
134e62a0fa feat(core): support ai chat add, pin and unpin (#13002)
Close [AI-241](https://linear.app/affine-design/issue/AI-241)
Close [AI-237](https://linear.app/affine-design/issue/AI-237)
Close [AI-238](https://linear.app/affine-design/issue/AI-238)

<img width="564" alt="截屏2025-07-03 15 54 10"
src="https://github.com/user-attachments/assets/8654db2b-cb71-4906-9e3b-0a723d7459e1"
/>


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

* **New Features**
* Introduced a chat toolbar with options to create new sessions and
pin/unpin chat sessions.
* Enhanced session management, allowing users to pin sessions and
control session reuse.
* Added the ability to update session properties directly from the chat
panel.

* **Improvements**
* Chat panel now prioritizes pinned sessions and provides clearer
session initialization.
* Editor actions in chat messages are shown only when relevant document
information is present.
  * Toolbar and chat content UI improved for clarity and usability.
  * Scroll position is preserved and restored for pinned chat sessions.
* Session API updated to support more structured input types and new
session creation options.

* **Bug Fixes**
* Actions and toolbar buttons are now conditionally displayed based on
session and message state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 11:26:36 +00:00
fengmk2
92cd2a3d0e chore(server): use jemalloc to reduce RSS (#13001)
close CLOUD-236



#### PR Dependency Tree


* **PR #13001** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **Chores**
* Updated system configuration to preload jemalloc for improved memory
management in the Node.js application.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 10:39:34 +00:00
DarkSky
41524425bc fix(server): incorrect list condition (#13005)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved session filtering to use the correct criteria when querying
sessions, ensuring more accurate results based on the action parameter.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 10:31:11 +00:00
L-Sun
7879e12718 fix(editor): comment style and creation issues (#13006)
#### PR Dependency Tree


* **PR #13006** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Style**
* Updated embedded note content to remove background color and underline
from inline comments for a cleaner appearance.

* **Bug Fixes**
* Improved comment functionality by ensuring comments are only added to
blocks without text content.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 10:08:00 +00:00
Peng Xiao
81f542a4da fix(core): dedicated link config for comments (#13003)
#### PR Dependency Tree


* **PR #13003** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Introduced enhanced member mention functionality in the comment
editor, including improved search, highlighting, and avatar display for
mentioning users.

* **Refactor**
* Simplified configuration options for linked widgets and menu groups,
removing the ability to selectively include menu groups.
* Updated internal logic to streamline menu group handling and improve
performance during member searches.

* **Bug Fixes**
* Prevented unnecessary member search calls on empty queries for better
efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 08:19:19 +00:00
Peng Xiao
532ea6af07 feat(core): support mentions in comments (#13000)
fix AF-2706, PD-2687

<img width="412" alt="image"
src="https://github.com/user-attachments/assets/b796f543-1c42-452a-8f65-9dddfa751ab4"
/>
<img width="384" alt="image"
src="https://github.com/user-attachments/assets/7ac3bcc5-6cf1-49bb-9786-1eb33fad7225"
/>
<img width="347" alt="image"
src="https://github.com/user-attachments/assets/02babd37-4740-4770-8be8-e253be18bb5a"
/>

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

## Summary by CodeRabbit

* **New Features**
* Added support for mentions in comments and replies, including
detection and notification when users are mentioned.
* Introduced new notification types for comments and comment mentions,
with dedicated notification components and localized messages.
  * Enabled navigation directly to specific comments from notifications.
* Sidebar comment tab and comment features now depend on both feature
flags and server support.

* **Improvements**
  * Comment creation and reply workflows now support optional mentions.
* Menu configurations for linked widgets can now selectively include
specific menu groups.
* Enhanced navigation helper with a function to jump directly to a page
comment.
  * Improved comment entity lifecycle management for proper cleanup.

* **Bug Fixes**
* Improved lifecycle management for comment entities to ensure proper
cleanup.

* **Style**
* Updated mention styling to use a dynamic font size based on theme
variables.
  * Adjusted comment preview container underline highlight color.

* **Localization**
* Added English translations for comment and mention notification
messages.

* **Configuration**
* Updated feature flag logic for comment features, making configuration
more flexible and environment-aware.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->





#### PR Dependency Tree


* **PR #13000** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-03 06:44:14 +00:00
fengmk2
a7aa761e43 refactor(server): use Blob Model on storage (#12897)
#### PR Dependency Tree


* **PR #12894**
  * **PR #12897** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-03 05:44:09 +00:00
fengmk2
062537c2cf feat(server): add Blob Model (#12894)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced comprehensive management for workspace blobs, including
creating, updating, soft and permanent deletion, and retrieval of blobs.
- Added the ability to list all blobs, list deleted blobs, and calculate
the total size of blobs within a workspace.

- **Tests**
- Added extensive automated tests to validate blob creation, updating,
deletion, retrieval, and aggregation functionalities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #12894** 👈
  * **PR #12897**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-03 05:17:59 +00:00
DarkSky
2ea3c3da9d feat(server): expose reuse latest chat (#12999)
fix AI-289
2025-07-03 04:46:23 +00:00
DarkSky
4fd42a8461 fix(server): model mistakenly thinks the edit failed (#12997)
fix AI-288
2025-07-03 04:32:45 +00:00
L-Sun
32c40bbf09 refactor(core): minimize comment editor (#12995)
#### PR Dependency Tree


* **PR #12995** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Introduced a new clipboard module, making clipboard-related
functionality available for external use.
* Added a comprehensive extension system for the comment editor,
supporting rich text features, widgets, and configurable options.

* **Bug Fixes**
* Improved stability by ensuring comment highlighting features and
toolbar event subscriptions handle missing dependencies gracefully,
preventing potential runtime errors.

* **Refactor**
* Simplified comment editor view manager setup for easier configuration
and maintenance.

* **Chores**
* Updated package exports to expose new clipboard modules and
configurations.
* Removed confirm modal and portal-related logic from the comment editor
component.
* Adjusted temporary store creation to omit adding an extra surface
block under the root page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 04:21:28 +00:00
fengmk2
5d8ee51e8c chore(infra): fix canary version format (#12994)
#### PR Dependency Tree


* **PR #12994** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Chores**
* Updated the timestamp format in canary version strings to remove the
hyphen between hour and minute components.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 03:59:23 +00:00
EYHN
b2bf591463 feat(core): bettery save mode (#12996)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a battery save mode that can pause background sync and
processing to conserve energy.
* Battery save mode is automatically enabled when the app window loses
focus and disabled when the window regains focus or is interacted with.
  * Available in both web and desktop (Electron) versions.

* **Improvements**
* Added user activity detection to optimize background operations for
better battery efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 03:48:17 +00:00
DarkSky
2ae3c3e2cd feat(server): deprecate blob id provide by client (#12991)
fix AI-287
2025-07-03 03:45:34 +00:00
EYHN
4641b080f2 feat(core): quick search support search locally (#12987)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a "search locally" option in the docs quick search,
allowing users to perform searches on their local device when supported.
* Added new quick search group labels and options for local search, with
dynamic UI updates based on search mode.
* **Improvements**
  * Enhanced search responsiveness by reducing input throttling delay.
  * Added a pre-submission check to improve search item handling.
* Improved stability by handling cases where document IDs may be missing
during search result processing.
* **Localization**
* Added English language support for new local search options and
labels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 03:11:55 +00:00
fengmk2
aa7edb7255 chore(server): add comment server feature flags (#12993)
https://github.com/toeverything/AFFiNE/pull/12989#discussion_r2180167232



#### PR Dependency Tree


* **PR #12993** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* The "Comment" feature is now recognized as a server feature and is
enabled by default.
* The server configuration and API schema have been updated to include
the "Comment" feature.

* **Tests**
* Added a new end-to-end test to verify that the "Comment" feature is
enabled by default.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 01:27:24 +00:00
Wu Yue
3e03599d11 feat(core): make editor host optional (#12990)
Close [AI-260](https://linear.app/affine-design/issue/AI-260)

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

* **New Features**
* Added support for passing workspace and document identifiers directly
to chat components, enabling improved context handling in AI chat
features.

* **Bug Fixes**
* Improved null safety and error handling across AI chat components to
prevent issues when certain properties are missing.
* Enhanced defensive checks to avoid runtime errors related to missing
or undefined properties.

* **Refactor**
* Simplified and standardized property types and data flow in AI chat
components, reducing reliance on certain objects and making properties
optional where appropriate.
* Streamlined error messaging and tool integration by updating property
and parameter structures.
* Updated tool components to use image proxy services directly, removing
dependency on host objects.

* **Chores**
* Updated type definitions and interfaces for better flexibility and
maintainability.
  * Added new interfaces to clarify session creation parameters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 00:49:07 +00:00
Peng Xiao
a21f1c943e feat(core): comment panel (#12989)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a full-featured document comment system with sidebar for
viewing, filtering, replying, resolving, and managing comments and
replies.
* Added a rich text comment editor supporting editable and read-only
modes.
  * Enabled comment-based navigation and highlighting within documents.
* Integrated comment functionality into the workspace sidebar (excluding
local workspaces).
* Added internationalization support and new UI strings for comment
features.
* Added new feature flag `enable_comment` for toggling comment
functionality.
  * Enhanced editor focus to support comment-related selections.
  * Added snapshot and store helpers for comment content management.
* Implemented backend GraphQL support for comment and reply operations.
* Added services for comment entity management and comment panel
behavior.
* Extended comment configuration to support optional framework
providers.
* Added preview generation from user selections when creating comments.
  * Enabled automatic sidebar opening on new pending comments.
  * Added comment-related query parameter support for navigation.
  * Included inline comment module exports for integration.
* Improved comment provider implementation with full lifecycle
management and UI integration.
* Added comment highlight state tracking and refined selection handling
in inline comments.

* **Style**
* Added comprehensive styles for the comment editor and sidebar
components.

* **Chores**
  * Updated language completeness percentages for multiple locales.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: L-Sun <zover.v@gmail.com>
2025-07-02 15:47:00 +00:00
Cats Juice
a59448ec4b feat(core): add a resizeable split view for ai chat (#12896)
The visibility of preview panel is controlled by `showPreviewPanel` in
`ChatPanel`, but there is no entrance to open it in this PR.

![CleanShot 2025-06-23 at 15 13
39](https://github.com/user-attachments/assets/fc0e9ecf-a64d-4a21-8e10-7e838cd9e985)



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

- **New Features**
- Introduced a split-view layout in the chat panel, allowing users to
view both the chat and a new preview panel side by side.
- Added a draggable divider for resizing the chat and preview panels,
with the divider position saved automatically for future sessions.

- **Refactor**
- Updated the chat panel interface to support the new split-view and
preview panel functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 10:15:23 +00:00
fengmk2
f761cbd964 chore(server): remove useless client allowGuestDemoWorkspace config (#12988)
#### PR Dependency Tree


* **PR #12988** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **Chores**
* Removed the configuration option that allowed guests to access demo
workspaces. This affects related settings in the system configuration
and admin interface.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 10:13:15 +00:00
L-Sun
d768ad4af0 feat(editor): block comment extension (#12980)
#### PR Dependency Tree


* **PR #12980** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-02 17:42:16 +08:00
L-Sun
8ce85f708d feat(editor): comment extension (#12948)
#### PR Dependency Tree


* **PR #12948** 👈
  * **PR #12980**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Introduced inline comment functionality, allowing users to add,
resolve, and highlight comments directly within text.
  * Added a new toolbar action for inserting comments when supported.
* Inline comments are visually highlighted and can be interacted with in
the editor.

* **Enhancements**
  * Integrated a feature flag to enable or disable the comment feature.
* Improved inline manager rendering to support wrapper specs for
advanced formatting.

* **Developer Tools**
* Added mock comment provider for testing and development environments.

* **Chores**
* Updated dependencies and project references to support the new inline
comment module.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 09:14:34 +00:00
L-Sun
a66096cdf9 refactor(editor): support dynamic text attribute key (#12947)
#### PR Dependency Tree


* **PR #12946**
  * **PR #12947** 👈
    * **PR #12948**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-02 08:09:01 +00:00
fengmk2
facf6ee28b fix(server): disable Apple oauth on client version < 0.22.0 (#12984)
close AF-2705



#### PR Dependency Tree


* **PR #12984** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* The Apple OAuth provider is now available only for clients version
0.22.0 or higher.
* Client version detection has been improved by extracting version
information from request headers.

* **Bug Fixes**
* Ensured that the Apple OAuth provider is hidden for clients below
version 0.22.0.

* **Tests**
* Added comprehensive end-to-end and utility tests for OAuth provider
selection and client version extraction.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 08:07:34 +00:00
L-Sun
bcd6a70b59 refactor(editor): narrow text format parameter (#12946)
#### PR Dependency Tree


* **PR #12946** 👈
  * **PR #12947**
    * **PR #12948**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Refactor**
* Updated terminology and types from "text style" to "text attributes"
throughout the text formatting features for improved clarity and
consistency.
* Separated style-specific attributes (like bold, italic, color, and
background) from other text metadata.
* Renamed relevant commands and updated menu and toolbar configurations
to use the new attribute structure.

* **New Features**
* Added support for color and background properties in text style
attributes.

* **Bug Fixes**
* Improved consistency and reliability in text formatting and
highlighting behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 06:55:14 +00:00
Cats Juice
423c5bd711 fix(core): adjust ai chat tool calling style (#12985)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Enhanced the visual appearance of tool cards with improved
backgrounds, shine animation, refined layouts, and smoother
collapse/expand transitions.
* Updated icon sizes, hover effects, and icon presentation for a more
polished user experience.

* **Bug Fixes**
* Improved result rendering logic to prevent errors when result arrays
are empty.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 06:29:33 +00:00
EYHN
3d12bb2adf feat(core): add missing affine version header to server config (#12986)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a custom version header to server configuration requests for
improved request context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 04:56:57 +00:00
fengmk2
a2810f3f61 feat(server): send comment notifications (#12925)
close CLOUD-228















#### PR Dependency Tree


* **PR #12924**
  * **PR #12925** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-01 14:25:31 +00:00
fengmk2
7ed72ed1d0 feat(server): support comment notification type (#12924)
#### PR Dependency Tree


* **PR #12924** 👈
  * **PR #12925**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Introduced comment and comment mention notifications, including email
notifications when users are mentioned or receive comments on documents.
* Added new email templates for comment and comment mention
notifications.
* Users can now control whether they receive comment-related emails via
a new user setting.

* **Bug Fixes**
  * None.

* **Documentation**
* Updated GraphQL schema documentation to reflect new notification types
and user settings.

* **Refactor**
* Streamlined and enhanced test coverage for notification and user
settings, including comment notifications.

* **Chores**
* Improved test setup and snapshot coverage for user settings and
notifications.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 13:48:06 +00:00
fengmk2
2aa5c13082 feat(server): comment service and resolver (#12761)
close CLOUD-227
close CLOUD-230

































#### PR Dependency Tree


* **PR #12761** 👈
  * **PR #12924**
    * **PR #12925**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Introduced a comprehensive commenting system, enabling users to
create, update, resolve, and delete comments and replies on documents.
* Added support for uploading attachments to comments, with clear error
messaging if size limits are exceeded.
* Implemented role-based permissions for comment actions, including a
new "Commenter" role.
* Enabled paginated listing and change tracking of comments and replies
via GraphQL queries.
* Provided full localization and error handling for comment-related
actions.

* **Bug Fixes**
* Improved uniqueness handling when fetching user data for comments and
replies.

* **Documentation**
* Extended GraphQL schema and frontend localization to document and
support new comment features.

* **Tests**
* Added extensive backend test suites covering all comment and reply
functionalities, permissions, and attachment uploads.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 13:12:28 +00:00
Lakr
6a04fbe335 fix: 🚑 build on Xcode 16.2 (#12981)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved compatibility for chat cell handling, allowing broader
support for different view model types in chat-related features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 12:47:21 +00:00
DarkSky
0326da0806 feat(server): add typed list session gql (#12979)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced new API endpoints and GraphQL queries to retrieve Copilot
chat sessions by workspace, document, and pinned status, with detailed
session and message information.
* Added support for filtering and querying Copilot chat histories with
new options such as pinned status and message ordering.

* **Bug Fixes**
* Improved filtering logic for listing and retrieving chat sessions,
ensuring accurate results for workspace, document, and pinned session
queries.

* **Tests**
* Expanded and refactored test coverage for session listing, filtering,
and new query options to ensure reliability and correctness of Copilot
session retrieval.
* Updated snapshot data to reflect new session types and filtering
capabilities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 11:31:37 +00:00
Wu Yue
6e9487a9e1 feat(core): remove chat-panel component's dependency on doc (#12975)
Close [AI-259](https://linear.app/affine-design/issue/AI-259)
Close [AI-243](https://linear.app/affine-design/issue/AI-243)

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

* **New Features**
* Introduced a unified AI chat content component to manage and display
chat interactions.
* Added new chat block message components for improved chat message
rendering.

* **Refactor**
* Simplified and unified session management across all AI chat
components, now passing full session objects instead of session IDs.
* Updated component and property names for clarity and consistency
(e.g., chat message and block message components).
* Consolidated chat history and actions retrieval for a more streamlined
chat experience.
* Removed redundant session ID getters and replaced them with direct
session object usage.
* Streamlined chat panel and composer components by removing internal
message and context state management.

* **Bug Fixes**
* Improved handling of chat session state and loading, reducing
redundant state properties.
* Enhanced event handling to prevent errors when chat parameters are
missing.

* **Tests**
* Removed outdated chat clearing test cases to align with new chat state
management.

* **Chores**
* Updated import paths and reorganized module exports for better
maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 11:20:20 +00:00
Hwang
d49a069351 fix(web): adjust sign-in panel height (#12976)
before:


![image](https://github.com/user-attachments/assets/dde283fb-65af-413f-8610-a297da8968d6)


after:

![Electron 2025-07-01 15 02
53](https://github.com/user-attachments/assets/cf1b58d2-612c-4fc3-9e7e-56685b6668c3)

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

## Summary by CodeRabbit

* **Style**
* Updated the sign-in dialog to have a flexible height, allowing it to
adjust between 550 and 650 pixels for improved display across different
content sizes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 09:26:53 +00:00
EYHN
f600a1534a chore(ios): revert upgrade ci xcode version (#12978)
Reverts toeverything/AFFiNE#12977

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

## Summary by CodeRabbit

* **Chores**
  * Updated the iOS build environment to use Xcode version 16.2.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 09:12:56 +00:00
EYHN
eb0ee52706 chore(ios): upgrade ci xcode version (#12977)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
  * Updated the iOS build process to use Xcode version 16.4.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 08:00:52 +00:00
fengmk2
e8bc8f2d63 feat(server): add comment-attachment storage (#12911)
close CLOUD-230












#### PR Dependency Tree


* **PR #12911** 👈
  * **PR #12761**
    * **PR #12924**
      * **PR #12925**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Added support for uploading and retrieving comment attachments in
workspace documents via a new API endpoint.
* Introduced a service for managing comment attachments, including
storage, retrieval, deletion, and URL generation.
* Implemented localized error messages and improved error handling for
missing comment attachments.

* **Bug Fixes**
  * Improved error feedback when comment attachments are not found.

* **Tests**
* Added comprehensive tests for comment attachment storage, retrieval,
deletion, API endpoint behavior, and permission checks.

* **Documentation**
* Updated GraphQL schema and localization files to include new error
types for comment attachments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 07:04:52 +00:00
DarkSky
6e034185cf feat: title of session (#12971)
fix AI-253
2025-07-01 05:24:42 +00:00
Lakr
2be3f84196 fix: 🚑 compiler issue on newer syntax (#12974)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Updated method and function signatures to accept any type conforming
to the chat cell view model protocol, improving flexibility and
extensibility of chat cell configuration and height estimation.
  * Simplified internal logic for determining text color in chat cells.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 13:06:06 +08:00
EYHN
f46d288b1b fix(core): fix client crash (#12966)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved reliability when displaying OAuth provider icons by handling
cases where the provider may not be recognized, preventing potential
errors during authentication.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 03:38:13 +00:00
Lakr
9529adf33e chore: remove intelligent button from release (#12970)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The "Intelligents" button is now only shown in debug builds; it will
not appear in production versions.
* **Bug Fixes**
* Removed all references to the "Intelligents" plugin and related UI,
ensuring a cleaner production app experience.
* **Chores**
* Cleaned up project settings and removed redundant or empty
configuration entries.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 10:48:58 +00:00
Peng Xiao
03aeb44dc9 fix(editor): peekable conditions for edgeless note block (#12969)
fix AF-2704

#### PR Dependency Tree


* **PR #12969** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved peek behavior to allow peeking inside note blocks, even when
the hit target differs from the current model, as long as the current
model is contained within the note block. This enhances usability when
interacting with nested note blocks.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 07:13:02 +00:00
德布劳外 · 贾贵
c9aad0d55e refactor(core): open embedding settings when click check-status button (#12772)
> CLOSE BS-3582

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

- **New Features**
- Added a "Check status" button in the AI chat interface that opens the
embedding settings panel for improved user access.

- **Refactor**
- Simplified the embedding status tooltip to display static information
and a direct link to settings, removing dynamic progress updates.
- Integrated workspace dialog service across AI chat components for
consistent dialog management.

- **Tests**
- Updated end-to-end tests to verify that clicking the "Check status"
button opens the embedding settings panel.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 07:00:17 +00:00
Lakr
29ae6afe71 chore: created first ai stream (#12968)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a redesigned chat interface with new cell types for user,
assistant, system, loading, and error messages.
  * Added streaming chat responses and improved session management.
* Enhanced input box behavior, allowing sending messages with the return
key and inserting new lines via the edit menu.
* Added new GraphQL queries for fetching recent and latest chat
sessions.

* **Refactor**
* Replaced previous chat message and session management with a new, more
structured view model system.
* Updated chat view to use a custom table view component for better
message rendering and empty state handling.
* Simplified and improved error and loading state handling in the chat
UI.

* **Bug Fixes**
  * Improved error reporting and retry options for failed chat messages.
  * Fixed inconsistent property types for message and error identifiers.

* **Style**
* Updated UI components for chat cells with modern layouts and
consistent styling.

* **Chores**
  * Added a new package dependency for event streaming support.
* Renamed various internal properties and classes for clarity and
consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 06:51:00 +00:00
Cats Juice
32787bc88b fix(core): fix ai input style in chat block and simply img rendering (#12943)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Improved visual styling and cursor behavior for chat input send, stop,
and preference trigger buttons.
* Enhanced appearance and interactivity cues for the chat input
preference trigger.

* **Refactor**
* Simplified image preview grid by using CSS hover states for close
button visibility and switching to background images for previews.
* Streamlined image deletion process for a more intuitive user
experience.

* **Tests**
* Updated image upload test to wait for image container elements,
improving test reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 06:22:04 +00:00
EYHN
bbafce2c40 fix(ios): fix testflight (#12964)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated internal workflow configuration for iOS TestFlight uploads. No
impact on app features or user experience.
* Improved version handling to preserve full version strings for iOS
marketing versions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 06:06:21 +00:00
Peng Xiao
f7f69c3bc4 chore(core): do remove timeout for audio transcription job (#12965)
#### PR Dependency Tree


* **PR #12965** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **Bug Fixes**
* Improved request timeout handling to ensure timeouts are only set when
appropriate and provide clearer error messages.
* Updated audio transcription submission to wait indefinitely for
completion, preventing requests from being aborted due to timeouts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 06:00:53 +00:00
fengmk2
5599c39e97 feat(server): add read doc tool (#12811)
close AI-186















#### PR Dependency Tree


* **PR #12811** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

- **New Features**
- Added a new tool enabling users to read document content and metadata
within a workspace, with enforced access control.
- **Improvements**
- Updated tool interfaces and outputs to support the document reading
functionality seamlessly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 03:37:34 +00:00
EYHN
6b2639cbbb fix(ios): fix xcode marketing version (#12963)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved version handling to ensure only the main version number is
used, ignoring any suffixes after a hyphen when updating the iOS
marketing version.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 03:35:45 +00:00
Richard Lora
82b3c0d264 feat(core): add allowGuestDemoWorkspace flag to force login (#12779)
https://github.com/user-attachments/assets/41a659c9-6def-4492-be8e-5910eb148d6f

This PR enforces login‑first access (#8716) by disabling or enabling the
guest demo workspace via Admin Server Client Page and redirecting
unauthenticated users straight to `/sign‑in`.

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

* **New Features**
* Added a configuration option to control whether guest users can create
demo workspaces.
* Updated server and client interfaces, GraphQL schema, and queries to
support the new guest demo workspace flag.

* **Bug Fixes**
* Improved sign-out behavior to redirect users appropriately based on
guest demo workspace permissions.
* Enhanced navigation flow to handle guest demo workspace access and
user authentication state.

* **Tests**
* Added tests to verify sign-out logic when guest demo workspaces are
enabled or disabled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: liuyi <forehalo@gmail.com>
Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-06-29 14:17:18 +00:00
Wu Yue
a4680d236d fix(core): ai make it real ci timeout (#12954)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Replaced the boolean flag for selecting AI workflow endpoints with a
clear and flexible enum, enhancing clarity and maintainability for
AI-powered features.

* **Tests**
  * Simplified example text in AI action tests to improve consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-06-28 12:56:12 +00:00
github-actions[bot]
f88e1dffb6 chore(i18n): sync translations (#12604)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-06-28 09:00:17 +00:00
fengmk2
e773930256 feat(server): add comment-attachment model (#12909)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced support for comment attachments, allowing users to add,
view, and manage attachments linked to comments within documents.
* **Tests**
* Added comprehensive tests to ensure correct behavior for adding,
updating, deleting, listing, and retrieving comment attachments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #12909** 👈
  * **PR #12911**
    * **PR #12761**
      * **PR #12924**
        * **PR #12925**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-06-28 08:45:24 +00:00
DarkSky
1c1dade2d5 feat(server): add morph doc edit tool (#12789)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced support for the Morph provider in the copilot module,
enabling integration with the Morph LLM API.
- Added a new document editing tool that allows users to propose and
apply edits to existing documents via AI assistance.
- **Configuration**
- Added configuration options for the Morph provider in both backend and
admin interfaces.
- **Enhancements**
- Expanded tool support to include document editing capabilities within
the copilot feature set.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-28 08:27:08 +00:00
fengmk2
e2a799c70a feat(server): comment model (#12760)
close CLOUD-226

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

- **New Features**
- Introduced support for comments and replies within workspaces and
documents, enabling users to create, update, delete, and resolve
comments, as well as manage threaded replies.
- **Bug Fixes**
- Added user-friendly error messages and handling for situations where
comments or replies are not found.
- **Tests**
- Added comprehensive tests to ensure correct behavior of comment and
reply operations.
- **Localization**
  - Added English translations for new comment and reply error messages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->














#### PR Dependency Tree


* **PR #12760** 👈
  * **PR #12909**
    * **PR #12911**
      * **PR #12761**
        * **PR #12924**
          * **PR #12925**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-06-28 08:01:53 +00:00
DarkSky
9b881eb59a feat(server): faster reranking based on confidence (#12957)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved document reranking with a more streamlined and accurate
scoring system.
* Enhanced support for binary ("yes"/"no") document relevance judgments.

* **Improvements**
* Simplified user prompts and output formats for reranking tasks, making
results easier to interpret.
  * Increased reliability and consistency in document ranking results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-28 03:41:53 +00:00
DarkSky
e6f91cced6 feat(server): remove context prefetch & integrate context search (#12956)
fix AI-173
2025-06-27 23:45:49 +08:00
478 changed files with 21168 additions and 3643 deletions

View File

@@ -15,13 +15,7 @@ yarn affine cert --install
```bash
# certificates will be located at `./.docker/dev/certs/${domain}`
yarn affine cert --domain dev.affine.fail
yarn affine cert --domain affine.localhost
```
### 3. Enable dns and nginx service in compose.yml
### 4. Add custom dns server
```bash
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/dev.affine.fail
```
### 3. Enable nginx service in compose.yml

View File

@@ -73,17 +73,6 @@ services:
# timeout: 10s
# retries: 120
# dns:
# image: strm/dnsmasq
# volumes:
# - ./dnsmasq.conf:/etc/dnsmasq.d/local.conf
# ports:
# - "53:53/udp"
# cap_add:
# - NET_ADMIN
# depends_on:
# - nginx
# nginx:
# image: nginx:alpine
# volumes:

View File

@@ -1,2 +0,0 @@
log-queries
address=/dev.affine.fail/127.0.0.1

View File

@@ -565,6 +565,11 @@
"type": "boolean",
"description": "Only allow users with early access features to access the app\n@default false",
"default": false
},
"allowGuestDemoWorkspace": {
"type": "boolean",
"description": "Whether allow guest users to create demo workspaces.\n@default true",
"default": true
}
}
},
@@ -732,6 +737,11 @@
},
"default": {}
},
"providers.morph": {
"type": "object",
"description": "The config for the morph provider.\n@default {}",
"default": {}
},
"unsplash": {
"type": "object",
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",

View File

@@ -21,7 +21,7 @@ runs:
if [ "${{ github.ref_type }}" == "tag" ]; then
APP_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//')
else
APP_VERSION=$(date '+%Y.%-m.%-d-canary.%-H%-M')
APP_VERSION=$(date '+%Y.%-m.%-d-canary.%-H%M')
fi
if [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
BUILD_TYPE=stable

View File

@@ -7,7 +7,10 @@ COPY ./packages/frontend/apps/mobile/dist /app/static/mobile
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends openssl && \
apt-get install -y --no-install-recommends openssl libjemalloc2 && \
rm -rf /var/lib/apt/lists/*
# Enable jemalloc by preloading the library
ENV LD_PRELOAD=libjemalloc.so.2
CMD ["node", "./dist/main.js"]

View File

@@ -124,7 +124,6 @@ jobs:
package: 'affine_mobile_native'
no-build: 'true'
- name: Testflight
if: ${{ env.BUILD_TYPE != 'stable' }}
working-directory: packages/frontend/apps/ios/App
run: |
echo -n "${{ env.BUILD_PROVISION_PROFILE }}" | base64 --decode -o $PP_PATH
@@ -132,6 +131,7 @@ jobs:
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
fastlane beta
env:
BUILD_TARGET: distribution
BUILD_PROVISION_PROFILE: ${{ secrets.BUILD_PROVISION_PROFILE }}
PP_PATH: ${{ runner.temp }}/build_pp.mobileprovision
APPLE_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_STORE_CONNECT_API_KEY_ID }}

View File

@@ -48,6 +48,7 @@
"@blocksuite/affine-gfx-template": "workspace:*",
"@blocksuite/affine-gfx-text": "workspace:*",
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
"@blocksuite/affine-inline-comment": "workspace:*",
"@blocksuite/affine-inline-footnote": "workspace:*",
"@blocksuite/affine-inline-latex": "workspace:*",
"@blocksuite/affine-inline-link": "workspace:*",
@@ -173,6 +174,7 @@
"./inlines/footnote": "./src/inlines/footnote/index.ts",
"./inlines/footnote/view": "./src/inlines/footnote/view.ts",
"./inlines/footnote/store": "./src/inlines/footnote/store.ts",
"./inlines/comment": "./src/inlines/comment/index.ts",
"./inlines/latex": "./src/inlines/latex/index.ts",
"./inlines/latex/store": "./src/inlines/latex/store.ts",
"./inlines/latex/view": "./src/inlines/latex/view.ts",
@@ -283,6 +285,7 @@
"./sync": "./src/sync/index.ts",
"./extensions/store": "./src/extensions/store.ts",
"./extensions/view": "./src/extensions/view.ts",
"./foundation/clipboard": "./src/foundation/clipboard.ts",
"./foundation/store": "./src/foundation/store.ts",
"./foundation/view": "./src/foundation/view.ts"
},

View File

@@ -33,6 +33,7 @@ import { PointerViewExtension } from '@blocksuite/affine-gfx-pointer/view';
import { ShapeViewExtension } from '@blocksuite/affine-gfx-shape/view';
import { TemplateViewExtension } from '@blocksuite/affine-gfx-template/view';
import { TextViewExtension } from '@blocksuite/affine-gfx-text/view';
import { InlineCommentViewExtension } from '@blocksuite/affine-inline-comment/view';
import { FootnoteViewExtension } from '@blocksuite/affine-inline-footnote/view';
import { LatexViewExtension as InlineLatexViewExtension } from '@blocksuite/affine-inline-latex/view';
import { LinkViewExtension } from '@blocksuite/affine-inline-link/view';
@@ -95,6 +96,7 @@ export function getInternalViewExtensions() {
RootViewExtension,
// Inline
InlineCommentViewExtension,
FootnoteViewExtension,
LinkViewExtension,
ReferenceViewExtension,

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-foundation/clipboard';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-inline-comment';

View File

@@ -45,6 +45,7 @@
{ "path": "../gfx/template" },
{ "path": "../gfx/text" },
{ "path": "../gfx/turbo-renderer" },
{ "path": "../inlines/comment" },
{ "path": "../inlines/footnote" },
{ "path": "../inlines/latex" },
{ "path": "../inlines/link" },

View File

@@ -17,6 +17,7 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
BlockCommentManager,
CitationProvider,
DocModeProvider,
FileSizeLimitProvider,
@@ -92,6 +93,14 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.citationService.isCitationModel(this.model);
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
convertTo = () => {
return this.std
.get(AttachmentEmbedProvider)
@@ -499,6 +508,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
class=${classMap({
'affine-attachment-container': true,
focused: this.selected$.value,
'comment-highlighted': this.isCommentHighlighted,
})}
style=${this.containerStyleMap}
>

View File

@@ -10,6 +10,7 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarModuleConfig,
@@ -240,6 +241,10 @@ const builtinToolbarConfig = {
replaceAction,
downloadAction,
captionAction,
{
id: 'f.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -15,6 +15,10 @@ export const styles = css`
}
}
.affine-attachment-container.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-attachment-card {
display: flex;
gap: 12px;

View File

@@ -8,6 +8,7 @@ import type {
} from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import {
BlockCommentManager,
CitationProvider,
DocModeProvider,
LinkPreviewServiceIdentifier,
@@ -128,6 +129,14 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
return this.std.get(ImageProxyService);
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
handleClick = (event: MouseEvent) => {
event.stopPropagation();

View File

@@ -45,6 +45,7 @@ export class BookmarkCard extends SignalWatcher(
[style]: true,
selected: this.bookmark.selected$.value,
edgeless: isGfxBlockComponent(this.bookmark),
'comment-highlighted': this.bookmark.isCommentHighlighted,
});
const domainName = url.match(

View File

@@ -17,6 +17,7 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
EmbedIframeService,
EmbedOptionProvider,
type LinkEventType,
@@ -288,6 +289,10 @@ const builtinToolbarConfig = {
},
} satisfies ToolbarActionGroup<ToolbarAction>,
captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -1,4 +1,4 @@
import { unsafeCSSVar } from '@blocksuite/affine-shared/theme';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { baseTheme } from '@toeverything/theme';
import { css, unsafeCSS } from 'lit';
@@ -158,6 +158,10 @@ export const styles = css`
border-radius: 4px;
}
.affine-bookmark-card.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-bookmark-card.loading {
.affine-bookmark-content-title-text {
color: var(--affine-placeholder-color);

View File

@@ -13,6 +13,7 @@
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
"@blocksuite/affine-inline-comment": "workspace:*",
"@blocksuite/affine-inline-latex": "workspace:*",
"@blocksuite/affine-inline-link": "workspace:*",
"@blocksuite/affine-inline-preset": "workspace:*",

View File

@@ -1,3 +1,4 @@
import { CommentInlineSpecExtension } from '@blocksuite/affine-inline-comment';
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
import {
@@ -20,7 +21,9 @@ import { z } from 'zod';
export const CodeBlockUnitSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'code-block-unit',
schema: z.undefined(),
schema: z.object({
'code-block-uint': z.undefined(),
}),
match: () => true,
renderer: ({ delta }) => {
return html`<affine-code-unit .delta=${delta}></affine-code-unit>`;
@@ -42,5 +45,6 @@ export const CodeBlockInlineManagerExtension =
LatexInlineSpecExtension.identifier,
LinkInlineSpecExtension.identifier,
CodeBlockUnitSpecExtension.identifier,
CommentInlineSpecExtension.identifier,
],
});

View File

@@ -6,6 +6,7 @@ import {
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
DocModeProvider,
NotificationProvider,
} from '@blocksuite/affine-shared/services';
@@ -390,6 +391,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
});
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
override async getUpdateComplete() {
const result = await super.getUpdateComplete();
await this._richTextElement?.updateComplete;
@@ -413,6 +422,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
<div
class=${classMap({
'affine-code-block-container': true,
'highlight-comment': this.isCommentHighlighted,
mobile: IS_MOBILE,
wrap: this.model.props.wrap,
'disable-line-numbers': !showLineNumbers,

View File

@@ -7,9 +7,10 @@ import {
WrapIcon,
} from '@blocksuite/affine-components/icons';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { CommentProviderIdentifier } from '@blocksuite/affine-shared/services';
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
import { noop, sleep } from '@blocksuite/global/utils';
import { NumberedListIcon } from '@blocksuite/icons/lit';
import { CommentIcon, NumberedListIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
@@ -113,6 +114,47 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
};
},
},
{
type: 'comment',
label: 'Comment',
tooltip: 'Comment',
icon: CommentIcon({
width: '20',
height: '20',
}),
when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),
generate: ({ blockComponent }) => {
return {
action: () => {
const commentProvider = blockComponent.std.getOptional(
CommentProviderIdentifier
);
if (!commentProvider) return;
commentProvider.addComment([
new BlockSelection({
blockId: blockComponent.model.id,
}),
]);
},
render: item =>
html`<editor-icon-button
class="code-toolbar-button comment"
aria-label=${ifDefined(item.label)}
.tooltip=${item.label}
.tooltipOffset=${4}
.iconSize=${'16px'}
.iconContainerPadding=${4}
@click=${(e: MouseEvent) => {
e.stopPropagation();
item.action();
}}
>
${item.icon}
</editor-icon-button>`,
};
},
},
],
},
];

View File

@@ -2,7 +2,9 @@ export * from './adapters';
export * from './clipboard';
export * from './code-block';
export * from './code-block-config';
export * from './code-block-service';
export * from './code-preview-extension';
export * from './code-toolbar';
export * from './highlight/const';
export * from './turbo/code-layout-handler';
export * from './turbo/code-painter.worker';

View File

@@ -1,4 +1,5 @@
import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit';
export const codeBlockStyles = css`
@@ -20,6 +21,10 @@ export const codeBlockStyles = css`
padding: 12px;
}
.affine-code-block-container.highlight-comment {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
${scrollbarStyle('.affine-code-block-container rich-text')}
.affine-code-block-container .inline-editor {

View File

@@ -10,6 +10,7 @@
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../gfx/turbo-renderer" },
{ "path": "../../inlines/comment" },
{ "path": "../../inlines/latex" },
{ "path": "../../inlines/link" },
{ "path": "../../inlines/preset" },

View File

@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import type { DataViewUILogicBase } from '@blocksuite/data-view';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
@@ -72,6 +73,12 @@ export class DatabaseTitle extends SignalWatcher(
.affine-database-title [data-title-focus='true']::before {
color: var(--affine-placeholder-color);
}
.affine-database-title.comment-highlighted {
border-bottom: 2px solid
${unsafeCSSVarV2('block/comment/highlightUnderline')};
background-color: ${unsafeCSSVarV2('block/comment/highlightActive')};
}
`;
private readonly compositionEnd = () => {
@@ -134,6 +141,7 @@ export class DatabaseTitle extends SignalWatcher(
const classList = classMap({
'affine-database-title': true,
ellipsis: !this.isFocus$.value,
'comment-highlighted': this.database?.isCommentHighlighted ?? false,
});
const untitledStyle = styleMap({
height: isEmpty ? 'auto' : 0,

View File

@@ -10,6 +10,8 @@ import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
CommentProviderIdentifier,
DocModeProvider,
NotificationProvider,
type TelemetryEventMap,
@@ -34,11 +36,12 @@ import {
import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { Rect } from '@blocksuite/global/gfx';
import {
CommentIcon,
CopyIcon,
DeleteIcon,
MoreHorizontalIcon,
} from '@blocksuite/icons/lit';
import { type BlockComponent } from '@blocksuite/std';
import { type BlockComponent, BlockSelection } from '@blocksuite/std';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import { Slice } from '@blocksuite/store';
import { autoUpdate } from '@floating-ui/dom';
@@ -82,6 +85,18 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
);
},
}),
menu.action({
prefix: CommentIcon(),
name: 'Comment',
hide: () => !this.std.getOptional(CommentProviderIdentifier),
select: () => {
this.std.getOptional(CommentProviderIdentifier)?.addComment([
new BlockSelection({
blockId: this.blockId,
}),
]);
},
}),
menu.action({
prefix: CopyIcon(),
name: 'Copy',
@@ -297,6 +312,14 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
};
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(

View File

@@ -70,7 +70,7 @@ function toggleStyle(
return [k, v];
}
})
);
) as AffineTextAttributes;
inlineEditor.formatText(inlineRange, newAttributes, {
mode: 'merge',

View File

@@ -11,6 +11,7 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
DocDisplayMetaProvider,
EditorSettingProvider,
type LinkEventType,
@@ -305,6 +306,10 @@ const builtinToolbarConfig = {
},
} satisfies ToolbarActionGroup<ToolbarAction>,
captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -338,6 +338,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
'note-empty': this.isNoteContentEmpty,
'in-canvas': inCanvas,
[this._cardStyle]: true,
'comment-highlighted': this.isCommentHighlighted,
});
const theme = this.std.get(ThemeProvider).theme;

View File

@@ -5,3 +5,4 @@ export * from './edgeless-clipboard-config';
export * from './embed-edgeless-linked-doc-block';
export * from './embed-linked-doc-block';
export * from './embed-linked-doc-spec';
export { getEmbedLinkedDocIcons } from './utils';

View File

@@ -15,6 +15,10 @@ export const styles = css`
position: relative;
}
.affine-embed-linked-doc-block.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-embed-linked-doc-block.in-canvas {
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
background: ${unsafeCSSVarV2('layer/background/linkedDocOnEdgeless')};
@@ -164,6 +168,7 @@ export const styles = css`
.affine-embed-linked-doc-banner {
margin: 12px 12px 0px 0px;
width: 204px;
min-width: 204px;
max-width: 100%;
height: 102px;
pointer-events: none;

View File

@@ -16,6 +16,7 @@ import {
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
EditorSettingProvider,
type LinkEventType,
type OpenDocMode,
@@ -225,6 +226,10 @@ const builtinToolbarConfig = {
openDocActionGroup,
conversionsActionGroup,
captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -232,6 +232,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
surface: false,
selected: this.selected$.value,
'show-hover-border': true,
'comment-highlighted': this.isCommentHighlighted,
})}
@click=${this._handleClick}
style=${containerStyleMap}

View File

@@ -57,6 +57,9 @@ export const blockStyles = css`
border-radius: 8px;
overflow: hidden;
}
.affine-embed-synced-doc-container.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-embed-synced-doc-container.show-hover-border:hover {
border-color: var(--affine-border-color);
}

View File

@@ -2,17 +2,20 @@ import {
CaptionedBlockComponent,
SelectedStyle,
} from '@blocksuite/affine-components/caption';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import type { EmbedCardStyle, EmbedProps } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_MIN_WIDTH,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
BlockCommentManager,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { findAncestorModel } from '@blocksuite/affine-shared/utils';
import type { BlockService } from '@blocksuite/std';
import {
type GfxCompatibleProps,
GfxViewInteractionExtension,
type ResizeConstraint,
} from '@blocksuite/std/gfx';
@@ -25,7 +28,7 @@ import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
export class EmbedBlockComponent<
Model extends BlockModel<GfxCompatibleProps> = BlockModel<GfxCompatibleProps>,
Model extends BlockModel<EmbedProps> = BlockModel<EmbedProps>,
Service extends BlockService = BlockService,
WidgetName extends string = string,
> extends CaptionedBlockComponent<Model, Service, WidgetName> {
@@ -59,6 +62,14 @@ export class EmbedBlockComponent<
*/
protected embedContainerStyle: StyleInfo = {};
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
renderEmbed = (content: () => TemplateResult) => {
if (
this._cardStyle === 'horizontal' ||
@@ -90,6 +101,11 @@ export class EmbedBlockComponent<
style=${styleMap({
height: `${this._cardHeight}px`,
width: '100%',
...(this.isCommentHighlighted
? {
border: `2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')}`,
}
: {}),
...this.embedContainerStyle,
})}
>

View File

@@ -57,6 +57,11 @@ export const embedNoteContentStyles = css`
font-weight: 600;
}
.affine-embed-doc-content-note-blocks inline-comment {
background-color: unset !important;
border-bottom: unset !important;
}
.affine-embed-linked-doc-block.horizontal {
affine-paragraph,
affine-list {

View File

@@ -1,4 +1,5 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { EmbedProps } from '@blocksuite/affine-model';
import { Bound } from '@blocksuite/global/gfx';
import {
blockComponentSymbol,
@@ -7,16 +8,13 @@ import {
GfxElementSymbol,
toGfxBlockComponent,
} from '@blocksuite/std';
import type {
GfxBlockElementModel,
GfxCompatibleProps,
} from '@blocksuite/std/gfx';
import type { GfxBlockElementModel } from '@blocksuite/std/gfx';
import type { StyleInfo } from 'lit/directives/style-map.js';
import type { EmbedBlockComponent } from './embed-block-element.js';
export function toEdgelessEmbedBlock<
Model extends GfxBlockElementModel<GfxCompatibleProps>,
Model extends GfxBlockElementModel<EmbedProps>,
Service extends BlockService,
WidgetName extends string,
B extends typeof EmbedBlockComponent<Model, Service, WidgetName>,

View File

@@ -13,6 +13,7 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
EmbedOptionProvider,
type LinkEventType,
type ToolbarAction,
@@ -348,6 +349,10 @@ function createBuiltinToolbarConfigForExternal(
});
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -18,6 +18,7 @@ import type { BaseSelection } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
@@ -76,6 +77,10 @@ export class ImageBlockPageComponent extends SignalWatcher(
width: 100%;
height: 100%;
}
affine-page-image .comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
`;
resizeable$ = computed(() => this.block.resizeable$.value);
@@ -364,7 +369,13 @@ export class ImageBlockPageComponent extends SignalWatcher(
const { loading, error, icon, description, needUpload } = this.state;
return html`
<div class="resizable-img" style=${styleMap(imageSize)}>
<div
class=${classMap({
'resizable-img': true,
'comment-highlighted': this.block.isCommentHighlighted,
})}
style=${styleMap(imageSize)}
>
<img
class="drag-target"
draggable="false"

View File

@@ -1,6 +1,7 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
@@ -49,6 +50,10 @@ const builtinToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -141,6 +146,10 @@ const builtinSurfaceToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
],
when: ctx => ctx.getSurfaceModelsByType(ImageBlockModel).length === 1,

View File

@@ -5,7 +5,10 @@ import { Peekable } from '@blocksuite/affine-components/peek';
import { ResourceController } from '@blocksuite/affine-components/resource';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { ImageSelection } from '@blocksuite/affine-shared/selection';
import { ToolbarRegistryIdentifier } from '@blocksuite/affine-shared/services';
import {
BlockCommentManager,
ToolbarRegistryIdentifier,
} from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
@@ -65,6 +68,14 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
return this.pageImage?.resizeImg;
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
private _handleClick(event: MouseEvent) {
// the peek view need handle shift + click
if (event.defaultPrevented) return;

View File

@@ -8,6 +8,7 @@ import {
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
CitationProvider,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
@@ -107,6 +108,14 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
);
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
@@ -268,7 +277,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
}
</style>
<div
class="affine-paragraph-block-container"
class=${classMap({
'affine-paragraph-block-container': true,
'highlight-comment': this.isCommentHighlighted,
})}
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
>
<div

View File

@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit';
export const paragraphBlockStyles = css`
@@ -15,6 +16,11 @@ export const paragraphBlockStyles = css`
position: relative;
}
.affine-paragraph-block-container.highlight-comment {
background-color: ${unsafeCSSVarV2('block/comment/highlightActive')};
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
affine-paragraph code {
font-size: calc(var(--affine-font-base) - 3px);
padding: 0px 4px 2px;

View File

@@ -9,6 +9,7 @@ import {
promptDocTitle,
} from '@blocksuite/affine-block-embed';
import { updateBlockType } from '@blocksuite/affine-block-note';
import type { HighlightType } from '@blocksuite/affine-components/highlight-dropdown-menu';
import { toast } from '@blocksuite/affine-components/toast';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import {
@@ -19,6 +20,10 @@ import {
isFormatSupported,
textFormatConfigs,
} from '@blocksuite/affine-inline-preset';
import {
EmbedLinkedDocBlockSchema,
EmbedSyncedDocBlockSchema,
} from '@blocksuite/affine-model';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
copySelectedModelsCommand,
@@ -37,8 +42,10 @@ import type {
ToolbarActionGroup,
ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { ActionPlacement } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import {
ActionPlacement,
blockCommentToolbarButton,
} from '@blocksuite/affine-shared/services';
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import {
CopyIcon,
@@ -47,7 +54,11 @@ import {
DuplicateIcon,
LinkedPageIcon,
} from '@blocksuite/icons/lit';
import { type BlockComponent, BlockSelection } from '@blocksuite/std';
import {
type BlockComponent,
BlockSelection,
BlockViewIdentifier,
} from '@blocksuite/std';
import { toDraftModel } from '@blocksuite/store';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
@@ -140,7 +151,7 @@ const highlightActionGroup = {
id: 'c.highlight',
when: ({ chain }) => isFormatSupported(chain).run()[0],
content({ chain }) {
const updateHighlight = (styles: AffineTextAttributes) => {
const updateHighlight = (styles: HighlightType) => {
const payload = { styles };
chain
.try(chain => [
@@ -161,7 +172,7 @@ const highlightActionGroup = {
} as const satisfies ToolbarAction;
const turnIntoDatabase = {
id: 'd.convert-to-database',
id: 'e.convert-to-database',
tooltip: 'Create Table',
icon: DatabaseTableViewIcon(),
when({ chain }) {
@@ -208,10 +219,21 @@ const turnIntoDatabase = {
} as const satisfies ToolbarAction;
const turnIntoLinkedDoc = {
id: 'e.convert-to-linked-doc',
id: 'f.convert-to-linked-doc',
tooltip: 'Create Linked Doc',
icon: LinkedPageIcon(),
when({ chain }) {
when({ chain, std }) {
const supportFlavours = [
EmbedLinkedDocBlockSchema,
EmbedSyncedDocBlockSchema,
].map(schema => schema.model.flavour);
if (
supportFlavours.some(
flavour => !std.getOptional(BlockViewIdentifier(flavour))
)
)
return false;
const [ok, { selectedModels }] = chain
.pipe(getSelectedModelsCommand, {
types: ['block', 'text'],
@@ -273,6 +295,10 @@ export const builtinToolbarConfig = {
highlightActionGroup,
turnIntoDatabase,
turnIntoLinkedDoc,
{
id: 'g.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -5,6 +5,7 @@ import {
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -61,6 +62,10 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show();
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,

View File

@@ -13,6 +13,7 @@ import {
type SurfaceRefBlockModel,
} from '@blocksuite/affine-model';
import {
BlockCommentManager,
DocModeProvider,
EditPropsStore,
type OpenDocMode,
@@ -76,6 +77,10 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')};
}
.affine-surface-ref.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
@media print {
.affine-surface-ref {
outline: none !important;
@@ -137,6 +142,14 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
return this._referencedModel;
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
private readonly _handleClick = () => {
this.selection.update(() => {
return [this.selection.create(BlockSelection, { blockId: this.blockId })];
@@ -456,6 +469,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
class=${classMap({
'affine-surface-ref': true,
focused: this.selected$.value,
'comment-highlighted': this.isCommentHighlighted,
})}
@click=${this._handleClick}
>

View File

@@ -1,4 +1,4 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
import { PropTypes, requiredProperties } from '@blocksuite/std';
import { LitElement } from 'lit';
import { property } from 'lit/decorators.js';
@@ -20,7 +20,10 @@ const colors = [
'grey',
] as const;
type HighlightType = 'color' | 'background';
export type HighlightType = Pick<
AffineTextStyleAttributes,
'color' | 'background'
>;
// TODO(@fundon): these recent settings should be added to the dropdown menu
// tests/blocksutie/e2e/format-bar.spec.ts#253
@@ -33,13 +36,13 @@ type HighlightType = 'color' | 'background';
})
export class HighlightDropdownMenu extends LitElement {
@property({ attribute: false })
accessor updateHighlight!: (styles: AffineTextAttributes) => void;
accessor updateHighlight!: (styles: HighlightType) => void;
private readonly _update = (value: string | null, type: HighlightType) => {
private readonly _update = (style: HighlightType) => {
// latestHighlightColor = value;
// latestHighlightType = type;
this.updateHighlight({ [`${type}`]: value });
this.updateHighlight(style);
};
override render() {
@@ -71,7 +74,7 @@ export class HighlightDropdownMenu extends LitElement {
return html`
<editor-menu-action
data-testid="foreground-${color}"
@click=${() => this._update(value, 'color')}
@click=${() => this._update({ color: value })}
>
<affine-text-duotone-icon
style=${styleMap({
@@ -92,7 +95,7 @@ export class HighlightDropdownMenu extends LitElement {
return html`
<editor-menu-action
data-testid="background-${color}"
@click=${() => this._update(value, 'background')}
@click=${() => this._update({ background: value })}
>
<affine-text-duotone-icon
style=${styleMap({

View File

@@ -1,7 +1,14 @@
import { NoteBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { isInsideEdgelessEditor } from '@blocksuite/affine-shared/utils';
import {
isInsideEdgelessEditor,
matchModels,
} from '@blocksuite/affine-shared/utils';
import type { Constructor } from '@blocksuite/global/utils';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import {
GfxBlockElementModel,
GfxControllerIdentifier,
} from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { LitElement, TemplateResult } from 'lit';
@@ -72,6 +79,20 @@ export const Peekable =
);
if (hitTarget && hitTarget !== model) {
// Check if hitTarget is a GfxBlockElementModel (which extends BlockModel)
// and if it's a NoteBlockModel, then check if current model is inside it
if (
hitTarget instanceof GfxBlockElementModel &&
matchModels(hitTarget, [NoteBlockModel])
) {
let curModel: BlockModel | null = model;
while (curModel) {
if (curModel === hitTarget) {
return true; // Model is inside the NoteBlockModel, allow peek
}
curModel = curModel.parent;
}
}
return false;
}

View File

@@ -190,7 +190,10 @@ export class Tooltip extends LitElement {
middleware: [
this.autoFlip && flip({ padding: AUTO_FLIP_PADDING }),
this.autoShift && shift({ padding: AUTO_SHIFT_PADDING }),
offset((this.arrow ? TRIANGLE_HEIGHT : 0) + this.offset),
offset({
mainAxis: (this.arrow ? TRIANGLE_HEIGHT : 0) + this.offsetY,
crossAxis: this.offsetX,
}),
arrow({
element: portalRoot.shadowRoot!.querySelector('.arrow')!,
}),
@@ -264,7 +267,7 @@ export class Tooltip extends LitElement {
* Show a triangle arrow pointing to the reference element.
*/
@property({ attribute: false })
accessor arrow = true;
accessor arrow = false;
/**
* changes the placement of the floating element in order to keep it in view,
@@ -303,7 +306,10 @@ export class Tooltip extends LitElement {
* See https://floating-ui.com/docs/offset
*/
@property({ attribute: false })
accessor offset = 4;
accessor offsetY = 6;
@property({ attribute: false })
accessor offsetX = 0;
@property({ attribute: 'tip-position' })
accessor placement: Placement = 'top';

View File

@@ -32,6 +32,7 @@
},
"exports": {
".": "./src/index.ts",
"./clipboard": "./src/clipboard.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"
},

View File

@@ -43,7 +43,7 @@ const imageClipboardConfigs = [
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
export const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,

View File

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

View File

@@ -0,0 +1,46 @@
{
"name": "@blocksuite/affine-inline-comment",
"description": "Inline comment for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lit-html": "^3.2.1",
"lodash-es": "^4.17.21",
"rxjs": "^7.8.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "3.1.3"
},
"exports": {
".": "./src/index.ts",
"./view": "./src/view.ts",
"./store": "./src/store.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.21.0"
}

View File

@@ -0,0 +1,11 @@
import { InlineComment } from './inline-comment';
export function effects() {
customElements.define('inline-comment', InlineComment);
}
declare global {
interface HTMLElementTagNameMap {
'inline-comment': InlineComment;
}
}

View File

@@ -0,0 +1,2 @@
export * from './inline-spec';
export * from './utils';

View File

@@ -0,0 +1,218 @@
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
import {
BlockCommentManager,
type CommentId,
CommentProviderIdentifier,
findAllCommentedBlocks,
} from '@blocksuite/affine-shared/services';
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
LifeCycleWatcher,
type TextRangePoint,
TextSelection,
} from '@blocksuite/std';
import type { BaseSelection, BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import difference from 'lodash-es/difference';
import {
extractCommentIdFromDelta,
findAllCommentedTexts,
findCommentedTexts,
} from './utils';
export class InlineCommentManager extends LifeCycleWatcher {
static override key = 'inline-comment-manager';
private readonly _disposables = new DisposableGroup();
private readonly _highlightedCommentId$ = signal<CommentId | null>(null);
private get _provider() {
return this.std.getOptional(CommentProviderIdentifier);
}
override mounted() {
const provider = this._provider;
if (!provider) return;
this._init().catch(console.error);
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(this._handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentResolved(this._handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment)
);
this._disposables.add(
this.std.selection.slots.changed.subscribe(this._handleSelectionChanged)
);
}
override unmounted() {
this._disposables.dispose();
}
private async _init() {
const provider = this._provider;
if (!provider) return;
const commentsInProvider = await provider.getComments('unresolved');
const inlineComments = [...findAllCommentedTexts(this.std.store).values()];
const blockComments = findAllCommentedBlocks(this.std.store).flatMap(
block => Object.keys(block.props.comments)
);
const commentsInEditor = [
...new Set([...inlineComments, ...blockComments]),
];
// resolve comments that are in provider but not in editor
// which means the commented content may be deleted
difference(commentsInProvider, commentsInEditor).forEach(comment => {
provider.resolveComment(comment);
});
// remove comments that are in editor but not in provider
// which means the comment may be removed or resolved in provider side
difference(commentsInEditor, commentsInProvider).forEach(comment => {
this._handleDeleteAndResolve(comment);
this.std.get(BlockCommentManager).handleDeleteAndResolve(comment);
});
}
private readonly _handleAddComment = (
id: CommentId,
selections: BaseSelection[]
) => {
const needCommentTexts = selections
.map(selection => {
if (!selection.is(TextSelection)) return [];
const [_, { selectedBlocks }] = this.std.command
.chain()
.pipe(getSelectedBlocksCommand, {
textSelection: selection,
})
.run();
if (!selectedBlocks) return [];
type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>;
};
return selectedBlocks
.map(
({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const
)
.filter(
(
pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1]
)
.map(([model, inlineEditor]) => {
let from: TextRangePoint;
let to: TextRangePoint | null;
if (model.id === selection.from.blockId) {
from = selection.from;
to = null;
} else if (model.id === selection.to?.blockId) {
from = selection.to;
to = null;
} else {
from = {
blockId: model.id,
index: 0,
length: model.text.yText.length,
};
to = null;
}
return [new TextSelection({ from, to }), inlineEditor] as const;
});
})
.flat();
if (needCommentTexts.length === 0) return;
needCommentTexts.forEach(([selection, inlineEditor]) => {
inlineEditor.formatText(
selection.from,
{
[`comment-${id}`]: true,
},
{
withoutTransact: true,
}
);
});
};
private readonly _handleDeleteAndResolve = (id: CommentId) => {
const commentedTexts = findCommentedTexts(this.std.store, id);
if (commentedTexts.length === 0) return;
this.std.store.withoutTransact(() => {
commentedTexts.forEach(selection => {
const inlineEditor = getInlineEditorByModel(
this.std,
selection.from.blockId
);
inlineEditor?.formatText(
selection.from,
{
[`comment-${id}`]: null,
},
{
withoutTransact: true,
}
);
});
});
};
private readonly _handleHighlightComment = (id: CommentId | null) => {
this._highlightedCommentId$.value = id;
};
private readonly _handleSelectionChanged = (selections: BaseSelection[]) => {
const currentHighlightedCommentId = this._highlightedCommentId$.peek();
if (selections.length === 1) {
const selection = selections[0];
// InlineCommentManager only handle text selection
if (!selection.is(TextSelection)) return;
if (!selection.isCollapsed() && currentHighlightedCommentId !== null) {
this._provider?.highlightComment(null);
return;
}
const model = this.std.store.getModelById(selection.from.blockId);
if (!model) return;
const inlineEditor = getInlineEditorByModel(this.std, model);
if (!inlineEditor) return;
const delta = inlineEditor.getDeltaByRangeIndex(selection.from.index);
if (!delta) return;
const commentIds = extractCommentIdFromDelta(delta);
if (commentIds.length !== 0) return;
}
if (currentHighlightedCommentId !== null) {
this._provider?.highlightComment(null);
}
};
}

View File

@@ -0,0 +1,97 @@
import {
type CommentId,
CommentProviderIdentifier,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import {
type BlockStdScope,
PropTypes,
requiredProperties,
ShadowlessElement,
stdContext,
} from '@blocksuite/std';
import { consume } from '@lit/context';
import { css, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
import { html } from 'lit-html';
import { isEqual } from 'lodash-es';
@requiredProperties({
commentIds: PropTypes.arrayOf(id => typeof id === 'string'),
})
export class InlineComment extends WithDisposable(ShadowlessElement) {
static override styles = css`
inline-comment {
display: inline-block;
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
border-bottom: 2px solid
${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
inline-comment.highlighted {
background-color: ${unsafeCSSVarV2('block/comment/highlightActive')};
}
`;
@property({
attribute: false,
hasChanged: (newVal: string[], oldVal: string[]) =>
!isEqual(newVal, oldVal),
})
accessor commentIds!: string[];
private _index: number = 0;
@consume({ context: stdContext })
private accessor _std!: BlockStdScope;
@state()
accessor highlighted = false;
private get _provider() {
return this._std.getOptional(CommentProviderIdentifier);
}
private readonly _handleClick = () => {
this._provider?.highlightComment(this.commentIds[this._index]);
this._index = (this._index + 1) % this.commentIds.length;
};
private readonly _handleHighlight = (id: CommentId | null) => {
if (this.highlighted) {
if (!id || !this.commentIds.includes(id)) {
this.highlighted = false;
}
} else {
if (id && this.commentIds.includes(id)) {
this.highlighted = true;
}
}
};
override connectedCallback() {
super.connectedCallback();
const provider = this._provider;
if (provider) {
this.disposables.addFromEvent(this, 'click', this._handleClick);
this.disposables.add(
provider.onCommentHighlighted(this._handleHighlight)
);
}
}
override willUpdate(_changedProperties: PropertyValues<this>) {
if (_changedProperties.has('highlighted')) {
if (this.highlighted) {
this.classList.add('highlighted');
} else {
this.classList.remove('highlighted');
}
}
}
override render() {
return html`<slot></slot>`;
}
}

View File

@@ -0,0 +1,38 @@
import { type CommentId } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { dynamicSchema, InlineSpecExtension } from '@blocksuite/std/inline';
import { html, nothing } from 'lit-html';
import { when } from 'lit-html/directives/when.js';
import { z } from 'zod';
import { extractCommentIdFromDelta } from './utils';
type InlineCommendId = `comment-${CommentId}`;
function isInlineCommendId(key: string): key is InlineCommendId {
return key.startsWith('comment-');
}
export const CommentInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'comment',
schema: dynamicSchema(
isInlineCommendId,
z.boolean().optional().nullable().catch(undefined)
),
match: delta => {
if (!delta.attributes) return false;
const comments = Object.entries(delta.attributes).filter(
([key, value]) => isInlineCommendId(key) && value === true
);
return comments.length > 0;
},
renderer: ({ delta, children }) =>
html`<inline-comment .commentIds=${extractCommentIdFromDelta(delta)}
>${when(
children,
() => html`${children}`,
() => nothing
)}</inline-comment
>`,
wrapper: true,
});

View File

@@ -0,0 +1,64 @@
import type { CommentId } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { TextSelection } from '@blocksuite/std';
import type { DeltaInsert, Store } from '@blocksuite/store';
export function findAllCommentedTexts(
store: Store
): Map<TextSelection, CommentId> {
const result = new Map<TextSelection, CommentId>();
store.getAllModels().forEach(model => {
if (!model.text) return;
let index = 0;
model.text.toDelta().forEach(delta => {
if (!delta.insert) return;
const length = delta.insert.length;
if (!delta.attributes) {
index += length;
return;
}
Object.keys(delta.attributes)
.filter(key => key.startsWith('comment-'))
.forEach(key => {
const commentId = key.replace('comment-', '');
const selection = new TextSelection({
from: {
blockId: model.id,
index,
length,
},
to: null,
});
result.set(selection, commentId);
});
index += length;
});
});
return result;
}
export function findCommentedTexts(
store: Store,
commentId: CommentId
): TextSelection[] {
return [...findAllCommentedTexts(store).entries()]
.filter(([_, id]) => id === commentId)
.map(([selection]) => selection);
}
export function extractCommentIdFromDelta(
delta: DeltaInsert<AffineTextAttributes>
) {
if (!delta.attributes) return [];
return Object.keys(delta.attributes)
.filter(key => key.startsWith('comment-'))
.map(key => key.replace('comment-', ''));
}

View File

@@ -0,0 +1,22 @@
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import { effects } from './effects';
import { InlineCommentManager } from './inline-comment-manager';
import { CommentInlineSpecExtension } from './inline-spec';
export class InlineCommentViewExtension extends ViewExtensionProvider {
override name = 'affine-inline-comment';
override effect(): void {
super.effect();
effects();
}
override setup(context: ViewExtensionContext) {
super.setup(context);
context.register([CommentInlineSpecExtension, InlineCommentManager]);
}
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src"],
"references": [
{ "path": "../../ext-loader" },
{ "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/std" },
{ "path": "../../../framework/store" }
]
}

View File

@@ -3,6 +3,7 @@ import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { StdIdentifier } from '@blocksuite/std';
import { InlineSpecExtension } from '@blocksuite/std/inline';
import { html } from 'lit';
import z from 'zod';
import { FootNoteNodeConfigIdentifier } from './footnote-node/footnote-config';
@@ -13,7 +14,9 @@ export const FootNoteInlineSpecExtension =
provider.getOptional(FootNoteNodeConfigIdentifier) ?? undefined;
return {
name: 'footnote',
schema: FootNoteSchema.optional().nullable().catch(undefined),
schema: z.object({
footnote: FootNoteSchema.optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.footnote;
},

View File

@@ -9,7 +9,9 @@ export const LatexInlineSpecExtension =
const std = provider.get(StdIdentifier);
return {
name: 'latex',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
latex: z.string().optional().nullable().catch(undefined),
}),
match: delta => typeof delta.attributes?.latex === 'string',
renderer: ({ delta, selected, editor, startOffset, endOffset }) => {
return html`<affine-latex-node
@@ -28,7 +30,9 @@ export const LatexInlineSpecExtension =
export const LatexEditorUnitSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'latex-editor-unit',
schema: z.undefined(),
schema: z.object({
'latex-editor-unit': z.undefined(),
}),
match: () => true,
renderer: ({ delta }) => {
return html`<latex-editor-unit .delta=${delta}></latex-editor-unit>`;

View File

@@ -9,7 +9,9 @@ export const LinkInlineSpecExtension =
const std = provider.get(StdIdentifier);
return {
name: 'link',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
link: z.string().optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.link;
},

View File

@@ -23,7 +23,7 @@ export class AffineMention extends SignalWatcher(
'clig' off;
/* Client/baseMedium */
font-family: Inter;
font-size: 15px;
font-size: var(--affine-font-size-base);
font-style: normal;
font-weight: 500;
line-height: 24px; /* 160% */

View File

@@ -9,14 +9,16 @@ export const MentionInlineSpecExtension =
const std = provider.get(StdIdentifier);
return {
name: 'mention',
schema: z
.object({
member: z.string(),
notification: z.string().optional(),
})
.optional()
.nullable()
.catch(undefined),
schema: z.object({
mention: z
.object({
member: z.string(),
notification: z.string().optional(),
})
.optional()
.nullable()
.catch(undefined),
}),
match: delta => {
return !!delta.attributes?.mention?.member;
},

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-inline-comment": "workspace:*",
"@blocksuite/affine-inline-footnote": "workspace:*",
"@blocksuite/affine-inline-latex": "workspace:*",
"@blocksuite/affine-inline-link": "workspace:*",

View File

@@ -11,7 +11,7 @@ import { type EditorHost, TextSelection } from '@blocksuite/std';
import type { TemplateResult } from 'lit';
import {
isTextStyleActive,
isTextAttributeActive,
toggleBold,
toggleCode,
toggleItalic,
@@ -38,7 +38,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => {
const [result] = host.std.command
.chain()
.pipe(isTextStyleActive, { key: 'bold' })
.pipe(isTextAttributeActive, { key: 'bold' })
.run();
return result;
},
@@ -54,7 +54,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => {
const [result] = host.std.command
.chain()
.pipe(isTextStyleActive, { key: 'italic' })
.pipe(isTextAttributeActive, { key: 'italic' })
.run();
return result;
},
@@ -70,7 +70,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => {
const [result] = host.std.command
.chain()
.pipe(isTextStyleActive, { key: 'underline' })
.pipe(isTextAttributeActive, { key: 'underline' })
.run();
return result;
},
@@ -86,7 +86,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => {
const [result] = host.std.command
.chain()
.pipe(isTextStyleActive, { key: 'strike' })
.pipe(isTextAttributeActive, { key: 'strike' })
.run();
return result;
},
@@ -102,7 +102,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => {
const [result] = host.std.command
.chain()
.pipe(isTextStyleActive, { key: 'code' })
.pipe(isTextAttributeActive, { key: 'code' })
.run();
return result;
},
@@ -118,7 +118,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => {
const [result] = host.std.command
.chain()
.pipe(isTextStyleActive, { key: 'link' })
.pipe(isTextAttributeActive, { key: 'link' })
.run();
return result;
},

View File

@@ -1,6 +1,9 @@
import { clearMarksOnDiscontinuousInput } from '@blocksuite/affine-rich-text';
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type {
AffineTextAttributes,
AffineTextStyleAttributes,
} from '@blocksuite/affine-shared/types';
import type { Command, TextSelection } from '@blocksuite/std';
import {
INLINE_ROOT_ATTR,
@@ -13,7 +16,7 @@ import { FORMAT_TEXT_SUPPORT_FLAVOURS } from './consts.js';
export const formatTextCommand: Command<{
currentTextSelection?: TextSelection;
textSelection?: TextSelection;
styles: AffineTextAttributes;
styles: AffineTextStyleAttributes;
mode?: 'replace' | 'merge';
}> = (ctx, next) => {
const { styles, mode = 'merge' } = ctx;

View File

@@ -10,8 +10,8 @@ export { formatBlockCommand } from './format-block.js';
export { formatNativeCommand } from './format-native.js';
export { formatTextCommand } from './format-text.js';
export {
getTextStyle,
isTextStyleActive,
getTextAttributes,
isTextAttributeActive,
toggleBold,
toggleCode,
toggleItalic,

View File

@@ -2,25 +2,31 @@ import {
getBlockSelectionsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type {
AffineTextAttributes,
AffineTextStyleAttributes,
} from '@blocksuite/affine-shared/types';
import type { Command } from '@blocksuite/std';
import { formatBlockCommand } from './format-block.js';
import { formatNativeCommand } from './format-native.js';
import { formatTextCommand } from './format-text.js';
import { getCombinedTextStyle } from './utils.js';
import { getCombinedTextAttributes } from './utils.js';
export const toggleTextStyleCommand: Command<{
key: Extract<
keyof AffineTextAttributes,
keyof AffineTextStyleAttributes,
'bold' | 'italic' | 'underline' | 'strike' | 'code'
>;
}> = (ctx, next) => {
const { std, key } = ctx;
const [active] = std.command.chain().pipe(isTextStyleActive, { key }).run();
const [active] = std.command
.chain()
.pipe(isTextAttributeActive, { key })
.run();
const payload: {
styles: AffineTextAttributes;
styles: AffineTextStyleAttributes;
mode?: 'replace' | 'merge';
} = {
styles: {
@@ -46,7 +52,7 @@ export const toggleTextStyleCommand: Command<{
const toggleTextStyleCommandWrapper = (
key: Extract<
keyof AffineTextAttributes,
keyof AffineTextStyleAttributes,
'bold' | 'italic' | 'underline' | 'strike' | 'code'
>
): Command => {
@@ -66,30 +72,29 @@ export const toggleUnderline = toggleTextStyleCommandWrapper('underline');
export const toggleStrike = toggleTextStyleCommandWrapper('strike');
export const toggleCode = toggleTextStyleCommandWrapper('code');
export const getTextStyle: Command<{}, { textStyle: AffineTextAttributes }> = (
ctx,
next
) => {
const [result, innerCtx] = getCombinedTextStyle(
export const getTextAttributes: Command<
{},
{ textAttributes: AffineTextAttributes }
> = (ctx, next) => {
const [result, innerCtx] = getCombinedTextAttributes(
ctx.std.command.chain()
).run();
if (!result) {
return false;
}
return next({ textStyle: innerCtx.textStyle });
return next({ textAttributes: innerCtx.textAttributes });
};
export const isTextStyleActive: Command<{ key: keyof AffineTextAttributes }> = (
ctx,
next
) => {
export const isTextAttributeActive: Command<{
key: keyof AffineTextAttributes;
}> = (ctx, next) => {
const key = ctx.key;
const [result] = getCombinedTextStyle(ctx.std.command.chain())
const [result] = getCombinedTextAttributes(ctx.std.command.chain())
.pipe((ctx, next) => {
const { textStyle } = ctx;
const { textAttributes } = ctx;
if (textStyle && key in textStyle) {
if (textAttributes && key in textAttributes) {
return next();
}

View File

@@ -77,8 +77,8 @@ function handleCurrentSelection(
handler: (
type: 'text' | 'block' | 'native',
inlineEditors: InlineEditor<AffineTextAttributes>[]
) => { textStyle: AffineTextAttributes } | boolean | void
): Chain<InitCommandCtx & { textStyle: AffineTextAttributes }> {
) => { textAttributes: AffineTextAttributes } | boolean | void
): Chain<InitCommandCtx & { textAttributes: AffineTextAttributes }> {
return chain.try(chain => [
// text selection, corresponding to `formatText` command
chain
@@ -174,25 +174,25 @@ function handleCurrentSelection(
]);
}
export function getCombinedTextStyle(chain: Chain<InitCommandCtx>) {
export function getCombinedTextAttributes(chain: Chain<InitCommandCtx>) {
return handleCurrentSelection(chain, (type, inlineEditors) => {
if (type === 'text') {
return {
textStyle: getCombinedFormatFromInlineEditors(
textAttributes: getCombinedFormatFromInlineEditors(
inlineEditors.map(e => [e, e.getInlineRange()])
),
};
}
if (type === 'block') {
return {
textStyle: getCombinedFormatFromInlineEditors(
textAttributes: getCombinedFormatFromInlineEditors(
inlineEditors.map(e => [e, { index: 0, length: e.yTextLength }])
),
};
}
if (type === 'native') {
return {
textStyle: getCombinedFormatFromInlineEditors(
textAttributes: getCombinedFormatFromInlineEditors(
inlineEditors.map(e => [e, e.getInlineRange()])
),
};

View File

@@ -1,3 +1,4 @@
import { CommentInlineSpecExtension } from '@blocksuite/affine-inline-comment';
import { FootNoteInlineSpecExtension } from '@blocksuite/affine-inline-footnote';
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
@@ -32,5 +33,6 @@ export const DefaultInlineManagerExtension =
LinkInlineSpecExtension.identifier,
FootNoteInlineSpecExtension.identifier,
MentionInlineSpecExtension.identifier,
CommentInlineSpecExtension.identifier,
],
});

View File

@@ -12,7 +12,9 @@ export type AffineInlineRootElement = InlineRootElement<AffineTextAttributes>;
export const BoldInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'bold',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
bold: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.bold;
},
@@ -24,7 +26,9 @@ export const BoldInlineSpecExtension =
export const ItalicInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'italic',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
italic: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.italic;
},
@@ -36,7 +40,9 @@ export const ItalicInlineSpecExtension =
export const UnderlineInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'underline',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
underline: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.underline;
},
@@ -48,7 +54,9 @@ export const UnderlineInlineSpecExtension =
export const StrikeInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'strike',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
strike: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.strike;
},
@@ -60,7 +68,9 @@ export const StrikeInlineSpecExtension =
export const CodeInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'inline-code',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
code: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.code;
},
@@ -72,7 +82,9 @@ export const CodeInlineSpecExtension =
export const BackgroundInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'background',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
background: z.string().optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.background;
},
@@ -84,7 +96,9 @@ export const BackgroundInlineSpecExtension =
export const ColorInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'color',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
color: z.string().optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.color;
},

View File

@@ -9,6 +9,7 @@
"references": [
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../comment" },
{ "path": "../footnote" },
{ "path": "../latex" },
{ "path": "../link" },

View File

@@ -27,18 +27,20 @@ export const ReferenceInlineSpecExtension =
}
return {
name: 'reference',
schema: z
.object({
type: z.enum([
// @deprecated Subpage is deprecated, use LinkedPage instead
'Subpage',
'LinkedPage',
]),
})
.merge(ReferenceInfoSchema)
.optional()
.nullable()
.catch(undefined),
schema: z.object({
reference: z
.object({
type: z.enum([
// @deprecated Subpage is deprecated, use LinkedPage instead
'Subpage',
'LinkedPage',
]),
})
.merge(ReferenceInfoSchema)
.optional()
.nullable()
.catch(undefined),
}),
match: delta => {
return !!delta.attributes?.reference;
},

View File

@@ -58,6 +58,8 @@ export type AttachmentBlockProps = {
style?: (typeof AttachmentBlockStyles)[number];
footnoteIdentifier: string | null;
comments?: Record<string, boolean>;
} & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -78,6 +80,7 @@ export const defaultAttachmentProps: AttachmentBlockProps = {
'meta:createdBy': undefined,
'meta:updatedBy': undefined,
footnoteIdentifier: null,
comments: undefined,
};
export const AttachmentBlockSchema = defineBlockSchema({

View File

@@ -28,6 +28,7 @@ export type BookmarkBlockProps = {
url: string;
caption: string | null;
footnoteIdentifier: string | null;
comments?: Record<string, boolean>;
} & LinkPreviewData &
Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -52,6 +53,7 @@ const defaultBookmarkProps: BookmarkBlockProps = {
'meta:updatedBy': undefined,
footnoteIdentifier: null,
comments: undefined,
};
export const BookmarkBlockSchema = defineBlockSchema({

View File

@@ -14,6 +14,7 @@ type CodeBlockProps = {
caption: string;
preview?: boolean;
lineNumber?: boolean;
comments?: Record<string, boolean>;
} & BlockMeta;
export const CodeBlockSchema = defineBlockSchema({
@@ -26,6 +27,7 @@ export const CodeBlockSchema = defineBlockSchema({
caption: '',
preview: undefined,
lineNumber: undefined,
comments: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -16,6 +16,7 @@ export type DatabaseBlockProps = {
title: Text;
cells: SerializedCells;
columns: Array<ColumnDataType>;
comments?: Record<string, boolean>;
};
export class DatabaseBlockModel extends BlockModel<DatabaseBlockProps> {}
@@ -27,6 +28,7 @@ export const DatabaseBlockSchema = defineBlockSchema({
title: internal.Text(),
cells: Object.create(null),
columns: [],
comments: undefined,
}),
metadata: {
role: 'hub',

View File

@@ -26,6 +26,7 @@ import { DefaultTheme } from '../../themes/default';
type EdgelessTextProps = {
hasMaxWidth: boolean;
comments?: Record<string, boolean>;
} & Omit<TextStyleProps, 'fontSize'> &
GfxCommonBlockProps;
@@ -54,6 +55,7 @@ export const EdgelessTextBlockSchema = defineBlockSchema({
scale: 1,
rotate: 0,
hasMaxWidth: false,
comments: undefined,
...EdgelessTextZodSchema.parse(undefined),
}),
metadata: {

View File

@@ -30,6 +30,7 @@ export type FrameBlockProps = {
background: Color;
childElementIds?: Record<string, boolean>;
presentationIndex?: string;
comments?: Record<string, boolean>;
} & GfxCompatibleProps;
export const FrameZodSchema = z
@@ -50,6 +51,7 @@ export const FrameBlockSchema = defineBlockSchema({
childElementIds: Object.create(null),
presentationIndex: generateKeyBetweenV2(null, null),
lockedBySelf: false,
comments: undefined,
}),
metadata: {
version: 1,

View File

@@ -19,6 +19,7 @@ export type ImageBlockProps = {
height?: number;
rotate: number;
size?: number;
comments?: Record<string, boolean>;
} & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -32,6 +33,7 @@ const defaultImageProps: ImageBlockProps = {
lockedBySelf: false,
rotate: 0,
size: -1,
comments: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -11,6 +11,7 @@ import {
export type LatexProps = {
latex: string;
comments?: Record<string, boolean>;
} & GfxCommonBlockProps;
export const LatexBlockSchema = defineBlockSchema({
@@ -22,6 +23,7 @@ export const LatexBlockSchema = defineBlockSchema({
scale: 1,
rotate: 0,
latex: '',
comments: undefined,
}),
metadata: {
version: 1,

View File

@@ -16,6 +16,7 @@ export type ListProps = {
checked: boolean;
collapsed: boolean;
order: number | null;
comments?: Record<string, boolean>;
} & BlockMeta;
export const ListBlockSchema = defineBlockSchema({
@@ -29,6 +30,7 @@ export const ListBlockSchema = defineBlockSchema({
// number type only for numbered list
order: null,
comments: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -69,6 +69,7 @@ export const NoteBlockSchema = defineBlockSchema({
shadowType: DEFAULT_NOTE_SHADOW,
},
},
comments: undefined,
}),
metadata: {
version: 1,
@@ -91,6 +92,7 @@ export type NoteProps = {
background: Color;
displayMode: NoteDisplayMode;
edgeless: NoteEdgelessProps;
comments?: Record<string, boolean>;
/**
* @deprecated
* use `displayMode` instead

View File

@@ -21,6 +21,7 @@ export type ParagraphProps = {
type: ParagraphType;
text: Text;
collapsed: boolean;
comments?: Record<string, boolean>;
} & BlockMeta;
export const ParagraphBlockSchema = defineBlockSchema({
@@ -29,6 +30,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
type: 'text',
text: internal.Text(),
collapsed: false,
comments: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -8,14 +8,16 @@ export type SurfaceRefProps = {
reference: string;
caption: string;
refFlavour: string;
comments?: Record<string, boolean>;
};
export const SurfaceRefBlockSchema = defineBlockSchema({
flavour: 'affine:surface-ref',
props: () => ({
props: (): SurfaceRefProps => ({
reference: '',
caption: '',
refFlavour: '',
comments: undefined,
}),
metadata: {
version: 1,

View File

@@ -29,6 +29,7 @@ export interface TableBlockProps extends BlockMeta {
columns: Record<string, TableColumn>;
// key = `${rowId}:${columnId}`
cells: Record<string, TableCell>;
comments?: Record<string, boolean>;
}
export interface TableCellSerialized {
@@ -51,6 +52,7 @@ export const TableBlockSchema = defineBlockSchema({
rows: {},
columns: {},
cells: {},
comments: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -43,6 +43,7 @@ export const ReferenceParamsSchema = z
databaseId: z.string().optional(),
databaseRowId: z.string().optional(),
xywh: SerializedXYWHSchema.optional(),
commentId: z.string().optional(),
})
.partial();

View File

@@ -10,7 +10,11 @@ import {
import type { BlockMeta } from './types';
export type EmbedProps<Props = object> = Props & GfxCompatibleProps & BlockMeta;
export type EmbedProps<Props = object> = Props &
GfxCompatibleProps &
BlockMeta & {
comments?: Record<string, boolean>;
};
export function defineEmbedModel<
Props extends object,
@@ -52,6 +56,7 @@ export function createEmbedBlockSchema<
xywh: '[0,0,0,0]',
lockedBySelf: false,
rotate: 0,
comments: undefined,
'meta:createdAt': undefined,
'meta:updatedAt': undefined,
'meta:createdBy': undefined,

View File

@@ -63,7 +63,8 @@
"./theme": "./src/theme/index.ts",
"./styles": "./src/styles/index.ts",
"./services": "./src/services/index.ts",
"./adapters": "./src/adapters/index.ts"
"./adapters": "./src/adapters/index.ts",
"./test-utils": "./src/test-utils/index.ts"
},
"files": [
"src",

View File

@@ -4,7 +4,7 @@
import { describe, expect, it } from 'vitest';
import { getFirstBlockCommand } from '../../../commands/block-crud/get-first-content-block';
import { affine } from '../../helpers/affine-template';
import { affine } from '../../../test-utils';
describe('commands/block-crud', () => {
describe('getFirstBlockCommand', () => {

View File

@@ -4,7 +4,7 @@
import { describe, expect, it } from 'vitest';
import { getLastBlockCommand } from '../../../commands/block-crud/get-last-content-block';
import { affine } from '../../helpers/affine-template';
import { affine } from '../../../test-utils';
describe('commands/block-crud', () => {
describe('getLastBlockCommand', () => {

View File

@@ -1,13 +1,11 @@
/**
* @vitest-environment happy-dom
*/
import '../../helpers/affine-test-utils';
import type { TextSelection } from '@blocksuite/std';
import { describe, expect, it } from 'vitest';
import { replaceSelectedTextWithBlocksCommand } from '../../../commands/model-crud/replace-selected-text-with-blocks';
import { affine, block } from '../../helpers/affine-template';
import { affine, block } from '../../../test-utils';
describe('commands/model-crud', () => {
describe('replaceSelectedTextWithBlocksCommand', () => {

View File

@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
import { isNothingSelectedCommand } from '../../../commands/selection/is-nothing-selected';
import { ImageSelection } from '../../../selection';
import { affine } from '../../helpers/affine-template';
import { affine } from '../../../test-utils';
describe('commands/selection', () => {
describe('isNothingSelectedCommand', () => {

View File

@@ -1,298 +0,0 @@
import {
CodeBlockSchemaExtension,
DatabaseBlockSchemaExtension,
ImageBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
} from '@blocksuite/affine-model';
import { TextSelection } from '@blocksuite/std';
import { type Block, type Store } from '@blocksuite/store';
import { Text } from '@blocksuite/store';
import { TestWorkspace } from '@blocksuite/store/test';
import { createTestHost } from './create-test-host';
// Extensions array
const extensions = [
RootBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
ListBlockSchemaExtension,
ImageBlockSchemaExtension,
DatabaseBlockSchemaExtension,
CodeBlockSchemaExtension,
];
// Mapping from tag names to flavours
const tagToFlavour: Record<string, string> = {
'affine-page': 'affine:page',
'affine-note': 'affine:note',
'affine-paragraph': 'affine:paragraph',
'affine-list': 'affine:list',
'affine-image': 'affine:image',
'affine-database': 'affine:database',
'affine-code': 'affine:code',
};
interface SelectionInfo {
anchorBlockId?: string;
anchorOffset?: number;
focusBlockId?: string;
focusOffset?: number;
cursorBlockId?: string;
cursorOffset?: number;
}
/**
* Parse template strings and build BlockSuite document structure,
* then create a host object with the document
*
* Example:
* ```
* const host = affine`
* <affine-page id="page">
* <affine-note id="note">
* <affine-paragraph id="paragraph-1">Hello, world<anchor /></affine-paragraph>
* <affine-paragraph id="paragraph-2">Hello, world<focus /></affine-paragraph>
* </affine-note>
* </affine-page>
* `;
* ```
*/
export function affine(strings: TemplateStringsArray, ...values: any[]) {
// Merge template strings and values
let htmlString = '';
strings.forEach((str, i) => {
htmlString += str;
if (i < values.length) {
htmlString += values[i];
}
});
// Create a new doc
const workspace = new TestWorkspace({});
workspace.meta.initialize();
const doc = workspace.createDoc('test-doc');
const store = doc.getStore({ extensions });
let selectionInfo: SelectionInfo = {};
// Use DOMParser to parse HTML string
doc.load(() => {
const parser = new DOMParser();
const dom = parser.parseFromString(htmlString.trim(), 'text/html');
const root = dom.body.firstElementChild;
if (!root) {
throw new Error('Template must contain a root element');
}
buildDocFromElement(store, root, null, selectionInfo);
});
// Create host object
const host = createTestHost(store);
// Set selection if needed
if (selectionInfo.anchorBlockId && selectionInfo.focusBlockId) {
const anchorBlock = store.getBlock(selectionInfo.anchorBlockId);
const anchorTextLength = anchorBlock?.model?.text?.length ?? 0;
const focusOffset = selectionInfo.focusOffset ?? 0;
const anchorOffset = selectionInfo.anchorOffset ?? 0;
if (selectionInfo.anchorBlockId === selectionInfo.focusBlockId) {
const selection = host.selection.create(TextSelection, {
from: {
blockId: selectionInfo.anchorBlockId,
index: anchorOffset,
length: focusOffset,
},
to: null,
});
host.selection.setGroup('note', [selection]);
} else {
const selection = host.selection.create(TextSelection, {
from: {
blockId: selectionInfo.anchorBlockId,
index: anchorOffset,
length: anchorTextLength - anchorOffset,
},
to: {
blockId: selectionInfo.focusBlockId,
index: 0,
length: focusOffset,
},
});
host.selection.setGroup('note', [selection]);
}
} else if (selectionInfo.cursorBlockId) {
const selection = host.selection.create(TextSelection, {
from: {
blockId: selectionInfo.cursorBlockId,
index: selectionInfo.cursorOffset ?? 0,
length: 0,
},
to: null,
});
host.selection.setGroup('note', [selection]);
}
return host;
}
/**
* Create a single block from template string
*
* Example:
* ```
* const block = block`<affine-note />`
* ```
*/
export function block(
strings: TemplateStringsArray,
...values: any[]
): Block | null {
// Merge template strings and values
let htmlString = '';
strings.forEach((str, i) => {
htmlString += str;
if (i < values.length) {
htmlString += values[i];
}
});
// Create a temporary doc to hold the block
const workspace = new TestWorkspace({});
workspace.meta.initialize();
const doc = workspace.createDoc('temp-doc');
const store = doc.getStore({ extensions });
let blockId: string | null = null;
const selectionInfo: SelectionInfo = {};
// Use DOMParser to parse HTML string
doc.load(() => {
const parser = new DOMParser();
const dom = parser.parseFromString(htmlString.trim(), 'text/html');
const root = dom.body.firstElementChild;
if (!root) {
throw new Error('Template must contain a root element');
}
// Create a root block if needed
const flavour = tagToFlavour[root.tagName.toLowerCase()];
if (
flavour === 'affine:paragraph' ||
flavour === 'affine:list' ||
flavour === 'affine:code'
) {
const pageId = store.addBlock('affine:page', {});
const noteId = store.addBlock('affine:note', {}, pageId);
blockId = buildDocFromElement(store, root, noteId, selectionInfo);
} else {
blockId = buildDocFromElement(store, root, null, selectionInfo);
}
});
// Return the created block
return blockId ? (store.getBlock(blockId) ?? null) : null;
}
/**
* Recursively build document structure
* @param doc
* @param element
* @param parentId
* @param selectionInfo
* @returns
*/
function buildDocFromElement(
doc: Store,
element: Element,
parentId: string | null,
selectionInfo: SelectionInfo
): string {
const tagName = element.tagName.toLowerCase();
// Handle selection tags
if (tagName === 'anchor') {
if (!parentId) return '';
const parentBlock = doc.getBlock(parentId);
if (parentBlock) {
const textBeforeCursor = element.previousSibling?.textContent ?? '';
selectionInfo.anchorBlockId = parentId;
selectionInfo.anchorOffset = textBeforeCursor.length;
}
return parentId;
} else if (tagName === 'focus') {
if (!parentId) return '';
const parentBlock = doc.getBlock(parentId);
if (parentBlock) {
const textBeforeCursor = element.previousSibling?.textContent ?? '';
selectionInfo.focusBlockId = parentId;
selectionInfo.focusOffset = textBeforeCursor.length;
}
return parentId;
} else if (tagName === 'cursor') {
if (!parentId) return '';
const parentBlock = doc.getBlock(parentId);
if (parentBlock) {
const textBeforeCursor = element.previousSibling?.textContent ?? '';
selectionInfo.cursorBlockId = parentId;
selectionInfo.cursorOffset = textBeforeCursor.length;
}
return parentId;
}
const flavour = tagToFlavour[tagName];
if (!flavour) {
throw new Error(`Unknown tag name: ${tagName}`);
}
const props: Record<string, any> = {};
const customId = element.getAttribute('id');
// If ID is specified, add it to props
if (customId) {
props.id = customId;
}
// Process element attributes
Array.from(element.attributes).forEach(attr => {
if (attr.name !== 'id') {
// Skip id attribute, we already handled it
props[attr.name] = attr.value;
}
});
// Special handling for different block types based on their flavours
switch (flavour) {
case 'affine:paragraph':
case 'affine:list':
if (element.textContent) {
props.text = new Text(element.textContent);
}
break;
}
// Create block
const blockId = doc.addBlock(flavour, props, parentId);
// Process all child nodes, including text nodes
Array.from(element.children).forEach(child => {
if (child.nodeType === Node.ELEMENT_NODE) {
// Handle element nodes
buildDocFromElement(doc, child as Element, blockId, selectionInfo);
} else if (child.nodeType === Node.TEXT_NODE) {
// Handle text nodes
console.log('buildDocFromElement text node:', child.textContent);
}
});
return blockId;
}

View File

@@ -1,7 +1,7 @@
import { TextSelection } from '@blocksuite/std';
import { describe, expect, it } from 'vitest';
import { affine } from './affine-template';
import { affine } from '../../test-utils';
describe('helpers/affine-template', () => {
it('should create a basic document structure from template', () => {

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