Compare commits

...

31 Commits

Author SHA1 Message Date
flrande
e363ba5f4f feat(editor): support edgeless code block 2025-05-19 18:15:55 +08:00
fengmk2
1e7774929c feat(server): filter docs by access role (#12311)
close CLOUD-208

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

- **New Features**
  - Enhanced document access control with batch permission checks, enabling efficient filtering of documents based on user roles and permissions.
  - Added detailed document-level role and permission management for workspace users.
- **Bug Fixes**
  - Improved accuracy in filtering search results to only display documents users have permission to read.
- **Tests**
  - Added comprehensive tests for document-level permission filtering and search result accuracy.
  - Introduced new mock utilities to support permission-related test scenarios.
- **Refactor**
  - Simplified and optimized permission logic for determining user roles and document access.
- **Documentation**
  - Updated type definitions for improved clarity in permission handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-19 03:28:22 +00:00
CatsJuice
85bb728ca8 feat(core): new docs list for tag detail (#12298)
close AF-2583

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

## Summary by CodeRabbit

- **New Features**
  - Introduced a new tag list header with breadcrumb navigation and a tag selector dropdown for improved navigation and tag management.
  - Added a searchable dropdown menu for selecting and switching between tags.

- **Improvements**
  - Updated the tag detail page to use a more dynamic, subscription-based document explorer for displaying tagged content.
  - Enhanced header controls with an updated display menu button.

- **Style**
  - Added comprehensive new styles for the tag list header and tag selector components.
  - Introduced a new scroll area style for flexible layout.

- **Documentation**
  - Marked an older page list header component as deprecated.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-19 02:56:03 +00:00
CatsJuice
8b669b725b feat(core): new doc list for collection detail (#12278)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new document list view with support for different layouts (list, grid, masonry) and improved multi-selection and batch deletion capabilities.
  - Added a view toggle control for switching between document list layouts across relevant pages.
  - Implemented a new collection list header with breadcrumb navigation and streamlined actions for editing collections and creating new pages.

- **Improvements**
  - Simplified and unified document list rendering by delegating logic to a shared component.
  - Enhanced document selection behavior, allowing toggling of individual items in select mode.
  - Updated styles for document lists, group headers, and collection pages for a more consistent appearance.
  - Refined wrapper component styling to prevent unintended HTML attribute forwarding.

- **Refactor**
  - Replaced local implementations of view toggles and document grouping with shared components for easier maintenance.
  - Removed deprecated and unused styles and props to streamline components and improve code clarity.
  - Refactored collection detail and header components to adopt new context-driven document explorer architecture.

- **Documentation**
  - Added deprecation notice to an outdated collection page list header component.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-19 01:59:38 +00:00
fundon
4ecdfb1258 fix(core): should use AttachmentViewerView in split view and standalone page (#12323)
Closes: [AF-2564](https://linear.app/affine-design/issue/AF-2564/pdf-split-view-多了-header)

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

- **Refactor**
  - Improved code clarity and maintainability with clearer comments and streamlined property usage.
  - Updated the workspace attachment page to use a more context-appropriate attachment viewer component.
- **Style**
  - Minor formatting improvements for better code readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-18 05:47:52 +00:00
Saul-Mirone
820c3fda63 refactor(editor): cleanup effects export (#12312)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Chores**
  - Removed multiple internal export entries related to effects modules across various packages.
  - Updated dependencies and configuration references to improve internal consistency. No visible changes for end-users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-18 04:17:08 +00:00
fundon
a028f027be chore(editor): add tracking events to attachments (#12317)
Closes: [BS-3483](https://linear.app/affine-design/issue/BS-3483/event-tracking-loading-block)
2025-05-18 01:57:42 +00:00
fundon
8726b0e462 refactor(editor): optimize pasting process of attachments and images (#12276)
Related to: [BS-3146](https://linear.app/affine-design/issue/BS-3146/import-paste-接口改进优化)
2025-05-18 01:57:42 +00:00
fundon
f3693a91c3 fix(editor): should update image size field (#12274)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Image content now includes file size information in its metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-18 01:57:41 +00:00
renovate
f215b680ef chore: bump up oxlint version to v0.16.11 (#12335)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

#### \[0.16.11] - 2025-05-16

##### Features

-   [`078bf0b`](https://redirect.github.com/oxc-project/oxc/commit/078bf0b) language_server: Better fallback handling when passing invalid `Options` values ([#&#8203;10930](https://redirect.github.com/oxc-project/oxc/issues/10930)) (Sysix)
-   [`be7f7e1`](https://redirect.github.com/oxc-project/oxc/commit/be7f7e1) language_server/editor: Support multi workspace folders ([#&#8203;10875](https://redirect.github.com/oxc-project/oxc/issues/10875)) (Sysix)
-   [`eef93b4`](https://redirect.github.com/oxc-project/oxc/commit/eef93b4) linter: Add import/no-unassigned-import ([#&#8203;10970](https://redirect.github.com/oxc-project/oxc/issues/10970)) (yefan)
-   [`cc0112f`](https://redirect.github.com/oxc-project/oxc/commit/cc0112f) linter: No-unused-vars add setting for `reportVarsOnlyUsedAsTypes` ([#&#8203;11009](https://redirect.github.com/oxc-project/oxc/issues/11009)) (camc314)
-   [`17e49c3`](https://redirect.github.com/oxc-project/oxc/commit/17e49c3) linter: Implement configuration and checking loops for `eslint/no_constant_condition` ([#&#8203;10949](https://redirect.github.com/oxc-project/oxc/issues/10949)) (Ulrich Stark)
-   [`21117ac`](https://redirect.github.com/oxc-project/oxc/commit/21117ac) linter: Implement react/forbid-elements ([#&#8203;10928](https://redirect.github.com/oxc-project/oxc/issues/10928)) (Thomas BOCQUEZ)
-   [`466c24a`](https://redirect.github.com/oxc-project/oxc/commit/466c24a) linter: Add gitlab reporter output format ([#&#8203;10927](https://redirect.github.com/oxc-project/oxc/issues/10927)) (Connor Pearson)
-   [`a064082`](https://redirect.github.com/oxc-project/oxc/commit/a064082) linter: Add import/consistent-type-specifier-style rule ([#&#8203;10858](https://redirect.github.com/oxc-project/oxc/issues/10858)) (yefan)
-   [`4733b52`](https://redirect.github.com/oxc-project/oxc/commit/4733b52) linter/no-extraneous-class: Add conditional fixer ([#&#8203;10798](https://redirect.github.com/oxc-project/oxc/issues/10798)) (DonIsaac)

##### Bug Fixes

-   [`87bf2a8`](https://redirect.github.com/oxc-project/oxc/commit/87bf2a8) editor: Send only `workspace/didChangeConfiguration` when some workspace configuration is effected ([#&#8203;11017](https://redirect.github.com/oxc-project/oxc/issues/11017)) (Sysix)
-   [`ed5708d`](https://redirect.github.com/oxc-project/oxc/commit/ed5708d) editor: Detect all workspaces config path changes ([#&#8203;11016](https://redirect.github.com/oxc-project/oxc/issues/11016)) (Sysix)
-   [`89cc21b`](https://redirect.github.com/oxc-project/oxc/commit/89cc21b) language_server: Normalize oxlintrc config path ([#&#8203;10982](https://redirect.github.com/oxc-project/oxc/issues/10982)) (Sysix)
-   [`c52a9ba`](https://redirect.github.com/oxc-project/oxc/commit/c52a9ba) linter: Fix plugins inside overrides not being applied ([#&#8203;11057](https://redirect.github.com/oxc-project/oxc/issues/11057)) (camc314)
-   [`b12bd48`](https://redirect.github.com/oxc-project/oxc/commit/b12bd48) linter: Fix rule config not being correctly applied ([#&#8203;11055](https://redirect.github.com/oxc-project/oxc/issues/11055)) (camc314)
-   [`9a368be`](https://redirect.github.com/oxc-project/oxc/commit/9a368be) linter: False negative in no-restriced-imports with `patterns` and side effects ([#&#8203;11027](https://redirect.github.com/oxc-project/oxc/issues/11027)) (camc314)
-   [`8c2cfbc`](https://redirect.github.com/oxc-project/oxc/commit/8c2cfbc) linter: False negative in no-restricted-imports ([#&#8203;11026](https://redirect.github.com/oxc-project/oxc/issues/11026)) (camc314)
-   [`8956870`](https://redirect.github.com/oxc-project/oxc/commit/8956870) linter: False positive in no-unused-vars ([#&#8203;11002](https://redirect.github.com/oxc-project/oxc/issues/11002)) (camc314)
-   [`33a60d2`](https://redirect.github.com/oxc-project/oxc/commit/33a60d2) linter: Skip eslint/no-redeclare when running on modules ([#&#8203;11004](https://redirect.github.com/oxc-project/oxc/issues/11004)) (camc314)
-   [`39063ce`](https://redirect.github.com/oxc-project/oxc/commit/39063ce) linter: Reword diagnostic message for no-control-regex ([#&#8203;10993](https://redirect.github.com/oxc-project/oxc/issues/10993)) (camc314)
-   [`9eedb58`](https://redirect.github.com/oxc-project/oxc/commit/9eedb58) linter: False positive with negative matches in no-restricted-imports ([#&#8203;10976](https://redirect.github.com/oxc-project/oxc/issues/10976)) (camc314)
-   [`10e77d7`](https://redirect.github.com/oxc-project/oxc/commit/10e77d7) linter: Improve diagnostics for no-control-regex ([#&#8203;10959](https://redirect.github.com/oxc-project/oxc/issues/10959)) (camc314)
-   [`0961296`](https://redirect.github.com/oxc-project/oxc/commit/0961296) linter: Add `gitlab` to linter `--help` docs ([#&#8203;10932](https://redirect.github.com/oxc-project/oxc/issues/10932)) (camc314)
-   [`82889ae`](https://redirect.github.com/oxc-project/oxc/commit/82889ae) linter/no-extraneous-class: Improve docs, reporting and code refactor ([#&#8203;10797](https://redirect.github.com/oxc-project/oxc/issues/10797)) (DonIsaac)
-   [`11c34e7`](https://redirect.github.com/oxc-project/oxc/commit/11c34e7) linter/no-img-element: Improve diagnostic and docs ([#&#8203;10908](https://redirect.github.com/oxc-project/oxc/issues/10908)) (DonIsaac)
-   [`584d8b9`](https://redirect.github.com/oxc-project/oxc/commit/584d8b9) napi: Enable mimalloc `no_opt_arch` feature on linux aarch64 ([#&#8203;11053](https://redirect.github.com/oxc-project/oxc/issues/11053)) (Boshen)
-   [`126ae75`](https://redirect.github.com/oxc-project/oxc/commit/126ae75) semantic: Distinguish class private elements ([#&#8203;11044](https://redirect.github.com/oxc-project/oxc/issues/11044)) (magic-akari)
-   [`773d0de`](https://redirect.github.com/oxc-project/oxc/commit/773d0de) semantic: Correctly handle nested brackets in jsdoc parsing ([#&#8203;10922](https://redirect.github.com/oxc-project/oxc/issues/10922)) (camc314)
-   [`b215b6c`](https://redirect.github.com/oxc-project/oxc/commit/b215b6c) semantic: Dont parse `@` as jsdoc tags inside `[`/`]` ([#&#8203;10919](https://redirect.github.com/oxc-project/oxc/issues/10919)) (camc314)

##### Documentation

-   [`db6afb9`](https://redirect.github.com/oxc-project/oxc/commit/db6afb9) linter: Improve docs of no-debugger ([#&#8203;11033](https://redirect.github.com/oxc-project/oxc/issues/11033)) (camc314)
-   [`16541de`](https://redirect.github.com/oxc-project/oxc/commit/16541de) linter: Improve docs of default-param-last ([#&#8203;11032](https://redirect.github.com/oxc-project/oxc/issues/11032)) (camc314)
-   [`2c2f3c4`](https://redirect.github.com/oxc-project/oxc/commit/2c2f3c4) linter: Improve docs of default-case-last ([#&#8203;11031](https://redirect.github.com/oxc-project/oxc/issues/11031)) (camc314)
-   [`56bb9ce`](https://redirect.github.com/oxc-project/oxc/commit/56bb9ce) linter: Improve docs of array-callback-return ([#&#8203;11030](https://redirect.github.com/oxc-project/oxc/issues/11030)) (camc314)
-   [`13dbcc6`](https://redirect.github.com/oxc-project/oxc/commit/13dbcc6) linter: Correct docs for default config for no-redeclare ([#&#8203;10995](https://redirect.github.com/oxc-project/oxc/issues/10995)) (camc314)
-   [`a86cbb3`](https://redirect.github.com/oxc-project/oxc/commit/a86cbb3) linter: Fix incorrect backticks of fenced code blocks ([#&#8203;10947](https://redirect.github.com/oxc-project/oxc/issues/10947)) (Ulrich Stark)

##### Refactor

-   [`3cc1466`](https://redirect.github.com/oxc-project/oxc/commit/3cc1466) language_server: New configuration structure for `initialize` and `workspace/didChangeConfiguration` ([#&#8203;10890](https://redirect.github.com/oxc-project/oxc/issues/10890)) (Sysix)
-   [`bd2ef7d`](https://redirect.github.com/oxc-project/oxc/commit/bd2ef7d) language_server: Use `Arc` for `diagnostic_report_map` ([#&#8203;10940](https://redirect.github.com/oxc-project/oxc/issues/10940)) (Sysix)
-   [`bb999a3`](https://redirect.github.com/oxc-project/oxc/commit/bb999a3) language_server: Avoid cloning linter by taking reference in LintService ([#&#8203;10907](https://redirect.github.com/oxc-project/oxc/issues/10907)) (Ulrich Stark)
-   [`d1b0c83`](https://redirect.github.com/oxc-project/oxc/commit/d1b0c83) linter: Remove overrides index vec ([#&#8203;11058](https://redirect.github.com/oxc-project/oxc/issues/11058)) (camc314)
-   [`7ad6cf8`](https://redirect.github.com/oxc-project/oxc/commit/7ad6cf8) linter: Store severity separately, remove `RuleWithSeverity` ([#&#8203;11051](https://redirect.github.com/oxc-project/oxc/issues/11051)) (camchenry)
-   [`e31c361`](https://redirect.github.com/oxc-project/oxc/commit/e31c361) linter: Remove nested match statements in no-restricted-imports ([#&#8203;10975](https://redirect.github.com/oxc-project/oxc/issues/10975)) (camc314)
-   [`6ad9d4f`](https://redirect.github.com/oxc-project/oxc/commit/6ad9d4f) linter: Tidy `eslint/func-names` ([#&#8203;10923](https://redirect.github.com/oxc-project/oxc/issues/10923)) (camc314)
-   [`faf0a95`](https://redirect.github.com/oxc-project/oxc/commit/faf0a95) syntax: Rename `NameSpaceModule` to `NamespaceModule` ([#&#8203;10917](https://redirect.github.com/oxc-project/oxc/issues/10917)) (Dunqing)

##### Testing

-   [`76b6b33`](https://redirect.github.com/oxc-project/oxc/commit/76b6b33) editor: Add tests for multi workspace folder setup ([#&#8203;10904](https://redirect.github.com/oxc-project/oxc/issues/10904)) (Sysix)

</details>

---

### Configuration

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

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

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

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

---

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

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC4xMS4xOCIsInVwZGF0ZWRJblZlciI6IjQwLjExLjE4IiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-05-17 14:41:42 +00:00
fengmk2
f0c9453459 fix(server): add AFFINE_INDEXER_SEARCH_ENDPOINT to self-host compose.yml (#12324)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
  - Added a `.gitignore` file to prevent the `.env` file in the self-hosted Docker directory from being tracked by Git.

- **New Features**
  - Introduced a new environment variable for the search endpoint in the Docker Compose configuration for improved service connectivity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-17 12:51:26 +00:00
doouding
5eca722edf fix: connector issues (#12308)
Fixes [BS-3161](https://linear.app/affine-design/issue/BS-3161/发现已连接的connector会响应对齐线)
Fixes [BS-3337](https://linear.app/affine-design/issue/BS-3337/connector你肿么了)
Fixes [BS-3334](https://linear.app/affine-design/issue/BS-3334/connector-不应该能够被拖拽)

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

## Summary by CodeRabbit

- **Bug Fixes**
	- Corrected typos related to label editing state, ensuring more reliable label editing and display for connectors.
	- Fixed logic in the auto-complete overlay, improving when overlays appear during hover actions.

- **New Features**
	- Improved connector label handling by ensuring label state is preserved and restored during editing.
	- Enhanced connector movement behavior, allowing connectors to be moved only when appropriate elements are selected.

- **Tests**
	- Added end-to-end tests to verify connector movement and selection behaviors for improved reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-16 09:49:22 +00:00
doodlewind
8ed4f14380 feat(editor): support border radius for shape dom renderer (#12326)
Comparison:

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/f2a266ba-c3d5-46ea-9aa5-38e5d0de6d5a.png)

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

## Summary by CodeRabbit

- **New Features**
	- Border radius and border thickness of shapes now scale dynamically with zoom level for improved visual consistency.

- **Tests**
	- Added a test to ensure percentage-based border radius values are correctly rendered in the DOM.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-16 09:02:45 +00:00
LongYinan
101062aa25 ci: temporary skip ios pipeline 2025-05-16 15:12:24 +08:00
doodlewind
b6e9c41ee3 fix(editor): mid button drag in presentation mode (#12309)
Fixes https://linear.app/affine-design/issue/BS-3448

Before this PR, presentation mode would force quit if user either:

1. Press space
2. Drag with mouse middle button

Unfixed behavior:

https://github.com/user-attachments/assets/8ff4e13a-69a8-4de6-8994-bf36e6e3eb49

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

- **Bug Fixes**
	- Improved presentation mode to preserve your current panned view when exiting pan mode or toggling fullscreen, preventing unwanted viewport resets.
	- Spacebar actions are now correctly disabled when using the frame navigator tool, avoiding accidental tool switches.
- **New Features**
	- Enhanced presentation controls for smoother transitions and better handling of user navigation states.
	- Added a one-time toast notification for presentations without frames, shown only once per session for better user guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 11:12:41 +00:00
Flrande
147fa9a6b1 feat(editor): add line number display option for code block (#12305)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a toggle in the code block toolbar to show or hide line numbers for individual code blocks.
  - The display of line numbers now respects both global and per-block settings, allowing more flexible control.
- **Style**
  - Updated styles to hide line numbers when disabled via the new toggle option.
- **Tests**
  - Added end-to-end tests to verify toggling line numbers visibility and undo/redo behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 10:59:38 +00:00
Saul-Mirone
3a2fe0bf91 refactor(editor): extract widgets (#12304)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced two new widgets: Edgeless Dragging Area and Note Slicer, now available for use.
  - Added extension support for these widgets, enabling enhanced interaction and integration within the application.

- **Chores**
  - Updated package configurations and workspace settings to include the new widgets and their dependencies.
  - Added project references and configuration files to support modular development and build processes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 10:44:41 +00:00
doouding
e98ec93af1 fix: connector label editing (#12282)
Fixes [BS-3373](https://linear.app/affine-design/issue/BS-3373/connector%E7%9A%84%E5%8F%8C%E5%87%BB%E6%B7%BB%E5%8A%A0note%E8%A1%8C%E4%B8%BA%E5%8F%97%E5%88%B0%E4%BA%86%E8%A6%86%E7%9B%96%E8%8C%83%E5%9B%B4%E7%9A%84%E5%BD%B1%E5%93%8D)

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

- **New Features**
  - Connector label elements now include identity and creator metadata.

- **Bug Fixes**
  - Improved hit-testing for pointer interactions, resulting in more accurate detection of hovered elements.

- **Refactor**
  - Enhanced internal comparison logic for elements, improving sorting and ordering consistency.
  - Strengthened type definitions for search filters, improving result accuracy and clarity.

- **Tests**
  - Added end-to-end tests to verify correct label entry and retrieval for multiple connectors.
  - Introduced utility functions to fetch connector labels and improved connector creation in test actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 10:31:55 +00:00
yoyoyohamapi
6c9f28e08b feat(core): workspace embedding settings (#11801)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced "Indexer & Embedding" workspace settings to manage AI embedding for local content, including document ignoring and attachment uploads.
  - Added UI components for embedding settings, attachments, and ignored documents with pagination and deletion capabilities.
  - Provided comprehensive file-type icons for attachments.

- **Improvements**
  - Added a new tab for indexing and embedding in workspace settings navigation.
  - Included test identifiers on key UI elements to enhance automated testing.

- **Localization**
  - Added English localization strings covering all embedding-related UI text and actions.

- **Bug Fixes**
  - Enabled previously skipped end-to-end tests for embedding settings to improve reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 09:36:28 +00:00
darkskygit
6224344a4f chore(server): improve ignored docs list & match (#12307)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved the accuracy of document matching by excluding ignored documents from search results.
- **Chores**
	- Updated internal handling of ignored document lists for better consistency and reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 09:36:28 +00:00
darkskygit
393458871d fix(server): self hosted config (#12253)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
  - Updated the Docker Compose configuration to use a different image for the Postgres service.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 06:56:36 +00:00
yoyoyohamapi
d00315e372 test(core): embedding settings (#11554)
### TL;DR

tests: workspace embedding e2e

> CLOSE BS-3052

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

## Summary by CodeRabbit

- **New Features**
  - Introduced comprehensive end-to-end tests for workspace embedding settings, including toggling embedding, uploading and managing attachments, pagination, and ignoring documents.
  - Added utilities for automated interaction with the settings panel and document creation in tests.

- **Tests**
  - Implemented detailed scenarios to verify workspace embedding functionality and user interactions within the settings panel.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 06:43:07 +00:00
akumatus
9fee8147cb feat(core): add ai model switch ui (#12266)
Close [AI-86](https://linear.app/affine-design/issue/AI-86)

![截屏2025-05-14 11.32.41.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/b92d5c32-fa5a-4afd-93e6-3699347575be.png)

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

## Summary by CodeRabbit

- **New Features**
  - Introduced AI model switching in chat, allowing users to select from multiple AI models during conversations.
  - Added a floating menu for easy AI model selection within the chat interface.
  - Enabled visibility of the AI model switcher through a new experimental feature flag, configurable in workspace settings (canary builds only).

- **Enhancements**
  - Improved session management in the chat panel for smoother model switching and state handling.
  - Updated localization to support the new AI model switch feature in settings.

- **Bug Fixes**
  - None.

- **Chores**
  - Registered new components and services to support AI model switching functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 06:29:37 +00:00
fengmk2
6a13d69dea chore(server): separate elasticsearch to run independently (#12299)
close CLOUD-217

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

- **New Features**
  - Introduced a dedicated test job for Elasticsearch, running only Elasticsearch-specific tests during CI.
- **Chores**
  - Enhanced server test workflows with explicit setup steps and automated coverage uploads.
  - Improved test suite structure for Elasticsearch provider to enable conditional and asynchronous test execution based on environment variables.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 06:16:06 +00:00
akumatus
fabcdd3b2c feat(core): add exa url crawl tool (#12277)
Close [AI-126](https://linear.app/affine-design/issue/AI-126)

![截屏2025-05-14 17.01.19.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/1a86ac68-f9f1-4740-8ddb-2293838682d2.png)

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

## Summary by CodeRabbit

- **New Features**
  - Introduced a new web crawling tool, allowing users to extract live content from specific web pages in addition to traditional web search.
- **Improvements**
  - Enhanced error handling for web search and web crawl operations, providing clearer failure messages.
  - Updated terminology in AI prompts and user-facing messages to reflect the new web search/crawl capabilities.
  - Improved formatting of web search and crawl results for better readability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 05:22:55 +00:00
akumatus
fcc9b31da9 feat(core): add get session graphql api (#12237)
Close [AI-116](https://linear.app/affine-design/issue/AI-116)

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

- **New Features**
  - Added the ability to retrieve detailed information for a specific Copilot session by its ID, including model metadata and optional models, via the user interface and API.
  - Session data now includes additional fields such as the model used and a list of optional models.
  - Enhanced GraphQL queries and UI components to support fetching and displaying these new session details.

- **Improvements**
  - Session lists now provide richer information, including model details, for each session.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 04:55:50 +00:00
Saul-Mirone
6052743671 refactor(editor): extract selected rect widget (#12290)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced the Edgeless Selected Rectangle widget, providing enhanced selection and interaction capabilities in edgeless mode.
  - Added rotation-aware resize cursors for improved usability when resizing selections.
  - Integrated new autocomplete panels and selection components for a smoother user experience.

- **Refactor**
  - Modularized the Edgeless Selected Rectangle widget as a standalone package for better maintainability and integration.
  - Updated internal references and imports to utilize the new widget package.

- **Chores**
  - Updated project and package configurations to include the new widget and ensure proper build and type-checking across the workspace.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 04:25:07 +00:00
renovate
43948f205e chore: bump up Node.js to v22.15.1 (#12286)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [node](https://nodejs.org) ([source](https://redirect.github.com/nodejs/node)) | patch | `22.15.0` -> `22.15.1` |

---

### Release Notes

<details>
<summary>nodejs/node (node)</summary>

### [`v22.15.1`](https://redirect.github.com/nodejs/node/compare/v22.15.0...v22.15.1)

[Compare Source](https://redirect.github.com/nodejs/node/compare/v22.15.0...v22.15.1)

</details>

---

### Configuration

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

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

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

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

---

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

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC4xMS45IiwidXBkYXRlZEluVmVyIjoiNDAuMTEuOSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-05-15 04:12:38 +00:00
L-Sun
6fabc0eb1f fix(editor): adjustment of scaled and folded synced doc (#12294)
Close [BS-3418](https://linear.app/affine-design/issue/BS-3418/折叠的embed-doc调整宽度时,会出现一个最小高度,不需要这个)

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

## Summary by CodeRabbit

- **New Features**
  - Added support for dynamically scaling the height of embedded synced document blocks, including proper handling when folding and unfolding.
  - Introduced a new property to track the scaled height of folded synced document blocks.

- **Bug Fixes**
  - Improved accuracy of height calculations for synced document blocks by accounting for both viewport zoom and block scale.

- **Tests**
  - Enhanced end-to-end tests to consistently apply scaling before running size adjustment checks.
  - Added a utility function to simulate scaling elements with keyboard shortcuts during test execution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 03:56:03 +00:00
doodlewind
74b2d4dc2e fix(editor): use persisted state for presentation mode background config (#12293)
Fixed this issue (black background is on but the toggle state is not synced):

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/bb4885c1-eccc-45fa-ac19-f868b9ea055a.png)

Issue source: https://linear.app/affine-design/issue/BS-3448

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

## Summary by CodeRabbit

- **Bug Fixes**
  - The background color now defaults to non-black for new users or when no previous setting exists.
  - Improved reliability when restoring user settings.

- **New Features**
  - Changes to the background color setting are now saved and persist between sessions.

- **Style**
  - Enhanced toggle switch responsiveness for background and toolbar settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 03:41:08 +00:00
fengmk2
ffb72a4491 chore(server): fix missing indexer service on ci (#12291)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
  - Standardized the naming of the search service to "indexer" in automated test workflows.
  - Ensured the "indexer" service is available in additional test jobs for improved consistency across workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 03:28:19 +00:00
255 changed files with 5682 additions and 1120 deletions

1
.docker/selfhost/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

View File

@@ -23,6 +23,7 @@ services:
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
- AFFINE_INDEXER_SEARCH_ENDPOINT=http://indexer:9308
restart: unless-stopped
affine_migration:
@@ -38,6 +39,7 @@ services:
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
- AFFINE_INDEXER_SEARCH_ENDPOINT=http://indexer:9308
depends_on:
postgres:
condition: service_healthy
@@ -57,7 +59,7 @@ services:
restart: unless-stopped
postgres:
image: postgres:16
image: pgvector/pgvector:pg16
container_name: affine_postgres
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data

View File

@@ -582,10 +582,80 @@ jobs:
ports:
- 1025:1025
- 8025:8025
manticoresearch:
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Download server-native.node
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/native
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Run server tests
run: yarn affine @affine/server test:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
CI_NODE_INDEX: ${{ matrix.node_index }}
CI_NODE_TOTAL: ${{ matrix.total_nodes }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-test-elasticsearch:
name: Server Test with Elasticsearch
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
env:
NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
AFFINE_INDEXER_SEARCH_PROVIDER: elasticsearch
AFFINE_INDEXER_SEARCH_ENDPOINT: http://localhost:9200
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
# https://github.com/elastic/elastic-github-actions/blob/master/elasticsearch/README.md
- name: Configure sysctl limits for Elasticsearch
@@ -618,8 +688,8 @@ jobs:
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Run server tests
run: yarn affine @affine/server test:coverage --forbid-only
- name: Run server tests with elasticsearch only
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
CI_NODE_INDEX: ${{ matrix.node_index }}
@@ -886,6 +956,10 @@ jobs:
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -981,6 +1055,10 @@ jobs:
image: redis
ports:
- 6379:6379
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -1003,6 +1081,7 @@ jobs:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- 'packages/frontend/core/src/blocksuite/ai/**'
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
- 'tests/affine-cloud-copilot/**'
- name: Setup Node.js

View File

@@ -59,6 +59,10 @@ jobs:
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -130,6 +134,10 @@ jobs:
image: redis
ports:
- 6379:6379
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4

View File

@@ -180,6 +180,7 @@ jobs:
- name: Testflight
if: ${{ env.BUILD_TYPE != 'stable' }}
working-directory: packages/frontend/apps/ios/App
continue-on-error: true
run: |
echo -n "${{ env.BUILD_PROVISION_PROFILE }}" | base64 --decode -o $PP_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles

2
.nvmrc
View File

@@ -1 +1 @@
22.15.0
22.15.1

View File

@@ -58,11 +58,14 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-drag-handle": "workspace:*",
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
"@blocksuite/affine-widget-edgeless-dragging-area": "workspace:*",
"@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-edgeless-zoom-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
"@blocksuite/affine-widget-linked-doc": "workspace:*",
"@blocksuite/affine-widget-note-slicer": "workspace:*",
"@blocksuite/affine-widget-page-dragging-area": "workspace:*",
"@blocksuite/affine-widget-remote-selection": "workspace:*",
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
@@ -178,6 +181,8 @@
"./widgets/drag-handle/view": "./src/widgets/drag-handle/view.ts",
"./widgets/edgeless-auto-connect": "./src/widgets/edgeless-auto-connect/index.ts",
"./widgets/edgeless-auto-connect/view": "./src/widgets/edgeless-auto-connect/view.ts",
"./widgets/edgeless-dragging-area": "./src/widgets/edgeless-dragging-area/index.ts",
"./widgets/edgeless-dragging-area/view": "./src/widgets/edgeless-dragging-area/view.ts",
"./widgets/edgeless-toolbar": "./src/widgets/edgeless-toolbar/index.ts",
"./widgets/edgeless-toolbar/view": "./src/widgets/edgeless-toolbar/view.ts",
"./widgets/frame-title": "./src/widgets/frame-title/index.ts",

View File

@@ -40,11 +40,14 @@ import { InlinePresetViewExtension } from '@blocksuite/affine-inline-preset/view
import { ReferenceViewExtension } from '@blocksuite/affine-inline-reference/view';
import { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/view';
import { EdgelessAutoConnectViewExtension } from '@blocksuite/affine-widget-edgeless-auto-connect/view';
import { EdgelessDraggingAreaViewExtension } from '@blocksuite/affine-widget-edgeless-dragging-area/view';
import { EdgelessSelectedRectViewExtension } from '@blocksuite/affine-widget-edgeless-selected-rect/view';
import { EdgelessToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-toolbar/view';
import { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view';
import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/view';
import { LinkedDocViewExtension } from '@blocksuite/affine-widget-linked-doc/view';
import { NoteSlicerViewExtension } from '@blocksuite/affine-widget-note-slicer/view';
import { PageDraggingAreaViewExtension } from '@blocksuite/affine-widget-page-dragging-area/view';
import { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view';
import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view';
@@ -112,6 +115,9 @@ export function getInternalViewExtensions() {
ViewportOverlayViewExtension,
EdgelessZoomToolbarViewExtension,
PageDraggingAreaViewExtension,
EdgelessSelectedRectViewExtension,
EdgelessDraggingAreaViewExtension,
NoteSlicerViewExtension,
// Fragment
DocTitleViewExtension,

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-edgeless-dragging-area';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-edgeless-dragging-area/view';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-edgeless-selected-rect';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-edgeless-selected-rect/view';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-note-slicer';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-note-slicer/view';

View File

@@ -55,11 +55,14 @@
{ "path": "../shared" },
{ "path": "../widgets/drag-handle" },
{ "path": "../widgets/edgeless-auto-connect" },
{ "path": "../widgets/edgeless-dragging-area" },
{ "path": "../widgets/edgeless-selected-rect" },
{ "path": "../widgets/edgeless-toolbar" },
{ "path": "../widgets/edgeless-zoom-toolbar" },
{ "path": "../widgets/frame-title" },
{ "path": "../widgets/keyboard-toolbar" },
{ "path": "../widgets/linked-doc" },
{ "path": "../widgets/note-slicer" },
{ "path": "../widgets/page-dragging-area" },
{ "path": "../widgets/remote-selection" },
{ "path": "../widgets/scroll-anchoring" },

View File

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

View File

@@ -17,7 +17,9 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
DocModeProvider,
FileSizeLimitProvider,
TelemetryProvider,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
@@ -143,7 +145,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.refreshData();
this.disposables.add(
this.model.props.sourceId$.subscribe(() => {
this.refreshData();
})
);
if (!this.model.props.style && !this.store.readonly) {
this.store.withoutTransact(() => {
@@ -183,6 +189,22 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
@click=${(event: MouseEvent) => {
event.stopPropagation();
onOverFileSize?.();
{
const mode =
this.std.get(DocModeProvider).getEditorMode() ?? 'page';
const segment = mode === 'page' ? 'doc' : 'whiteboard';
this.std
.getOptional(TelemetryProvider)
?.track('AttachmentUpgradedEvent', {
segment,
page: `${segment} editor`,
module: 'attachment',
control: 'upgrade',
category: 'card',
type: this.model.props.name.split('.').pop() ?? '',
});
}
}}
>
${UpgradeIcon()} Upgrade
@@ -198,6 +220,22 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
@click=${(event: MouseEvent) => {
event.stopPropagation();
this.refreshData();
{
const mode =
this.std.get(DocModeProvider).getEditorMode() ?? 'page';
const segment = mode === 'page' ? 'doc' : 'whiteboard';
this.std
.getOptional(TelemetryProvider)
?.track('AttachmentReloadedEvent', {
segment,
page: `${segment} editor`,
module: 'attachment',
control: 'reload',
category: 'card',
type: this.model.props.name.split('.').pop() ?? '',
});
}
}}
>
${ResetIcon()} Reload

View File

@@ -264,6 +264,12 @@ const builtinToolbarConfig = {
run(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.reload();
ctx.track('AttachmentReloadedEvent', {
...trackBaseProps,
control: 'reload',
type: block?.model.props.name.split('.').pop() ?? '',
});
},
},
{

View File

@@ -130,7 +130,7 @@ async function buildPropsWith(
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${mode} editor`,
module: 'attachment',
segment: 'attachment',
segment: mode,
control: 'uploader',
type,
category,

View File

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

View File

@@ -1,5 +1,3 @@
import '@blocksuite/affine-block-embed/effects';
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';

View File

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

View File

@@ -37,7 +37,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/code-painter.worker.ts",
"./view": "./src/view.ts",
"./store": "./src/store.ts"

View File

@@ -388,8 +388,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
override renderBlock(): TemplateResult<1> {
const showLineNumbers =
this.std.getOptional(CodeBlockConfigExtension.identifier)
?.showLineNumbers ?? true;
(this.std.getOptional(CodeBlockConfigExtension.identifier)
?.showLineNumbers ??
true) &&
(this.model.props.lineNumber ?? true);
const preview = !!this.model.props.preview;
const previewContext = this.std.getOptional(
@@ -403,6 +405,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
'affine-code-block-container': true,
mobile: IS_MOBILE,
wrap: this.model.props.wrap,
'disable-line-numbers': !showLineNumbers,
})}
>
<rich-text
@@ -420,16 +423,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
.enableUndoRedo=${false}
.wrapText=${this.model.props.wrap}
.verticalScrollContainerGetter=${() => getViewportElement(this.host)}
.vLineRenderer=${showLineNumbers
? (vLine: VLine) => {
return html`
<span contenteditable="false" class="line-number"
>${vLine.index + 1}</span
>
${vLine.renderVElements()}
`;
}
: undefined}
.vLineRenderer=${(vLine: VLine) => {
return html`
<span contenteditable="false" class="line-number"
>${vLine.index + 1}</span
>
${vLine.renderVElements()}
`;
}}
>
</rich-text>
<div

View File

@@ -0,0 +1,13 @@
import { toGfxBlockComponent } from '@blocksuite/std';
import { CodeBlockComponent } from './code-block.js';
export class CodeEdgelessBlockComponent extends toGfxBlockComponent(
CodeBlockComponent
) {}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-code': CodeEdgelessBlockComponent;
}
}

View File

@@ -9,10 +9,12 @@ import {
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
import { noop, sleep } from '@blocksuite/global/utils';
import { NumberedListIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { CodeBlockConfigExtension } from '../code-block-config.js';
import type { CodeBlockToolbarContext } from './context.js';
import { duplicateCodeBlock } from './utils.js';
@@ -148,6 +150,40 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
};
},
},
{
type: 'line-number',
when: ({ std }) =>
std.getOptional(CodeBlockConfigExtension.identifier)?.showLineNumbers ??
true,
generate: ({ blockComponent, close }) => {
return {
action: () => {},
render: () => {
const lineNumber = blockComponent.model.props.lineNumber ?? true;
const label = lineNumber ? 'Cancel line number' : 'Line number';
return html`
<editor-menu-action
@click=${() => {
blockComponent.store.updateBlock(blockComponent.model, {
lineNumber: !lineNumber,
});
close();
}}
aria-label=${label}
>
${NumberedListIcon()}
<span class="label">${label}</span>
<toggle-switch
style="margin-left: auto;"
.on="${lineNumber}"
></toggle-switch>
</editor-menu-action>
`;
},
};
},
},
{
type: 'duplicate',
label: 'Duplicate',

View File

@@ -1,4 +1,5 @@
import { CodeBlockComponent } from './code-block';
import { CodeEdgelessBlockComponent } from './code-edgeless-block';
import {
AFFINE_CODE_TOOLBAR_WIDGET,
AffineCodeToolbarWidget,
@@ -14,6 +15,7 @@ export function effects() {
customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget);
customElements.define('affine-code-unit', AffineCodeUnit);
customElements.define('affine-code', CodeBlockComponent);
customElements.define('affine-edgeless-code', CodeEdgelessBlockComponent);
customElements.define('preview-button', PreviewButton);
}

View File

@@ -50,6 +50,10 @@ export const codeBlockStyles = css`
user-select: none;
}
.affine-code-block-container.disable-line-numbers .line-number {
display: none;
}
affine-code .affine-code-block-preview {
padding: 12px;
}

View File

@@ -41,7 +41,11 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
context.register([
FlavourExtension('affine:code'),
CodeBlockHighlighter,
BlockViewExtension('affine:code', literal`affine-code`),
BlockViewExtension('affine:code', model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-code`
: literal`affine-code`;
}),
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
CodeKeymapExtension,
...getCodeClipboardExtensions(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,23 +52,25 @@ export const EmbedSyncedDocInteraction =
scale = newBound.w / realWidth;
}
const newWidth = newBound.w / scale;
newBound.w =
clamp(newWidth, constraint.minWidth, constraint.maxWidth) * scale;
clamp(
newBound.w / scale,
constraint.minWidth,
constraint.maxWidth
) * scale;
newBound.h =
clamp(newBound.h, constraint.minHeight, constraint.maxHeight) *
scale;
clamp(
newBound.h / scale,
constraint.minHeight,
constraint.maxHeight
) * scale;
const newHeight = newBound.h / scale;
// only adjust height check the fold state
if (originalBound.w === newBound.w) {
let preFoldHeight = 0;
if (newHeight === constraint.minHeight) {
preFoldHeight = initHeight;
}
model.props.preFoldHeight = preFoldHeight;
if (model.isFolded && newHeight > constraint.minHeight) {
model.props.preFoldHeight = 0;
} else if (!model.isFolded && newHeight <= constraint.minHeight) {
model.props.preFoldHeight = initHeight;
}
model.props.scale = scale;

View File

@@ -76,6 +76,8 @@ export function calcSyncedDocFullHeight(block: BlockComponent) {
const bottomPadding = 8;
return (
(headerHeight + contentHeight + bottomPadding) / block.gfx.viewport.zoom
(headerHeight + contentHeight + bottomPadding) /
block.gfx.viewport.zoom /
(block.model.props.scale ?? 1)
);
}

View File

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

View File

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

View File

@@ -185,9 +185,33 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error);
}
// Reset the flag when fully exiting presentation mode
this.edgeless.std
.get(EditPropsStore)
.setStorage('presentNoFrameToastShown', false);
}
private _moveToCurrentFrame() {
private _moveToCurrentFrame(forceMove = false) {
const currentToolOption = this.gfx.tool.currentToolOption$.value;
const toolOptions = currentToolOption?.options;
// If PresentTool is being activated after a temporary pan (indicated by restoredAfterPan)
// and a forced move isn't explicitly requested, skip moving to the current frame.
// This preserves the user's panned position instead of resetting to the frame's default view.
if (
currentToolOption?.toolType === PresentTool &&
toolOptions?.restoredAfterPan &&
!forceMove
) {
// Clear the flag so future navigations behave normally
this.gfx.tool.setTool(PresentTool, {
...toolOptions,
restoredAfterPan: false,
});
return;
}
const current = this._currentFrameIndex;
const viewport = this.gfx.viewport;
const frame = this._frames[current];
@@ -263,28 +287,56 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
_disposables.add(
effect(() => {
const currentTool = this.gfx.tool.currentToolOption$.value;
const selection = this.gfx.selection;
const currentToolOption = this.gfx.tool.currentToolOption$.value;
if (currentTool?.toolType === PresentTool) {
this._cachedIndex = this._currentFrameIndex;
this._navigatorMode =
(currentTool.options as ToolOptions<PresentTool>)?.mode ??
this._navigatorMode;
if (isFrameBlock(selection.selectedElements[0])) {
this._cachedIndex = this._frames.findIndex(
frame => frame.id === selection.selectedElements[0].id
);
if (currentToolOption?.toolType === PresentTool) {
const opts = currentToolOption.options as
| ToolOptions<PresentTool>
| undefined;
const isAlreadyFullscreen = !!document.fullscreenElement;
if (!isAlreadyFullscreen) {
this._toggleFullScreen();
} else {
this._fullScreenMode = true;
}
if (this._frames.length === 0)
toast(
this.host,
'The presentation requires at least 1 frame. You can firstly create a frame.',
5000
);
this._toggleFullScreen();
}
this._cachedIndex = this._currentFrameIndex;
this._navigatorMode = opts?.mode ?? this._navigatorMode;
const selection = this.gfx.selection;
if (
selection.selectedElements.length > 0 &&
isFrameBlock(selection.selectedElements[0])
) {
const selectedFrameId = selection.selectedElements[0].id;
const indexOfSelectedFrame = this._frames.findIndex(
frame => frame.id === selectedFrameId
);
if (indexOfSelectedFrame !== -1) {
this._cachedIndex = indexOfSelectedFrame;
}
}
const store = this.edgeless.std.get(EditPropsStore);
if (this._frames.length === 0) {
if (!store.getStorage('presentNoFrameToastShown')) {
toast(
this.host,
'The presentation requires at least 1 frame. You can firstly create a frame.',
5000
);
store.setStorage('presentNoFrameToastShown', true);
}
} else {
// If frames exist, and the flag was set, reset it.
// This allows the toast to show again if all frames are subsequently deleted.
if (store.getStorage('presentNoFrameToastShown')) {
store.setStorage('presentNoFrameToastShown', false);
}
}
}
this.requestUpdate();
})
);
@@ -305,12 +357,10 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
_disposables.addFromEvent(document, 'fullscreenchange', () => {
if (document.fullscreenElement) {
// When enter fullscreen, we need to set current frame to the cached index
this._timer = setTimeout(() => {
this._currentFrameIndex = this._cachedIndex;
}, 400);
} else {
// When exit fullscreen, we need to clear the timer
clearTimeout(this._timer);
if (
this.edgelessTool.toolType === PresentTool &&
@@ -324,7 +374,7 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
}
}
setTimeout(() => this._moveToCurrentFrame(), 400);
setTimeout(() => this._moveToCurrentFrame(true), 400);
this.slots.fullScreenToggled.next();
});
@@ -430,11 +480,29 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
}
protected override updated(changedProperties: PropertyValues) {
if (
changedProperties.has('_currentFrameIndex') &&
this.edgelessTool.toolType === PresentTool
) {
this._moveToCurrentFrame();
const currentToolOption = this.gfx.tool.currentToolOption$.value;
const isPresentToolActive = currentToolOption?.toolType === PresentTool;
const toolOptions = currentToolOption?.options;
const isRestoredAfterPan = !!(
isPresentToolActive && toolOptions?.restoredAfterPan
);
if (changedProperties.has('_currentFrameIndex') && isPresentToolActive) {
// When the current frame index changes (e.g., user navigates), a viewport update is needed.
// However, if PresentTool is merely being restored after a pan (isRestoredAfterPan = true)
// without an explicit index change in this update cycle, we avoid forcing a move to preserve the panned position.
// Thus, `forceMove` is true unless it's a pan restoration.
const shouldForceMove = !isRestoredAfterPan;
this._moveToCurrentFrame(shouldForceMove);
} else if (isPresentToolActive && changedProperties.has('edgelessTool')) {
// Handles cases where the tool is set/switched to PresentTool (e.g., initial activation or returning from another tool).
// Similar to frame index changes, avoid forcing a viewport move if restoring after a pan.
const currentToolIsPresentTool =
this.edgelessTool.toolType === PresentTool;
if (currentToolIsPresentTool) {
const shouldForceMoveOnToolChange = !isRestoredAfterPan;
this._moveToCurrentFrame(shouldForceMoveOnToolChange);
}
}
}

View File

@@ -4,6 +4,7 @@ import type { NavigatorMode } from './frame-manager';
export type PresentToolOption = {
mode?: NavigatorMode;
restoredAfterPan?: boolean;
};
export class PresentTool extends BaseTool<PresentToolOption> {

View File

@@ -77,18 +77,16 @@ export class EdgelessNavigatorSettingButton extends WithDisposable(LitElement) {
slots.navigatorSettingUpdated.next({
blackBackground: this.blackBackground,
});
this.edgeless.std
.get(EditPropsStore)
.setStorage('presentBlackBackground', checked);
};
private _tryRestoreSettings() {
const blackBackground = this.edgeless.std
.get(EditPropsStore)
.getStorage('presentBlackBackground');
this.blackBackground = blackBackground ?? true;
}
override connectedCallback() {
super.connectedCallback();
this._tryRestoreSettings();
this.blackBackground = blackBackground ?? false;
}
override disconnectedCallback(): void {
@@ -97,6 +95,7 @@ export class EdgelessNavigatorSettingButton extends WithDisposable(LitElement) {
}
override firstUpdated() {
if (this.edgeless) this._tryRestoreSettings();
this._navigatorSettingPopper = createButtonPopper({
reference: this._navigatorSettingButton,
popperElement: this._navigatorSettingMenu,
@@ -133,7 +132,7 @@ export class EdgelessNavigatorSettingButton extends WithDisposable(LitElement) {
<div class="text">Black background</div>
<toggle-switch
.subscribe=${this.blackBackground}
.on=${this.blackBackground}
.onChange=${this._onBlackBackgroundChange}
>
</toggle-switch>
@@ -143,7 +142,7 @@ export class EdgelessNavigatorSettingButton extends WithDisposable(LitElement) {
<div class="text">Hide toolbar</div>
<toggle-switch
.subscribe=${this.hideToolbar}
.on=${this.hideToolbar}
.onChange=${(checked: boolean) => {
this.onHideToolbarChange && this.onHideToolbarChange(checked);
}}
@@ -173,7 +172,7 @@ export class EdgelessNavigatorSettingButton extends WithDisposable(LitElement) {
private accessor _navigatorSettingMenu!: HTMLElement;
@state()
accessor blackBackground = true;
accessor blackBackground = false;
@property({ attribute: false })
accessor edgeless!: BlockComponent;

View File

@@ -34,7 +34,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/image-painter.worker.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"

View File

@@ -7,7 +7,7 @@ import {
} from '@blocksuite/affine-shared/commands';
import { ImageSelection } from '@blocksuite/affine-shared/selection';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import type { BlockComponent, UIEventStateContext } from '@blocksuite/std';
import {
BlockSelection,
@@ -15,8 +15,9 @@ import {
TextSelection,
} from '@blocksuite/std';
import type { BaseSelection } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
@@ -25,7 +26,9 @@ import { ImageResizeManager } from '../image-resize-manager';
import { shouldResizeImage } from '../utils';
import { ImageSelectedRect } from './image-selected-rect';
export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
export class ImageBlockPageComponent extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
affine-page-image {
position: relative;
@@ -68,6 +71,8 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
}
`;
resizeable$ = computed(() => this.block.resizeable$.value);
private _isDragging = false;
private get _doc() {
@@ -134,21 +139,21 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
return true;
},
Delete: ctx => {
if (this._host.store.readonly || !this._isSelected) return;
if (this._host.store.readonly || !this.resizeable$.peek()) return;
addParagraph(ctx);
this._doc.deleteBlock(this._model);
return true;
},
Backspace: ctx => {
if (this._host.store.readonly || !this._isSelected) return;
if (this._host.store.readonly || !this.resizeable$.peek()) return;
addParagraph(ctx);
this._doc.deleteBlock(this._model);
return true;
},
Enter: ctx => {
if (this._host.store.readonly || !this._isSelected) return;
if (this._host.store.readonly || !this.resizeable$.peek()) return;
addParagraph(ctx);
return true;
@@ -213,19 +218,6 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
private _handleSelection() {
const selection = this._host.selection;
this._disposables.add(
selection.slots.changed.subscribe(selList => {
this._isSelected = selList.some(
sel => sel.blockId === this.block.blockId && sel.is(ImageSelection)
);
})
);
this._disposables.add(
this._model.propsUpdated.subscribe(() => {
this.requestUpdate();
})
);
this._disposables.addFromEvent(
this.resizeImg,
@@ -249,7 +241,7 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
this.block.handleEvent(
'click',
() => {
if (!this._isSelected) return;
if (!this.resizeable$.peek()) return;
selection.update(selList =>
selList.filter(
@@ -356,7 +348,7 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
override render() {
const imageSize = this._normalizeImageSize();
const imageSelectedRect = this._isSelected
const imageSelectedRect = this.resizeable$.value
? ImageSelectedRect(this._doc.readonly)
: null;
@@ -389,9 +381,6 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
`;
}
@state()
accessor _isSelected = false;
@property({ attribute: false })
accessor block!: ImageBlockComponent;

View File

@@ -4,6 +4,7 @@ import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
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 {
ThemeProvider,
ToolbarRegistryIdentifier,
@@ -30,6 +31,13 @@ import {
enableOn: () => !IS_MOBILE,
})
export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel> {
resizeable$ = computed(() =>
this.std.selection.value.some(
selection =>
selection.is(ImageSelection) && selection.blockId === this.blockId
)
);
resourceController = new ResourceController(
computed(() => this.model.props.sourceId$.value),
'Image'
@@ -104,7 +112,11 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.refreshData();
this.disposables.add(
this.model.props.sourceId$.subscribe(() => {
this.refreshData();
})
);
}
override firstUpdated() {

View File

@@ -100,7 +100,11 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.refreshData();
this.disposables.add(
this.model.props.sourceId$.subscribe(() => {
this.refreshData();
})
);
}
override renderGfxBlock() {

View File

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

View File

@@ -36,7 +36,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/list-painter.worker.ts",
"./view": "./src/view.ts",
"./store": "./src/store.ts"

View File

@@ -39,7 +39,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/note-painter.worker.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"

View File

@@ -32,7 +32,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/paragraph-painter.worker.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"

View File

@@ -15,7 +15,6 @@
"@blocksuite/affine-block-database": "workspace:*",
"@blocksuite/affine-block-edgeless-text": "workspace:*",
"@blocksuite/affine-block-embed": "workspace:*",
"@blocksuite/affine-block-embed-doc": "workspace:*",
"@blocksuite/affine-block-frame": "workspace:*",
"@blocksuite/affine-block-image": "workspace:*",
"@blocksuite/affine-block-note": "workspace:*",
@@ -35,6 +34,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",

View File

@@ -3,6 +3,7 @@ import {
pasteMiddleware,
replaceIdMiddleware,
surfaceRefToEmbed,
uploadMiddleware,
} from '@blocksuite/affine-shared/adapters';
import {
clearAndSelectFirstModelCommand,
@@ -34,14 +35,17 @@ export class PageClipboard extends ReadOnlyClipboard {
// When pastina a surface-ref block to another doc
const surfaceRefToEmbedMiddleware = surfaceRefToEmbed(this.std);
const replaceId = replaceIdMiddleware(this.std.store.workspace.idGenerator);
const upload = uploadMiddleware(this.std);
this.std.clipboard.use(paste);
this.std.clipboard.use(surfaceRefToEmbedMiddleware);
this.std.clipboard.use(replaceId);
this.std.clipboard.use(upload);
this._disposables.add({
dispose: () => {
this.std.clipboard.unuse(paste);
this.std.clipboard.unuse(surfaceRefToEmbedMiddleware);
this.std.clipboard.unuse(replaceId);
this.std.clipboard.unuse(upload);
},
});
};

View File

@@ -1,65 +0,0 @@
import type {
CursorType,
ResizeHandle,
StandardCursor,
} from '@blocksuite/std/gfx';
const rotateCursorMap: {
[key in ResizeHandle]: number;
} = {
'top-right': 0,
'bottom-right': 90,
'bottom-left': 180,
'top-left': 270,
// not used
left: 0,
right: 0,
top: 0,
bottom: 0,
};
export function generateCursorUrl(
angle = 0,
handle: ResizeHandle,
fallback: StandardCursor = 'default'
): CursorType {
angle = ((angle % 360) + 360) % 360;
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg transform='rotate(${rotateCursorMap[handle] + angle} 16 16)'%3E%3Cpath fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/%3E%3Cpath d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/%3E%3C/g%3E%3C/svg%3E") 16 16, ${fallback}`;
}
const handleToRotateMap: {
[key in ResizeHandle]: number;
} = {
'top-left': 45,
'top-right': 135,
'bottom-right': 45,
'bottom-left': 135,
left: 0,
right: 0,
top: 90,
bottom: 90,
};
const rotateToHandleMap: {
[key: number]: StandardCursor;
} = {
0: 'ew-resize',
45: 'nwse-resize',
90: 'ns-resize',
135: 'nesw-resize',
};
export function getRotatedResizeCursor(option: {
handle: ResizeHandle;
angle: number;
}) {
const angle =
(Math.round(
(handleToRotateMap[option.handle] + ((option.angle + 360) % 360)) / 45
) %
4) *
45;
return rotateToHandleMap[angle] || 'default';
}

View File

@@ -1,7 +1,11 @@
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
import { FrameTool } from '@blocksuite/affine-block-frame';
import { DefaultTool, isNoteBlock } from '@blocksuite/affine-block-surface';
import {
DefaultTool,
EdgelessLegacySlotIdentifier,
isNoteBlock,
} from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import {
BrushTool,
@@ -47,6 +51,7 @@ import { SurfaceSelection, TextSelection } from '@blocksuite/std';
import {
type BaseTool,
GfxBlockElementModel,
GfxControllerIdentifier,
type GfxPrimitiveElementModel,
isGfxGroupCompatibleModel,
type ToolOptions,
@@ -66,7 +71,15 @@ import { isCanvasElement } from './utils/query.js';
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
get gfx() {
return this.rootComponent.gfx;
return this.std.get(GfxControllerIdentifier);
}
get slots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
get std() {
return this.rootComponent.std;
}
constructor(override rootComponent: EdgelessRootBlockComponent) {
@@ -118,7 +131,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
NoteBlockModel,
])
) {
rootComponent.slots.toggleNoteSlicer.next();
this.slots.toggleNoteSlicer.next();
}
},
f: () => {
@@ -153,7 +166,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
elements.length === 1 &&
isNoteBlock(elements[0])
) {
rootComponent.slots.toggleNoteSlicer.next();
this.slots.toggleNoteSlicer.next();
}
},
'@': () => {
@@ -454,6 +467,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const selection = gfx.selection;
if (event.code === 'Space' && !event.repeat) {
const currentToolName =
this.rootComponent.gfx.tool.currentToolName$.peek();
if (currentToolName === 'frameNavigator') return false;
this._space(event);
} else if (
!selection.editing &&
@@ -491,8 +507,12 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
ctx => {
const event = ctx.get('keyboardState').raw;
if (event.code === 'Space' && !event.repeat) {
const currentToolName =
this.rootComponent.gfx.tool.currentToolName$.peek();
if (currentToolName === 'frameNavigator') return false;
this._space(event);
}
return false;
},
{ global: true }
);
@@ -705,7 +725,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
}
this._setEdgelessTool(PanTool, { panning: false });
edgeless.dispatcher.disposables.addFromEvent(
this.std.event.disposables.addFromEvent(
document,
'keyup',
revertToPrevTool

View File

@@ -1,7 +1,6 @@
import { NoteConfigExtension } from '@blocksuite/affine-block-note';
import {
DefaultTool,
EdgelessLegacySlotIdentifier,
getBgGridGap,
normalizeWheelDeltaY,
type SurfaceBlockComponent,
@@ -45,7 +44,6 @@ import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js';
import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { isCanvasElement } from './utils/query.js';
@@ -121,43 +119,27 @@ export class EdgelessRootBlockComponent extends BlockComponent<
keyboardManager: EdgelessPageKeyboardManager | null = null;
get dispatcher() {
return this.std.event;
}
get fontLoader() {
return this.std.get(FontLoaderService);
}
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
get selectedRectWidget() {
return this.host.view.getWidget(
'edgeless-selected-rect',
this.host.id
) as EdgelessSelectedRectWidget;
}
get slots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
get surfaceBlockModel() {
return this.model.children.find(
child => child.flavour === 'affine:surface'
) as SurfaceBlockModel;
}
get viewportElement(): HTMLElement {
private get _viewportElement(): HTMLElement {
return this.std.get(ViewportElementProvider).viewportElement;
}
get fontLoader() {
return this.std.get(FontLoaderService);
}
private _initFontLoader() {
this.std
.get(FontLoaderService)
.ready.then(() => {
this.fontLoader.ready
.then(() => {
this.surface.refresh();
})
.catch(console.error);
@@ -181,7 +163,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _initPanEvent() {
this.disposables.add(
this.dispatcher.add('pan', ctx => {
this.std.event.add('pan', ctx => {
const { viewport } = this.gfx;
if (viewport.locked) return;
@@ -205,7 +187,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _initPinchEvent() {
this.disposables.add(
this.dispatcher.add('pinch', ctx => {
this.std.event.add('pinch', ctx => {
const { viewport } = this.gfx;
if (viewport.locked) return;
@@ -285,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
this.gfx.viewport.onResize();
});
resizeObserver.observe(this.viewportElement);
resizeObserver.observe(this._viewportElement);
this._resizeObserver = resizeObserver;
}
@@ -348,7 +330,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _initWheelEvent() {
this._disposables.add(
this.dispatcher.add('wheel', ctx => {
this.std.event.add('wheel', ctx => {
const config = this.std.getOptional(EditorSettingProvider)?.setting$;
const state = ctx.get('defaultState');
const e = state.event as WheelEvent;

View File

@@ -1,26 +1,5 @@
import { LifeCycleWatcher, WidgetViewExtension } from '@blocksuite/std';
import { LifeCycleWatcher } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
export const edgelessDraggingAreaWidget = WidgetViewExtension(
'affine:page',
EDGELESS_DRAGGING_AREA_WIDGET,
literal`${unsafeStatic(EDGELESS_DRAGGING_AREA_WIDGET)}`
);
export const noteSlicerWidget = WidgetViewExtension(
'affine:page',
NOTE_SLICER_WIDGET,
literal`${unsafeStatic(NOTE_SLICER_WIDGET)}`
);
export const edgelessSelectedRectWidget = WidgetViewExtension(
'affine:page',
EDGELESS_SELECTED_RECT_WIDGET,
literal`${unsafeStatic(EDGELESS_SELECTED_RECT_WIDGET)}`
);
export class EdgelessLocker extends LifeCycleWatcher {
static override key = 'edgeless-locker';

View File

@@ -1,7 +1,7 @@
export { EdgelessRootPreviewBlockComponent } from '../preview/edgeless-root-preview-block';
export * from './clipboard/clipboard';
export * from './clipboard/command';
export * from './edgeless-root-block.js';
export { EdgelessRootPreviewBlockComponent } from './edgeless-root-preview-block.js';
export { EdgelessRootService } from './edgeless-root-service.js';
export * from './utils/clipboard-utils.js';
export { sortEdgelessElements } from './utils/clone-utils.js';

View File

@@ -1,17 +1,3 @@
import { EdgelessAutoCompletePanel } from './edgeless/components/auto-complete/auto-complete-panel.js';
import { EdgelessAutoComplete } from './edgeless/components/auto-complete/edgeless-auto-complete.js';
import {
NOTE_SLICER_WIDGET,
NoteSlicer,
} from './edgeless/components/note-slicer/index.js';
import {
EDGELESS_DRAGGING_AREA_WIDGET,
EdgelessDraggingAreaRectWidget,
} from './edgeless/components/rects/edgeless-dragging-area-rect.js';
import {
EDGELESS_SELECTED_RECT_WIDGET,
EdgelessSelectedRectWidget,
} from './edgeless/components/rects/edgeless-selected-rect.js';
import {
EdgelessRootBlockComponent,
EdgelessRootPreviewBlockComponent,
@@ -22,7 +8,6 @@ import {
export function effects() {
// Register components by category
registerRootComponents();
registerMiscComponents();
}
function registerRootComponents() {
@@ -35,37 +20,9 @@ function registerRootComponents() {
);
}
function registerMiscComponents() {
// Auto-complete components
customElements.define(
'edgeless-auto-complete-panel',
EdgelessAutoCompletePanel
);
customElements.define('edgeless-auto-complete', EdgelessAutoComplete);
// Note and template components
customElements.define(NOTE_SLICER_WIDGET, NoteSlicer);
// Dragging area components
customElements.define(
EDGELESS_DRAGGING_AREA_WIDGET,
EdgelessDraggingAreaRectWidget
);
customElements.define(
EDGELESS_SELECTED_RECT_WIDGET,
EdgelessSelectedRectWidget
);
}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-root': EdgelessRootBlockComponent;
'affine-edgeless-root-preview': EdgelessRootPreviewBlockComponent;
'edgeless-auto-complete-panel': EdgelessAutoCompletePanel;
'edgeless-auto-complete': EdgelessAutoComplete;
'note-slicer': NoteSlicer;
'edgeless-dragging-area-rect': EdgelessDraggingAreaRectWidget;
'edgeless-selected-rect': EdgelessSelectedRectWidget;
'affine-page-root': PageRootBlockComponent;
}
}

View File

@@ -25,13 +25,9 @@ import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { isCanvasElement } from './utils/query.js';
import { isCanvasElement } from '../edgeless/utils/query';
export class EdgelessRootPreviewBlockComponent extends BlockComponent<
RootBlockModel,
EdgelessRootService
> {
export class EdgelessRootPreviewBlockComponent extends BlockComponent<RootBlockModel> {
static override styles = css`
affine-edgeless-root-preview {
pointer-events: none;
@@ -66,9 +62,13 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
}
`;
private get _viewport() {
return this._gfx.viewport;
}
private readonly _refreshLayerViewport = requestThrottledConnectedFrame(
() => {
const { zoom, translateX, translateY } = this.service.viewport;
const { zoom, translateX, translateY } = this._viewport;
const gap = getBgGridGap(zoom);
this.backgroundStyle = {
@@ -81,10 +81,6 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
private _resizeObserver: ResizeObserver | null = null;
get dispatcher() {
return this.service?.uiEventDispatcher;
}
private get _gfx() {
return this.std.get(GfxControllerIdentifier);
}

View File

@@ -18,12 +18,7 @@ import { PageClipboard, ReadOnlyClipboard } from './clipboard';
import { builtinToolbarConfig } from './configs/toolbar';
import { EdgelessClipboardController, EdgelessRootService } from './edgeless';
import { EdgelessElementToolbarExtension } from './edgeless/configs/toolbar';
import {
edgelessDraggingAreaWidget,
EdgelessLocker,
edgelessSelectedRectWidget,
noteSlicerWidget,
} from './edgeless/edgeless-root-spec';
import { EdgelessLocker } from './edgeless/edgeless-root-spec';
import { AltCloneExtension } from './edgeless/interact-extensions/clone-ext';
import { effects } from './effects';
import { fallbackKeymap } from './keyboard/keymap';
@@ -90,9 +85,6 @@ export class RootViewExtension extends ViewExtensionProvider {
}
context.register([
BlockViewExtension('affine:page', literal`affine-edgeless-root`),
edgelessDraggingAreaWidget,
noteSlicerWidget,
edgelessSelectedRectWidget,
EdgelessClipboardController,
AltCloneExtension,
]);

View File

@@ -12,7 +12,6 @@
{ "path": "../database" },
{ "path": "../edgeless-text" },
{ "path": "../embed" },
{ "path": "../embed-doc" },
{ "path": "../frame" },
{ "path": "../image" },
{ "path": "../note" },
@@ -32,6 +31,7 @@
{ "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../widgets/edgeless-selected-rect" },
{ "path": "../../widgets/edgeless-toolbar" },
{ "path": "../../data-view" },
{ "path": "../../../framework/global" },

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ export const SurfaceBlockSchema = defineBlockSchema({
'affine:attachment',
'affine:embed-*',
'affine:edgeless-text',
'affine:code',
],
},
transformer: transformerConfigs =>

View File

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

View File

@@ -0,0 +1,173 @@
import {
FileIconAepIcon,
FileIconAiIcon,
FileIconAviIcon,
FileIconCssIcon,
FileIconCsvIcon,
FileIconDmgIcon,
FileIconDocIcon,
FileIconDocxIcon,
FileIconEpsIcon,
FileIconExeIcon,
FileIconFigIcon,
FileIconGifIcon,
FileIconHtmlIcon,
FileIconInddIcon,
FileIconJavaIcon,
FileIconJpegIcon,
FileIconJsIcon,
FileIconJsonIcon,
FileIconMkvIcon,
FileIconMp3Icon,
FileIconMp4Icon,
FileIconMpegIcon,
FileIconNoneIcon,
FileIconPdfIcon,
FileIconPngIcon,
FileIconPptIcon,
FileIconPptxIcon,
FileIconPsdIcon,
FileIconRarIcon,
FileIconRssIcon,
FileIconSqlIcon,
FileIconSvgIcon,
FileIconTiffIcon,
FileIconTxtIcon,
FileIconWavIcon,
FileIconWebpIcon,
FileIconXlsIcon,
FileIconXlsxIcon,
FileIconXmlIcon,
FileIconZipIcon,
ImageIcon,
} from '@blocksuite/icons/rc';
export function getAttachmentFileIconRC(filetype: string) {
switch (filetype) {
case 'img':
return ImageIcon;
case 'image/jpeg':
case 'jpg':
case 'jpeg':
return FileIconJpegIcon;
case 'image/png':
case 'png':
return FileIconPngIcon;
case 'image/webp':
case 'webp':
return FileIconWebpIcon;
case 'image/tiff':
case 'tiff':
return FileIconTiffIcon;
case 'image/gif':
case 'gif':
return FileIconGifIcon;
case 'image/svg':
case 'svg':
return FileIconSvgIcon;
case 'image/eps':
case 'eps':
return FileIconEpsIcon;
case 'application/pdf':
case 'pdf':
return FileIconPdfIcon;
case 'application/msword':
case 'application/x-iwork-pages-sffpages':
case 'doc':
return FileIconDocIcon;
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case 'docx':
return FileIconDocxIcon;
case 'text/plain':
case 'txt':
return FileIconTxtIcon;
case 'csv':
return FileIconCsvIcon;
case 'application/vnd.ms-excel':
case 'xls':
return FileIconXlsIcon;
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case 'application/x-iwork-numbers-sffnumbers':
case 'xlsx':
return FileIconXlsxIcon;
case 'application/vnd.ms-powerpoint':
case 'application/x-iwork-keynote-sffkeynote':
case 'ppt':
return FileIconPptIcon;
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
case 'pptx':
return FileIconPptxIcon;
case 'application/illustrator':
case 'fig':
return FileIconFigIcon;
case 'application/postscript':
case 'ai':
return FileIconAiIcon;
case 'application/vnd.adobe.photoshop':
case 'psd':
return FileIconPsdIcon;
case 'application/vnd.adobe.indesign':
case 'indd':
return FileIconInddIcon;
case 'application/vnd.adobe.afterfx':
case 'aep':
return FileIconAepIcon;
case 'audio/mpeg':
case 'audio/mp3':
case 'mp3':
return FileIconMp3Icon;
case 'audio/wav':
case 'wav':
return FileIconWavIcon;
case 'video/mpeg':
case 'mpeg':
return FileIconMpegIcon;
case 'video/mp4':
case 'mp4':
return FileIconMp4Icon;
case 'video/avi':
case 'avi':
return FileIconAviIcon;
case 'video/mkv':
case 'mkv':
return FileIconMkvIcon;
case 'text/html':
case 'html':
return FileIconHtmlIcon;
case 'text/css':
case 'css':
return FileIconCssIcon;
case 'application/rss+xml':
case 'rss':
return FileIconRssIcon;
case 'application/sql':
case 'sql':
return FileIconSqlIcon;
case 'application/javascript':
case 'js':
return FileIconJsIcon;
case 'application/json':
case 'json':
return FileIconJsonIcon;
case 'application/java':
case 'java':
return FileIconJavaIcon;
case 'application/xml':
case 'xml':
return FileIconXmlIcon;
case 'application/x-msdos-program':
case 'exe':
return FileIconExeIcon;
case 'application/x-apple-diskimage':
case 'dmg':
return FileIconDmgIcon;
case 'application/zip':
case 'zip':
return FileIconZipIcon;
case 'application/x-rar-compressed':
case 'rar':
return FileIconRarIcon;
default:
return FileIconNoneIcon;
}
}

View File

@@ -1,5 +1,6 @@
export * from './ai.js';
export * from './file-icons.js';
export * from './file-icons-rc';
export * from './import-export.js';
export * from './list.js';
export * from './loading.js';

View File

@@ -35,6 +35,7 @@ export type ResolvedStateInfo = StateInfo & ResolvedStateInfoPart;
export class ResourceController implements Disposable {
readonly blobUrl$ = signal<string | null>(null);
// TODO(@fundon): default `loading` status.
readonly state$ = signal<Partial<BlobState>>({});
readonly resolvedState$ = computed<ResolvedStateInfoPart>(() => {

View File

@@ -32,7 +32,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

@@ -34,7 +34,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

@@ -33,7 +33,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

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

View File

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

View File

@@ -36,6 +36,8 @@ export class ConnectorFilter extends InteractivityExtension {
elements.sort((a, _) => (a instanceof ConnectorElementModel ? -1 : 1));
}
context.elements = elements;
return {};
});
}

View File

@@ -188,6 +188,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
});
this._resizeObserver.observe(this.richText);
this.connector.stash('labelXYWH');
this.updateComplete
.then(() => {
if (!this.inlineEditor) return;
@@ -257,7 +259,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
}
}
connector.lableEditing = false;
connector.labelEditing = false;
connector.pop('labelXYWH');
selection.set({
elements: [],
@@ -293,7 +296,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
}
);
connector.lableEditing = true;
connector.labelEditing = true;
})
.catch(console.error);
}

View File

@@ -142,6 +142,8 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
if (!curLabelElement) {
curLabelElement = labelElement;
labelElement.id = `#${this.model.id}-label`;
labelElement.creator = this.model;
labelElement.fillColor = 'transparent';
labelElement.strokeColor = 'transparent';
labelElement.strokeWidth = 0;

View File

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

View File

@@ -37,7 +37,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

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

View File

@@ -37,7 +37,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

@@ -34,7 +34,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

@@ -1,8 +1,13 @@
import { on } from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/std';
import { BaseTool, MouseButton } from '@blocksuite/std/gfx';
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
import { Signal } from '@preact/signals-core';
interface RestorablePresentToolOptions {
mode?: string; // 'fit' | 'fill', simplified to string for local use
restoredAfterPan?: boolean;
}
export type PanToolOption = {
panning: boolean;
};
@@ -53,14 +58,30 @@ export class PanTool extends BaseTool<PanToolOption> {
evt.raw.preventDefault();
const selection = this.gfx.selection.surfaceSelections;
const currentTool = this.controller.currentToolOption$.peek();
const restoreToPrevious = () => {
const { toolType, options } = currentTool;
if (toolType && options) {
this.controller.setTool(toolType, options);
this.gfx.selection.set(selection);
const { toolType, options: originalToolOptions } = currentTool;
const selectionToRestore = this.gfx.selection.surfaceSelections;
if (!toolType) return;
let finalOptions: ToolOptions<BaseTool<any>> | undefined =
originalToolOptions;
const PRESENT_TOOL_NAME = 'frameNavigator';
if (toolType.toolName === PRESENT_TOOL_NAME) {
// When restoring PresentTool (frameNavigator) after a temporary pan (e.g., via middle mouse button),
// set 'restoredAfterPan' to true. This allows PresentTool to avoid an unwanted viewport reset
// and maintain the panned position.
const currentPresentOptions = originalToolOptions as
| RestorablePresentToolOptions
| undefined;
finalOptions = {
...currentPresentOptions,
restoredAfterPan: true,
} as RestorablePresentToolOptions;
}
this.controller.setTool(toolType, finalOptions);
this.gfx.selection.set(selectionToRestore);
};
this.controller.setTool(PanTool, {

View File

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

View File

@@ -6,10 +6,16 @@ import { manageClassNames, setStyles } from './utils';
function applyShapeSpecificStyles(
model: ShapeElementModel,
element: HTMLElement
element: HTMLElement,
zoom: number
) {
if (model.shapeType === 'rect') {
element.style.borderRadius = `${model.radius ?? 0}px`;
const w = model.w * zoom;
const h = model.h * zoom;
const r = model.radius ?? 0;
const borderRadius =
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
element.style.borderRadius = borderRadius;
} else if (model.shapeType === 'ellipse') {
element.style.borderRadius = '50%';
} else {
@@ -20,11 +26,12 @@ function applyShapeSpecificStyles(
function applyBorderStyles(
model: ShapeElementModel,
element: HTMLElement,
strokeColor: string
strokeColor: string,
zoom: number
) {
element.style.border =
model.strokeStyle !== 'none'
? `${model.strokeWidth}px ${model.strokeStyle === 'dash' ? 'dashed' : 'solid'} ${strokeColor}`
? `${model.strokeWidth * zoom}px ${model.strokeStyle === 'dash' ? 'dashed' : 'solid'} ${strokeColor}`
: 'none';
}
@@ -85,11 +92,11 @@ export const shapeDomRenderer = (
element.style.width = `${model.w * zoom}px`;
element.style.height = `${model.h * zoom}px`;
applyShapeSpecificStyles(model, element);
applyShapeSpecificStyles(model, element, zoom);
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
applyBorderStyles(model, element, strokeColor);
applyBorderStyles(model, element, strokeColor, zoom);
applyTransformStyles(model, element);
element.style.boxSizing = 'border-box';

View File

@@ -36,7 +36,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,6 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [

View File

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

View File

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

View File

@@ -1,3 +1,8 @@
import {
type GfxCommonBlockProps,
GfxCompatible,
type GfxElementGeometry,
} from '@blocksuite/std/gfx';
import {
BlockModel,
BlockSchemaExtension,
@@ -13,7 +18,9 @@ type CodeBlockProps = {
wrap: boolean;
caption: string;
preview?: boolean;
} & BlockMeta;
lineNumber?: boolean;
} & BlockMeta &
GfxCommonBlockProps;
export const CodeBlockSchema = defineBlockSchema({
flavour: 'affine:code',
@@ -24,10 +31,15 @@ export const CodeBlockSchema = defineBlockSchema({
wrap: false,
caption: '',
preview: undefined,
lineNumber: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,
'meta:updatedBy': undefined,
xywh: '[0,0,16,16]',
index: 'a0',
scale: 1,
rotate: 0,
}) as CodeBlockProps,
metadata: {
version: 1,
@@ -37,6 +49,7 @@ export const CodeBlockSchema = defineBlockSchema({
'affine:paragraph',
'affine:list',
'affine:edgeless-text',
'affine:surface',
],
children: [],
},
@@ -45,4 +58,6 @@ export const CodeBlockSchema = defineBlockSchema({
export const CodeBlockSchemaExtension = BlockSchemaExtension(CodeBlockSchema);
export class CodeBlockModel extends BlockModel<CodeBlockProps> {}
export class CodeBlockModel
extends GfxCompatible<CodeBlockProps>(BlockModel)
implements GfxElementGeometry {}

View File

@@ -11,6 +11,10 @@ export type EmbedSyncedDocBlockProps = {
style: EmbedCardStyle;
caption?: string | null;
scale?: number;
/**
* Record the scaled height of the synced doc block when it is folded,
* a.k.a the fourth number of the `xywh`
*/
preFoldHeight?: number;
} & ReferenceInfo &
GfxCompatibleProps;

View File

@@ -278,7 +278,13 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
}
hasLabel() {
return Boolean(!this.lableEditing && this.labelDisplay && this.labelXYWH);
return Boolean(
!this.labelEditing &&
this.labelDisplay &&
this.labelXYWH &&
this.text &&
this.text.length
);
}
override includesPoint(
@@ -450,7 +456,7 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
* Local control display and hide, mainly used in editing scenarios.
*/
@local()
accessor lableEditing: boolean = false;
accessor labelEditing: boolean = false;
@field()
accessor mode: ConnectorMode = DEFAULT_CONNECTOR_MODE;

View File

@@ -1,5 +1,5 @@
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { sha } from '@blocksuite/global/utils';
import {
type AssetsManager,
BaseAdapter,
@@ -88,40 +88,49 @@ export class AttachmentAdapter extends BaseAdapter<Attachment> {
);
}
override async toSliceSnapshot(
payload: AttachmentToSliceSnapshotPayload
): Promise<SliceSnapshot | null> {
override async toSliceSnapshot({
assets,
file: files,
pageId,
workspaceId,
}: AttachmentToSliceSnapshotPayload): Promise<SliceSnapshot | null> {
if (files.length === 0) return null;
const content: SliceSnapshot['content'] = [];
for (const item of payload.file) {
const blobId = await sha(await item.arrayBuffer());
payload.assets?.getAssets().set(blobId, item);
await payload.assets?.writeToBlob(blobId);
const flavour = AttachmentBlockSchema.model.flavour;
for (const blob of files) {
const id = nanoid();
const { name, size, type } = blob;
assets?.uploadingAssetsMap.set(id, {
blob,
mapInto: sourceId => ({ sourceId }),
});
content.push({
type: 'block',
flavour: 'affine:attachment',
id: nanoid(),
flavour,
id,
props: {
name: item.name,
size: item.size,
type: item.type,
name,
size,
type,
embed: false,
style: 'horizontalThin',
index: 'a0',
xywh: '[0,0,0,0]',
rotate: 0,
sourceId: blobId,
},
children: [],
});
}
if (content.length === 0) {
return null;
}
return {
type: 'slice',
content,
workspaceId: payload.workspaceId,
pageId: payload.pageId,
pageId,
workspaceId,
};
}
}

View File

@@ -1,5 +1,5 @@
import { ImageBlockSchema } from '@blocksuite/affine-model';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { sha } from '@blocksuite/global/utils';
import {
type AssetsManager,
BaseAdapter,
@@ -88,32 +88,40 @@ export class ImageAdapter extends BaseAdapter<Image> {
);
}
override async toSliceSnapshot(
payload: ImageToSliceSnapshotPayload
): Promise<SliceSnapshot | null> {
override async toSliceSnapshot({
assets,
file: files,
pageId,
workspaceId,
}: ImageToSliceSnapshotPayload): Promise<SliceSnapshot | null> {
if (files.length === 0) return null;
const content: SliceSnapshot['content'] = [];
for (const item of payload.file) {
const blobId = await sha(await item.arrayBuffer());
payload.assets?.getAssets().set(blobId, item);
await payload.assets?.writeToBlob(blobId);
const flavour = ImageBlockSchema.model.flavour;
for (const blob of files) {
const id = nanoid();
const { size } = blob;
assets?.uploadingAssetsMap.set(id, {
blob,
mapInto: sourceId => ({ sourceId }),
});
content.push({
type: 'block',
flavour: 'affine:image',
id: nanoid(),
props: {
sourceId: blobId,
},
flavour,
id,
props: { size },
children: [],
});
}
if (content.length === 0) {
return null;
}
return {
type: 'slice',
content,
workspaceId: payload.workspaceId,
pageId: payload.pageId,
pageId,
workspaceId,
};
}
}

View File

@@ -9,3 +9,4 @@ export * from './proxy';
export * from './replace-id';
export * from './surface-ref-to-embed';
export * from './title';
export * from './upload';

View File

@@ -19,7 +19,7 @@ import { matchModels } from '../../utils';
export const replaceIdMiddleware =
(idGenerator: () => string): TransformerMiddleware =>
({ slots, docCRUD }) => {
({ slots, docCRUD, assetsManager }) => {
const idMap = new Map<string, string>();
// After Import
@@ -169,6 +169,16 @@ export const replaceIdMiddleware =
}
snapshot.id = newId;
// Should be re-paired.
if (['affine:attachment', 'affine:image'].includes(snapshot.flavour)) {
if (!assetsManager.uploadingAssetsMap.has(original)) return;
const data = assetsManager.uploadingAssetsMap.get(original)!;
assetsManager.uploadingAssetsMap.set(newId, data);
assetsManager.uploadingAssetsMap.delete(original);
return;
}
if (snapshot.flavour === 'affine:surface') {
// Generate new IDs for images and frames in advance.
snapshot.children.forEach(child => {

View File

@@ -0,0 +1,114 @@
import { sha } from '@blocksuite/global/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type {
BlockModel,
BlockProps,
TransformerMiddleware,
} from '@blocksuite/store';
import { filter, from, map, mergeMap } from 'rxjs';
const ALLOWED_FLAVOURS = new Set(['affine:attachment', 'affine:image']);
export const uploadMiddleware = (
std: BlockStdScope,
concurrent = 5
): TransformerMiddleware => {
const blockView$ = std.view.viewUpdated.pipe(
filter(payload => payload.type === 'block'),
filter(payload => ALLOWED_FLAVOURS.has(payload.view.model.flavour))
);
return ({ assetsManager }) => {
async function upload(
model: BlockModel,
{
blob,
mapInto,
abortController,
}: {
blob: Blob;
mapInto: (blobId: string) => Partial<BlockProps>;
abortController?: AbortController;
}
) {
if (!abortController) return null;
const signal = abortController.signal;
if (signal.aborted) return null;
// Double check
if (!model.store.hasBlock(model.id)) return null;
try {
signal.throwIfAborted();
const blobId = await Promise.race([
(async function processUpload() {
const blobId = await sha(await blob.arrayBuffer());
assetsManager.getAssets().set(blobId, blob);
await assetsManager.writeToBlob(blobId);
return await new Promise<string | null>(resolve => {
model.store.withoutTransact(() => {
if (signal.aborted) return resolve(null);
model.store.updateBlock(model, mapInto(blobId));
resolve(blobId);
});
});
})(),
// If the signal is not aborted, it will be in the pending state.
new Promise<null>(resolve => {
signal.addEventListener('abort', () => resolve(null), {
once: true,
});
if (signal.aborted) {
resolve(null);
}
}),
]);
return blobId;
} catch (err) {
console.error(err);
return null;
}
}
blockView$
.pipe(
map(payload => {
if (assetsManager.uploadingAssetsMap.size === 0) return null;
const model = payload.view.model;
if (!assetsManager.uploadingAssetsMap.has(model.id)) return null;
const state = assetsManager.uploadingAssetsMap.get(model.id)!;
if (payload.method === 'add') {
state.abortController = new AbortController();
return { model, state };
} else {
state.abortController?.abort();
assetsManager.uploadingAssetsMap.delete(model.id);
return null;
}
}),
filter(Boolean),
mergeMap(
({ model, state }) =>
from(
upload(model, state).then(() => {
assetsManager.uploadingAssetsMap.delete(model.id);
})
),
concurrent
)
)
.subscribe();
};
};

View File

@@ -41,6 +41,7 @@ const LocalPropsSchema = z.object({
presentBlackBackground: z.boolean(),
presentFillScreen: z.boolean(),
presentHideToolbar: z.boolean(),
presentNoFrameToastShown: z.boolean(),
autoHideEmbedHTMLFullScreenToolbar: z.boolean(),
});
@@ -126,6 +127,8 @@ export class EditPropsStore extends LifeCycleWatcher {
return 'blocksuite:presentation:fillScreen';
case 'presentHideToolbar':
return 'blocksuite:presentation:hideToolbar';
case 'presentNoFrameToastShown':
return 'blocksuite:presentation:noFrameToastShown';
case 'templateCache':
return 'blocksuite:' + id + ':templateTool';
case 'remoteColor':

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