Compare commits

...

76 Commits

Author SHA1 Message Date
darkskygit
9220b973c7 feat(server): increase embedding jobs concurrency & handle empty content after trim (#12574)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Improvements**
  - Increased the default concurrency for background tasks, enhancing processing efficiency.
  - Improved handling of empty or unsupported documents to ensure consistent processing.
  - Optimized document filtering to exclude certain documents from processing, improving performance.

- **Bug Fixes**
  - Enhanced detection of empty document summaries, reducing errors during processing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 14:28:34 +00:00
Saul-Mirone
7eb6b268a6 fix(editor): auto focus between tab switch (#12572)
Closes: BS-2290

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

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved focus behavior when switching between tabs to prevent unwanted automatic focusing of the content-editable area.
  - Enhanced selection clearing to avoid unnecessary blurring when the main editable element is already focused.
  - Refined focus checks in tests to specifically target contenteditable elements, ensuring more accurate validation of focus behavior.
  - Adjusted test assertions for block selection to be less strict and removed redundant blur operations for smoother test execution.
  - Updated toolbar dropdown closing method to use keyboard interaction for better reliability.
- **New Features**
  - Added a recoverable property to selection types, improving selection state management and recovery.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 13:38:02 +00:00
forehalo
dc7cd0487b refactor(server): decrypt license with provided aes key (#12570)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for a new AES key for license management, improving license encryption and decryption processes.

- **Bug Fixes**
  - Improved error messages and handling when activating expired or invalid licenses.

- **Refactor**
  - Updated license decryption logic to use a fixed AES key instead of deriving one from the workspace ID.
  - Added validation for environment variable values to prevent invalid configurations.

- **Tests**
  - Enhanced license-related tests to cover new key usage and updated error messages.
  - Updated environment variable validation tests with clearer error messages.

- **Chores**
  - Updated environment variable handling for improved consistency.
  - Set production environment variable explicitly in build configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 11:54:28 +00:00
darkskygit
7175019a0a feat(server): improve pdf parsing (#12356) 2025-05-27 11:36:48 +00:00
darkskygit
3c0fa429c5 feat(server): switch i2i to gpt (#12238)
fix AI-14
fix AI-17
fix AI-39
fix AI-112

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

- **New Features**
  - Expanded and reorganized prompt options for text and image actions, adding new prompts for image generation, style conversions, upscaling, background removal, and sticker creation.
  - Enhanced image editing capabilities with direct support for image attachments in prompts.

- **Improvements**
  - Updated prompt names and descriptions to be more user-friendly and descriptive.
  - Simplified and clarified prompt selection and image processing workflows with improved default behaviors.
  - Better organization of prompts through clear grouping and categorization.

- **Bug Fixes**
  - Improved validation and handling of image attachments during editing requests.

- **Refactor**
  - Internal code restructuring of prompts and provider logic for clarity and maintainability without affecting user workflows.
  - Refined message handling and content merging logic to ensure consistent prompt processing.
  - Adjusted image attachment rendering logic for improved display consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 11:36:47 +00:00
darkskygit
1e9cbdb65d feat(server): use generative ai api for transcript (#12569)
fix AI-151
2025-05-27 11:36:47 +00:00
CatsJuice
192266c0fd feat(core): move sign in button to workspace list (#12566)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved the appearance and layout of the "Sign in" menu item with updated styling and icon.
  - The "Sign in" option now appears as a standalone menu item in the workspace list when the user is not authenticated.

- **Style**
  - Enhanced visual consistency for the "Sign in" menu item to better match the overall theme.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 11:22:17 +00:00
pengx17
4ad008f712 fix(electron): optimize meeting privacy settings (#12530)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for requesting screen recording permission on macOS in addition to microphone permission.
  - Introduced a new "Permission issues" section in meeting privacy settings, including a button to restart the app if permission status is not updated.
- **Improvements**
  - Unified permission handling for screen and microphone settings, simplifying the user experience.
  - Added new localized strings for enhanced clarity regarding permission issues and app restart instructions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 11:08:06 +00:00
forehalo
d6476db64d chore: use PodMonitoring in charts instead (#12571)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Updated monitoring configuration to use a different resource type with simplified naming and label selectors for Kubernetes manifests.
- **Chores**
  - Removed Google Cloud Platform–specific monitoring configuration files from multiple components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 10:53:38 +00:00
donteatfriedrice
af3c002022 chore: remove link preview cache feature flag (#12568) 2025-05-27 10:07:33 +00:00
donteatfriedrice
69c7767003 chore: remove citation feature flag (#12567)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Footnote definitions and "Sources" headings are now always included in notes, without requiring a feature flag.
  - Enhanced footnote-related content with additional citation-style blocks such as bookmarks, embedded documents, and attachments.

- **Chores**
  - Removed the citation feature flag and its related configuration, logic, and translations from the application.

- **Documentation**
  - Updated localization files to remove entries related to the citation experimental feature.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 10:07:32 +00:00
renovate
28d8b35600 chore: bump up nestjs to v11.1.2 (#12524)
This PR contains the following updates:

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

---

### Release Notes

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

### [`v11.1.2`](https://redirect.github.com/nestjs/nest/compare/v11.1.1...32b5febcfaf4c8e01bc0d664d875d186a4f76cee)

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

</details>

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

### [`v11.1.2`](https://redirect.github.com/nestjs/nest/compare/v11.1.1...32b5febcfaf4c8e01bc0d664d875d186a4f76cee)

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

</details>

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

### [`v11.1.2`](https://redirect.github.com/nestjs/nest/compare/v11.1.1...32b5febcfaf4c8e01bc0d664d875d186a4f76cee)

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

</details>

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

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

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

#### v11.1.2 (2025-05-26)

##### Bug fixes

-   `microservices`
    -   [#&#8203;15172](https://redirect.github.com/nestjs/nest/pull/15172) fix(microservices): support custom strategy in async usefactory config ([@&#8203;mag123c](https://redirect.github.com/mag123c))
    -   [#&#8203;15166](https://redirect.github.com/nestjs/nest/pull/15166) fix(microservice): prevent error logs during redis client shutdown ([@&#8203;janroker](https://redirect.github.com/janroker))

##### Dependencies

-   `common`
    -   [#&#8203;15185](https://redirect.github.com/nestjs/nest/pull/15185) chore(deps): bump file-type from 20.5.0 to 21.0.0 ([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))
-   `platform-express`
    -   [#&#8203;15159](https://redirect.github.com/nestjs/nest/pull/15159) chore(deps): bump multer from 1.4.5-lts.2 to 2.0.0 ([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

##### Committers: 2

-   JaeHo Jang ([@&#8203;mag123c](https://redirect.github.com/mag123c))
-   Jan Roček ([@&#8203;janroker](https://redirect.github.com/janroker))

</details>

---

### Configuration

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

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

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

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

---

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

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC4xNi4wIiwidXBkYXRlZEluVmVyIjoiNDAuMTYuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2025-05-27 09:53:13 +00:00
zzj3720
0f1a3c212d refactor(editor): add a layer of ui-logic to enhance type safety (#12511)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced modular UI logic layers for Kanban and Table views, enhancing maintainability and scalability.
  - Added new CSS-in-JS style modules for database blocks and table views, improving visual consistency.
  - Expanded telemetry event tracking for database views, properties, filters, and groups.
  - Added utility functions for lazy initialization and cached computed values.

- **Refactor**
  - Unified logic and state management across Kanban and Table views by replacing direct component dependencies with logic-centric architecture.
  - Updated components and widgets to use the new logic-based approach for state, selection, and event handling.
  - Replaced inline styles with CSS classes; updated class names to align with new component structure.
  - Centralized state access through UI logic instances, eliminating direct DOM queries and simplifying dependencies.
  - Consolidated Kanban and Table view presets effects for streamlined initialization.
  - Replaced Lit reactive state with Preact signals in multiple components for improved reactivity.
  - Split monolithic components into separate logic and UI classes for clearer separation of concerns.
  - Removed obsolete components and consolidated exports for cleaner API surface.

- **Bug Fixes**
  - Enhanced selection and interaction reliability in database cells and views.
  - Fixed scrolling issues on mobile table views for improved compatibility.

- **Chores**
  - Updated end-to-end test selectors to reflect new component names and structure.
  - Removed deprecated utilities and cleaned up unused imports.

- **Documentation**
  - Improved type definitions and public API exports for better developer experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 09:36:44 +00:00
pengx17
9bf86e3f61 fix(core): add invite members button to sidebar (#12491)
fix AF-2661

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

- **New Features**
  - Added an "Invite Members" button to the sidebar, allowing users to quickly access workspace member settings (visible only for non-local workspaces).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 09:20:18 +00:00
yoyoyohamapi
c649ae5628 fix(core): ai chat button align (#12555)
> CLOSE AI-134

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

## Summary by CodeRabbit

- **Style**
  - Improved alignment and layout of the chat panel send button for a more visually balanced appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 09:04:33 +00:00
EYHN
dd1cc28194 fix(core): fix relative date filter (#12561)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Corrected date filtering to ensure months are consistently interpreted, improving accuracy when comparing dates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 08:49:43 +00:00
EYHN
ace5531b1f feat(core): remove old all docs code (#12558)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Removed Features**
  - The "All Pages (Old)" workspace view and its associated header have been removed.
  - The previous page list UI, including virtualized lists, group headers, and multi-selection, is no longer available.
  - Search and tag aggregation features within the old page list have been removed.

- **Style**
  - Styles related to the old page list and its components have been deleted.

- **Navigation**
  - The "All Pages (Old)" route has been removed from workspace navigation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 08:33:43 +00:00
EYHN
5033142a77 feat(core): all docs tracks (#12556)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added enhanced tracking for user interactions across document lists, navigation, display menus, quick actions, and collection operations.
  - User actions such as opening documents, editing collections, toggling favorites, changing view modes, and navigating collections are now logged for analytics.

- **Chores**
  - Expanded internal event tracking capabilities to support more detailed analytics on user interactions throughout the interface.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 08:17:14 +00:00
JimmFly
8d3b20ecc7 feat(core): add account deletion entry to account settings (#12385)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Implemented account deletion functionality with confirmation dialogs and success notifications.
  - Added a warning modal for team workspace owners before account deletion.
  - Introduced a new, richly formatted internationalized message for account deletion confirmation.
  - Added a new dialog component to inform users of successful account deletion.
- **Improvements**
  - Updated localization strings to provide detailed guidance and warnings for account deletion.
  - Enhanced error handling by converting errors into user-friendly notifications.
  - Simplified and improved the sign-out process with better error handling and streamlined navigation.
- **Style**
  - Added new style constants for success and warning modals related to account deletion.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 08:00:44 +00:00
EYHN
18da2fe4e6 feat(core): adjust filter area style (#12534)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for displaying the "Add Filter" action as either an icon button or a labeled button, depending on the filter state.
  - Introduced a localized label for the "Add Filter" button.

- **Style**
  - Improved filter area layout and styling for better visual consistency.
  - Adjusted padding and added styles to hide empty filter values.

- **Bug Fixes**
  - Updated test identifiers for filter value elements to improve test reliability.

- **Documentation**
  - Added a new English localization string for the "Add Filter" button.

- **Chores**
  - Updated translation completeness percentages for various locales.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 07:46:28 +00:00
yoyoyohamapi
1837c1fe84 feat(core): ai input scrolling carousel tips (#12540)
### TL;DR

feat: scrolling carousel for ai input tips

> CLOSE BS-3537
2025-05-27 07:29:15 +00:00
akumatus
f4cba7d6ee refactor(core): add text stream parser (#12459)
Support [AI-82](https://linear.app/affine-design/issue/AI-82).

Added a `TextStreamParser` class to standardize formatting of different types of AI stream chunks across providers.

### What changed?

- Created a new `TextStreamParser` class in `utils.ts` that handles formatting of various chunk types (text-delta, reasoning, tool-call, tool-result, error)
- Refactored the Anthropic, Gemini, and OpenAI providers to use this shared parser instead of duplicating formatting logic
- Added comprehensive tests for the new `TextStreamParser` class, including tests for individual chunk types and sequences of chunks
- Defined a common `AITools` type to standardize tool interfaces across providers

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

- **New Features**
	- Enhanced formatting and structure for streamed AI responses, including improved handling of callouts, web search, and web crawl results.
- **Refactor**
	- Streamlined and unified the processing of streamed AI response chunks across providers for more consistent output.
- **Bug Fixes**
	- Improved error handling and display for streamed responses.
- **Tests**
	- Added comprehensive tests to ensure correct formatting and handling of various streamed message types.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 07:14:17 +00:00
yoyoyohamapi
83caf98618 fix(core): space inside menu input triggers ai menu (#12552)
> CLOSE AI-137

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

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved input field behavior in context menus by preventing unintended actions caused by keypress events.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 06:58:27 +00:00
fengmk2
409e71ff8b fix(server): use /_bulk endpoint instead (#12542)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved reliability of batch write operations to search providers, ensuring documents are correctly indexed and retrievable.
- **Tests**
	- Added new test cases and snapshots to verify batch write functionality and confirm resolution of prior batch processing issues.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 06:43:26 +00:00
CatsJuice
b5b911b5d2 feat(core): doc explorer list item drag preview (#12553)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Improved the appearance of drag preview elements in the document list, including updated layout, spacing, background, and icon size for a more polished visual experience when dragging items.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 06:29:13 +00:00
forehalo
2f139bd02c chore(admin): remove useless config diff (#12545)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a GraphQL mutation to validate multiple app configuration updates, returning detailed validation results for each item.
  - Extended the API schema to support validation feedback, enabling client-side checks before applying changes.
  - Introduced a detailed, parameterized error message system for configuration validation errors.
  - Enabled validation of configuration inputs via the admin UI with clear, descriptive error messages.

- **Improvements**
  - Enhanced error reporting with specific, context-rich messages for invalid app configurations.
  - Simplified admin settings UI by removing the confirmation dialog and streamlining save actions.
  - Improved clarity and maintainability of validation logic and error handling components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 06:07:26 +00:00
EYHN
eed95366c9 fix(core): fix all docs permissions check (#12538) 2025-05-27 14:06:55 +08:00
EYHN
32c7a135f4 feat(core): adjust pinned collections edit button (#12533)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Added a centralized "Edit collection rules" button in the pinned collections footer, enabling users to edit collection rules via a dialog.
- **Style**
	- Removed hover effects and visual styling from the edit icon button in pinned collections.
- **Bug Fixes**
	- Removed per-item edit buttons to streamline the editing process.
- **Documentation**
	- Added a new localized label for the edit collection rules action.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 05:41:12 +00:00
fengmk2
3e6384604c chore(server): remove request success log (#12550)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
  - Removed detailed verbose logging for Elasticsearch requests to reduce log noise.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 05:26:04 +00:00
fundon
9465d0dc73 fix(editor): loading style (#12537)
Closes: [BS-3555](https://linear.app/affine-design/issue/BS-3555/ui-attachment-loading-变量更新)
Closes: [BS-3559](https://linear.app/affine-design/issue/BS-3559/ui-图片-loading-变量更新)

### Dark
<img width="625" alt="Screenshot 2025-05-26 at 20 32 36" src="https://github.com/user-attachments/assets/93501e3d-8fc6-45f9-84a0-ac147e5c5f9f" />

### Light
<img width="623" alt="Screenshot 2025-05-26 at 20 32 25" src="https://github.com/user-attachments/assets/7d5bc128-6667-45b5-982d-dab3a22706a7" />

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

- **New Features**
  - Loading icons are now invoked as functions, allowing for more flexible and customizable rendering with parameters like size and progress.

- **Refactor**
  - Replaced theme-dependent and static loading icon references with a unified `LoadingIcon()` component across multiple components and blocks.
  - Removed legacy icon variants and simplified icon import statements, centralizing icon rendering logic.

- **Style**
  - Updated styles for loading and reload buttons to use theme-aware CSS variables.
  - Enlarged and repositioned loading indicators in image blocks for better visibility.

- **Bug Fixes**
  - Achieved consistent loading icon rendering across various blocks and components by standardizing icon invocation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 05:10:27 +00:00
JimmFly
1b715e588c feat(core): support install license for self hosted client (#12287)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to upload and replace license files for self-hosted team workspaces via a new modal dialog.
  - Introduced clearer UI flows for activating, deactivating, and managing team licenses, including one-time purchase licenses.
  - Provided direct links and guidance for requesting licenses and managing payments.

- **Enhancements**
  - Improved license management interface with updated button labels and descriptions.
  - Added new styles for better layout and clarity in license dialogs.
  - Updated internationalization with new and revised texts for license operations.

- **Bug Fixes**
  - Prevented duplicate opening of license or plan dialogs when already open.

- **Chores**
  - Updated support and pricing links for clarity and accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 04:56:14 +00:00
fengmk2
382c237dac fix(server): return empty summary field value (#12517)
close AF-2658

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

- **Tests**
  - Added new test cases and snapshots to enhance coverage for search results involving empty or missing fields like summary, title, and ref_doc_id.
  - Verified consistent handling of empty string values and absence of fields across different search providers.

- **Bug Fixes**
  - Improved handling of empty string values for specific fields by converting them to null to ensure consistent search result formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 04:42:54 +00:00
EYHN
3676f3b769 feat(core): add default group and order (#12526)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added default sorting and grouping by "Last Updated" for document views.
  - Introduced clearer group headers for documents grouped by creation or update date, displaying relative dates or appropriate fallback text.

- **Improvements**
  - Enhanced date grouping headers with capitalized, user-friendly text and improved handling of missing dates.
  - Added a new localization for "Never updated" to improve clarity in document groupings.
  - Initialized document update timestamps at creation to improve date accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 04:08:57 +00:00
pengx17
ed8e50bca6 fix(electron): potential app crash on quit (#12480)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved stability during shutdown by preventing potential crashes when removing audio property listeners on macOS.
	- Suppressed unnecessary error logs related to device listener removal during system shutdown.
	- Enhanced handling of internal subscriptions to avoid redundant operations and improve reliability when loading or destroying views.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 03:43:27 +00:00
pengx17
bfe743b68b fix(core): audio block actions not showing (#12527)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated audio block containers to remove borders and allow visible overflow, improving the appearance of audio attachments.

- **Bug Fixes**
  - Ensured that the actions field is always present in audio transcription job results, defaulting to an empty string when not specified.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 03:29:10 +00:00
pengx17
83a483a06d fix(electron): optimize tab switching (#12518)
fix AF-2670

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

- **Style**
  - Updated styling so that opacity and transition effects apply only when a translucent background is present, enhancing visual precision.
- **Chores**
  - Renamed a data attribute in the app container for improved consistency.
  - Disabled background throttling for specific views to maintain performance when running in the background.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 03:14:12 +00:00
pengx17
502fb96f55 fix(electron): disable translucent sidebar by default (#12477)
fix AF-2662

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

## Summary by CodeRabbit

- **Chores**
  - Changed the default setting for blur background effect to be disabled for new users. Existing users' preferences remain unaffected.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 02:58:27 +00:00
L-Sun
1229ee134b fix(editor): drag handle disappeard when hover on the extra area between note and its background (#12536)
Close [BS-3391](https://linear.app/affine-design/issue/BS-3391/无法从note中拖出embed-synced-doc到白板)

### Before
can not hover on drag handle

https://github.com/user-attachments/assets/5596538e-e922-4d7f-8188-b719b234f3ee

### After
can hover on drag handle

https://github.com/user-attachments/assets/855743ec-7601-48a8-8453-cd5aa395bd06

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

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved detection of hovering over notes in edgeless mode, ensuring the drag handle appears correctly when hovering on the background of a selected note.
	- Enhanced background style updates for edgeless notes, providing more accurate visual feedback.

- **Tests**
	- Added a test to verify that the drag handle is visible when hovering over the background of a selected edgeless note.
	- Updated undo/redo tests to improve accuracy of background color evaluation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 02:43:27 +00:00
EYHN
9c5af576ee feat(nbstore): add more blob sync state (#12516) 2025-05-27 02:20:49 +00:00
EYHN
4aa9ae5e68 fix(nbstore): fix http request timeout handling (#12515)
`AbortSignal.timeout` will cause a timeout when transferring large blobs.  by using `setTimeout` currently, the timeout only covers the phase of establishing a connection.

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

- **Bug Fixes**
  - Improved reliability of request cancellation and timeout handling for HTTP requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 02:20:49 +00:00
yoyoyohamapi
6a912d1031 fix(core): workspace embedding ui opt (#12532)
### TL;DR

fix: workspace embedding ui optimization

> CLOSE BS-3531
> CLOSE BS-3532
> CLOSE BS-3533
> CLOSE BS-3534
> CLOSE BS-3535
> CLOSE BS-3536
> CLOSE BS-3553

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

## Summary by CodeRabbit

- **New Features**
  - Added a localized "Upload file" label for improved internationalization in embedding settings.

- **Style**
  - Improved layout and text overflow handling for attachments and ignored documents.
  - Enhanced visual alignment and consistency across embedding-related components.
  - Updated progress indicator color for better theme integration.

- **Bug Fixes**
  - Adjusted text truncation and spacing to prevent layout issues with long filenames and document titles.

- **Chores**
  - Updated translation completeness percentage for the "es-CL" locale.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 02:00:42 +00:00
darkskygit
8952ce4fb3 feat(server): update prompts (#12539)
fix AI-63
fix PD-2567
fix AI-150
fix AI-149
fix AI-148
fix AI-147
fix AI-146
fix AI-145
fix AI-144
fix AI-143

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

- **New Features**
  - Enhanced AI-generated responses for writing and code-related prompts with detailed, structured instructions for improved clarity and consistency.
  - Updated AI model for multiple prompts to deliver faster and more accurate results.

- **Bug Fixes**
  - Improved test validation for code explanation and error-checking prompts to better recognize correct outputs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 19:35:56 +00:00
akumatus
8b76644fc1 feat(core): use the same prompt for Search With AFFiNE AI (#12496)
Support [AI-59](https://linear.app/affine-design/issue/AI-59)

Deprecated use of the perplexity model. Makes chat and search use the same prompt content.

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

- **New Features**
  - Added support for two new AI models: Claude Opus 4 and Claude Sonnet 4, enabling enhanced text and image input capabilities.
  - Introduced a new chat prompt with detailed instructions for specialized AI interactions within AFFiNE.

- **Refactor**
  - Improved prompt management for AI chat and search features by centralizing shared settings for better consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 22:42:49 +08:00
akumatus
5fcdad46eb feat(core): add google vertex ai (#12423)
Close [AI-125](https://linear.app/affine-design/issue/AI-125)

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

- **New Features**
  - Added new provider configurations `geminiVertex` and `anthropicVertex` for Google Vertex AI in backend schema, provider classes, and admin config.
  - Introduced `GeminiVertexProvider` and `AnthropicVertexProvider` classes supporting Vertex AI models with specific capabilities.
  - Expanded model options for transcription prompts with newer Gemini models.
  - Re-exported provider modules to include Vertex AI variants.

- **Improvements**
  - Extended provider architecture to support separate Vertex AI configurations and models.
  - Updated test setup to replace deprecated provider references with new Vertex variants.
  - Consolidated environment variables for server testing with a single `SERVER_CONFIG`.

- **Bug Fixes**
  - Updated mock models and import references in tests to align with new provider classes.

- **Chores**
  - Added `@ai-sdk/google-vertex` dependency for Vertex AI support.
  - Updated dependency list to include `@ai-sdk/google-vertex`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 13:09:29 +00:00
darkskygit
eb26e99ecd fix(server): skip embedding when not configured (#12544)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved reliability by ensuring certain features are only enabled when required support and configuration are present, reducing the risk of runtime errors.
  - Enhanced platform detection logic for better accuracy across different environments, including macOS and Windows systems.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 12:52:15 +00:00
forehalo
c2ffcb2c2c chore: remove multiple cloud server flag (#12531)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - The "Add Selfhosted" and "Add Server" options are now displayed whenever the build configuration is native, without relying on feature flags.

- **Refactor**
  - Simplified conditional rendering for server addition buttons by replacing feature flag checks with build configuration checks.

- **Chores**
  - Removed the "Multiple Cloud Servers" feature flag and its related localization strings, streamlining feature management and UI labels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 12:37:15 +00:00
forehalo
7f2b094eb5 chore: get dev server url from browser url (#12525)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved development server with automatic handling of exit signals.
  - Configured default WebSocket URL for enhanced client-server communication during development.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 12:22:20 +00:00
Saul-Mirone
41f0a2d01a feat(editor): add at member highlight (#12535)
Closes: BS-2896

<img width="468" alt="image" src="https://github.com/user-attachments/assets/2b84c484-29b8-4650-b74c-da7afd3a1e41" />

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

## Summary by CodeRabbit

- **New Features**
  - Enhanced member search results with highlighted text, making it easier to visually identify matched parts of member names during searches.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 12:08:03 +00:00
Flrande
53a23dd4bf fix(editor): do not display emoji container when it is empty string (#12543)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - The emoji container in callout blocks now automatically hides when no emoji is present, providing a cleaner appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 11:40:17 +00:00
doouding
0be30f15ea fix: dnd not working in initial doc (#12519)
Fixes [BS-3152](https://linear.app/affine-design/issue/BS-3152/)

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

- **Bug Fixes**
  - Improved widget stability by removing the event listener that caused the widget to hide on block updates, leaving only viewport changes to trigger hiding.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 11:25:15 +00:00
Flrande
5d28657d76 chore(editor): add track event for latex (#12541)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Enhanced telemetry tracking for LaTeX and equation creation actions, capturing detailed context such as editor mode and location within the app.
- **Chores**
  - Expanded telemetry event types to include LaTeX-specific actions for improved analytics and observability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 11:10:32 +00:00
Saul-Mirone
9343e29fea fix: linked doc popover selector error (#12528)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved scrolling behavior to the focused item in the linked document popover, ensuring reliable navigation regardless of special characters in item identifiers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 10:06:04 +00:00
EYHN
01369954d6 feat(core): save last opened workspace id when import clipper (#12487)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - The app now remembers the last selected workspace when importing, improving continuity for future imports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 09:51:09 +00:00
Yifeng Wang
9d2330fc2b fix(editor): possible race condition in viewport clipping (#12503) 2025-05-26 17:08:31 +08:00
Saul-Mirone
051dc4296d fix(editor): limit at members list length (#12529)
Closes: BS-3009

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

- **New Features**
  - The mention members menu now displays up to three members, with a localized hint indicating the number of additional hidden members.
- **Localization**
  - Added a new translation key for the overflow members hint in the English language pack.
- **Chores**
  - Updated translation completeness statistics for the "es-CL" locale.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 08:37:33 +00:00
yoyoyohamapi
0e8f19b92c refactor(core): workspace embedding entities (#12490)
### TL;DR

refactor: split workspace embedding module entities into:

* additional-attachments
* ignored-docs
* embedding-enabled
* embedding-progress
2025-05-26 07:17:38 +00:00
yoyoyohamapi
c06c72e108 refactor(core): workspace mutation effect (#12488)
### TL;DR

* refactor: workspace embedding mutation effect
* tests: error display for workspace embedding
2025-05-26 07:17:37 +00:00
yoyoyohamapi
da22391910 refactor(core): using computed data & optimizing data fetching timing & loading initial values (#12478)
## TL;DR

refactor workspace embedding:

* using computed data
* optimizing data fetching timing(constructor -> component mounted)
* set loading initial values to `true`

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

## Summary by CodeRabbit

- **New Features**
  - Improved loading indicators and state handling for embedding settings, including a more accurate loading state for the embedding toggle.
- **Bug Fixes**
  - The embedding toggle now safely handles unknown or loading states and is disabled while loading, preventing unintended interactions.
- **Refactor**
  - Simplified pagination logic and optimized initial data fetching for a smoother user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 07:17:37 +00:00
Saul-Mirone
d06bb0222f fix: codebar language search hotkey conflict (#12522)
Closes: BS-3395

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

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved keyboard input handling in filterable lists to prevent unintended interactions when using arrow keys, Enter, or Escape.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 06:42:02 +00:00
forehalo
25aa5701bd chore(core): fix mixpanel init (#12513)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Adjusted telemetry settings to ensure Sentry is disabled when telemetry is enabled and clarified Mixpanel’s automatic opt-out behavior.

- **Documentation**
  - Added a comment explaining Mixpanel’s handling of telemetry preferences for improved transparency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 06:28:32 +00:00
Saul-Mirone
8ba4584b88 fix: latex editor max-height (#12520)
Closes: BS-3538

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

## Summary by CodeRabbit

- **Refactor**
  - Improved click handling for LaTeX blocks, making interactions more consistent and maintainable.
  - Updated LaTeX editor menu layout to enhance vertical scrolling, ensuring better usability with large content.

- **Style**
  - Added a maximum height and vertical scrolling to the LaTeX editor for improved user experience with lengthy formulas.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 06:14:17 +00:00
L-Sun
7aacfee789 feat(editor): bring back line width panel of brush in edgelss toolbar (#12514)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to select line width for pen tools, allowing users to customize brush and highlighter thickness in the toolbar menu.

- **Bug Fixes**
  - Restored and verified the functionality for adding brush elements with different sizes, ensuring accurate rendering of brush strokes based on selected size.

- **Improvements**
  - Enhanced slider component interaction by refining pointer event handling and updated slider styles for better touch interaction support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 05:49:31 +00:00
doouding
81be5818cc fix: edgeless note mask does not restore after dnd dropping (#12495) 2025-05-26 05:03:10 +00:00
doouding
6518c5904e fix: linked-doc and figma scale issue (#12493)
Fixes [BS-2993](https://linear.app/affine-design/issue/BS-2993/)
2025-05-26 05:03:10 +00:00
doouding
3d0dc64516 fix: bookmark link can be click only when selected (#12450)
Fixes [BS-3390](https://linear.app/affine-design/issue/BS-3390/)
2025-05-26 05:03:09 +00:00
doouding
5de63c29f5 fix: rewrite selection logic and frame selection handling logic (#12421)
Fixes [BS-3528](https://linear.app/affine-design/issue/BS-3528)
Fixes [BS-3331](https://linear.app/affine-design/issue/BS-3331/frame-移动逻辑很奇怪)

### Changed
- Remove `onSelected` method from gfx view, use `handleSelection` provided by `GfxViewInteraction` instead.
- Add `selectable` to allow model to filter out itself from selection.
- Frame can be selected by body only if it's locked or its background is not transparent.

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

- **New Features**
  - Enhanced selection behavior for frames, edgeless text, notes, and mind map elements with refined control based on lock state and background transparency.
  - Introduced group-aware selection logic promoting selection of appropriate group ancestors.
  - Added support for element selection events in interactivity extensions.

- **Bug Fixes**
  - Resolved frame selection issues by enabling selection via title clicks and restricting body selection to locked frames or those with non-transparent backgrounds.

- **Documentation**
  - Added clarifying comments for group retrieval methods.

- **Tests**
  - Updated and added end-to-end tests for frame and lock selection reflecting new selection conditions.

- **Refactor**
  - Unified and simplified selection handling by moving logic from component methods to interaction handlers and removing deprecated selection methods.
  - Streamlined selection candidate processing with extension-driven target suggestion.
  - Removed legacy group element retrieval and selection helper methods to simplify interaction logic.

- **Style**
  - Renamed types and improved type signatures for selection context and interaction configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 05:03:09 +00:00
JimmFly
14a89c1e8a feat(core): highlight the share button (#12470)
close AF-2659
![CleanShot 2025-05-23 at 11 07 24@2x](https://github.com/user-attachments/assets/9d2047a8-4f84-40dd-8f0d-9d8a6abac616)

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

## Summary by CodeRabbit

- **Style**
  - Updated the share button to use the primary styling for improved visual emphasis.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 04:01:34 +00:00
pengx17
f619762b0c fix(core): setting modal max-width (#12494)
fix AF-2670

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

## Summary by CodeRabbit

- **Style**
  - Updated the maximum width of the settings modal for improved adaptability across different screen sizes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 03:47:17 +00:00
fengmk2
d6000ce70b chore(tools): add @affine/admin to available packages (#12507)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/hTwOityLamd4hitrae7M/52e2017b-cd59-4549-8c24-be84b7000d56.png)

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

## Summary by CodeRabbit

- **New Features**
  - Added '@affine/admin' to the list of selectable packages for development commands.
- **Enhancements**
  - Improved package selection prompt by displaying up to 10 choices at a time for easier navigation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 03:31:32 +00:00
CatsJuice
20af4c35ee feat(core): card view drag handle for doc explorer (#12431)
close AF-2624, AF-2628, AF-2581

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

- **New Features**
  - Introduced a draggable handle to document cards in the explorer, visible on hover in card view.
  - Added an option to remove grouping in the display menu.
  - Added contextual tooltips for user avatars indicating creation or last update.
  - Enabled optional tooltips on public user labels.
  - Extended dropdown buttons to accept custom styling classes.
  - Added a new masonry story showcasing item heights determined by ratios.

- **Style**
  - Enhanced drag handle appearance and visibility for card view items.
  - Replaced static shadows with theme-aware, smoothly transitioning shadows on card items.
  - Adjusted spacing between items in the document explorer for improved layout, with increased horizontal and (in card view) vertical gaps.
  - Reduced top padding in workspace page styles.
  - Added new button background style for secondary buttons.

- **Bug Fixes**
  - Removed duplicate internal property declarations to eliminate redundancy.

- **Refactor**
  - Simplified layout props by removing fixed height parameters in multiple components.
  - Updated masonry layout logic to support ratio-based item sizing alongside fixed heights.
  - Removed randomized skeleton loading placeholders, replacing them with fixed or no placeholders.
  - Refined masonry component typings and scrollbar placement for improved styling and layout.
  - Improved selection logic to activate selection mode when selecting all documents.

- **Localization**
  - Added new translation keys for grouping removal and user attribution tooltips.
  - Updated English locale with new strings for "Remove group" and user-created/updated tooltips.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 03:17:19 +00:00
EYHN
7d3b7a8555 feat(core): add migration background cover (#12485)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added a background image to migration notifications that adapts to light or dark theme settings.

- **Style**
  - Improved padding for migration notification containers for a more balanced appearance.
  - Introduced new styling for the migration background image.

- **Chores**
  - Updated translation completeness percentage for Argentinian Spanish.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 02:49:30 +00:00
fengmk2
e3d63896bf chore(server): add job backoff strategies (#12499)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Implemented an exponential backoff retry strategy for job queues, resulting in progressively longer wait times between retry attempts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 02:33:58 +00:00
CatsJuice
adbdf32d8b fix(core): doc explorer navigation padding (#12430)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated navigation header to include additional left padding for improved spacing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 02:06:03 +00:00
doodlewind
2192f28500 fix(editor): allow space-drag in presentation mode (#12501)
### TL;DR

Fix presentation mode space-drag interaction by disabling black background during panning and properly restoring presentation state.

### What changed?

- Modified how the presentation tool handles state restoration after panning
- Disabled black background during space-drag and middle-mouse panning in presentation mode
- Fixed tool state management to properly restore presentation mode after space panning
- Added direct modification of the current tool's activated options instead of triggering a full tool change

### How to test?

1. Create a frame in edgeless mode
2. Enter presentation mode
3. Press space and drag to pan around
4. Verify the black background disappears during panning
5. Verify presentation mode is properly restored after releasing space
6. Try the same with middle-mouse button dragging

### Why make this change?

The black background in presentation mode was causing visibility issues during panning operations. Additionally, the presentation state wasn't being properly restored after space-drag panning. These changes improve the user experience by making content visible during navigation while maintaining the presentation mode state.

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

- **New Features**
  - Presentation mode now automatically hides the black background overlay when dragging with the space key or middle mouse button, improving visibility during navigation.

- **Bug Fixes**
  - Improved handling of tool switching after panning in presentation mode, ensuring smoother transitions and state restoration.

- **Tests**
  - Added an end-to-end test to verify that the black background is hidden during space-drag actions in presentation mode.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 01:16:32 +00:00
CatsJuice
9599494e87 feat(core): new doc list for trash page (#12429)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added "Delete permanently" and "Restore" quick actions for documents in the Trash, enabling users to restore or permanently delete trashed documents directly from the UI.
  - Introduced multi-restore support, allowing batch restoration of selected trashed documents.
- **Improvements**
  - Quick action tooltips are now localized for better international user experience.
  - Trash page now uses an updated explorer interface for a more consistent and reactive document management experience.
  - Enhanced quick actions with optional click handlers for better extensibility.
- **Documentation**
  - Added new translation keys for "Delete permanently" and "Restore" actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-24 12:45:41 +00:00
fengmk2
dfa62f7683 chore(server): support dynamic disable indexer (#12498)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved system stability by ensuring that indexing jobs do not run when the indexer feature is disabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-24 12:27:53 +00:00
440 changed files with 10152 additions and 7582 deletions

View File

@@ -31,9 +31,13 @@
"properties": {
"queue": {
"type": "object",
"description": "The config for job queues\n@default {\"attempts\":5,\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
"description": "The config for job queues\n@default {\"attempts\":5,\"backoff\":{\"type\":\"exponential\",\"delay\":1000},\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
"default": {
"attempts": 5,
"backoff": {
"type": "exponential",
"delay": 1000
},
"removeOnComplete": true,
"removeOnFail": {
"age": 86400,
@@ -48,14 +52,14 @@
},
"queues.copilot": {
"type": "object",
"description": "The config for copilot job queue\n@default {\"concurrency\":5}",
"description": "The config for copilot job queue\n@default {\"concurrency\":10}",
"properties": {
"concurrency": {
"type": "number"
}
},
"default": {
"concurrency": 5
"concurrency": 10
}
},
"queues.doc": {
@@ -639,6 +643,41 @@
"apiKey": ""
}
},
"providers.geminiVertex": {
"type": "object",
"description": "The config for the google vertex provider.\n@default {}",
"properties": {
"location": {
"type": "string",
"description": "The location of the google vertex provider."
},
"project": {
"type": "string",
"description": "The project name of the google vertex provider."
},
"googleAuthOptions": {
"type": "object",
"description": "The google auth options for the google vertex provider.",
"properties": {
"credentials": {
"type": "object",
"description": "The credentials for the google vertex provider.",
"properties": {
"client_email": {
"type": "string",
"description": "The client email for the google vertex provider."
},
"private_key": {
"type": "string",
"description": "The private key for the google vertex provider."
}
}
}
}
}
},
"default": {}
},
"providers.perplexity": {
"type": "object",
"description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}",
@@ -653,6 +692,41 @@
"apiKey": ""
}
},
"providers.anthropicVertex": {
"type": "object",
"description": "The config for the google vertex provider.\n@default {}",
"properties": {
"location": {
"type": "string",
"description": "The location of the google vertex provider."
},
"project": {
"type": "string",
"description": "The project name of the google vertex provider."
},
"googleAuthOptions": {
"type": "object",
"description": "The google auth options for the google vertex provider.",
"properties": {
"credentials": {
"type": "object",
"description": "The credentials for the google vertex provider.",
"properties": {
"client_email": {
"type": "string",
"description": "The client email for the google vertex provider."
},
"private_key": {
"type": "string",
"description": "The private key for the google vertex provider."
}
}
}
}
}
},
"default": {}
},
"unsplash": {
"type": "object",
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",

View File

@@ -29,11 +29,7 @@ runs:
- name: Import config
shell: bash
env:
DEFAULT_CONFIG: '{}'
run: |
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"},"providers.anthropic":{"apiKey":"%s"},"exa":{"key":"%s"}}}' \
"$COPILOT_FAL_API_KEY" \
"$COPILOT_GOOGLE_API_KEY" \
"$COPILOT_OPENAI_API_KEY" \
"$COPILOT_PERPLEXITY_API_KEY" \
"$COPILOT_ANTHROPIC_API_KEY" \
"$COPILOT_EXA_API_KEY" > ./packages/backend/server/config.json
printf '%s\n' "${SERVER_CONFIG:-$DEFAULT_CONFIG}" > ./packages/backend/server/config.json

View File

@@ -1,12 +0,0 @@
{{- if eq .Values.global.deployment.platform "gcp" -}}
apiVersion: monitoring.googleapis.com/v1
kind: ClusterPodMonitoring
metadata:
name: "{{ include "graphql.fullname" . }}"
spec:
selector:
{{- include "graphql.selectorLabels" . | nindent 4 }}
endpoints:
- port: 9464
interval: 30s
{{- end }}

View File

@@ -1,12 +0,0 @@
{{- if eq .Values.global.deployment.platform "gcp" -}}
apiVersion: monitoring.googleapis.com/v1
kind: ClusterPodMonitoring
metadata:
name: "{{ include "renderer.fullname" . }}"
spec:
selector:
{{- include "renderer.selectorLabels" . | nindent 4 }}
endpoints:
- port: 9464
interval: 30s
{{- end }}

View File

@@ -1,12 +0,0 @@
{{- if eq .Values.global.deployment.platform "gcp" -}}
apiVersion: monitoring.googleapis.com/v1
kind: ClusterPodMonitoring
metadata:
name: "{{ include "sync.fullname" . }}"
spec:
selector:
{{- include "sync.selectorLabels" . | nindent 4 }}
endpoints:
- port: 9464
interval: 30s
{{- end }}

View File

@@ -1,11 +1,12 @@
{{- if eq .Values.global.deployment.platform "gcp" -}}
apiVersion: monitoring.googleapis.com/v1
kind: ClusterPodMonitoring
kind: PodMonitoring
metadata:
name: "{{ include "doc.fullname" . }}"
name: "{{ .Release.Name }}-monitoring"
spec:
selector:
{{- include "doc.selectorLabels" . | nindent 4 }}
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
endpoints:
- port: 9464
interval: 30s

View File

@@ -138,6 +138,7 @@ jobs:
uses: ./.github/actions/build-rust
env:
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
AFFINE_PRO_LICENSE_AES_KEY: ${{ secrets.AFFINE_PRO_LICENSE_AES_KEY }}
with:
target: ${{ matrix.targets.name }}
package: '@affine/server-native'

View File

@@ -1001,12 +1001,7 @@ jobs:
- name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
env:
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
uses: ./.github/actions/server-test-env
- name: Run server tests
@@ -1105,12 +1100,7 @@ jobs:
- name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
env:
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

View File

@@ -81,12 +81,7 @@ jobs:
- name: Prepare Server Test Environment
env:
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
uses: ./.github/actions/server-test-env
- name: Run server tests
@@ -156,12 +151,7 @@ jobs:
- name: Prepare Server Test Environment
env:
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

57
Cargo.lock generated
View File

@@ -20,8 +20,7 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "adobe-cmap-parser"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
source = "git+https://github.com/darkskygit/adobe-cmap-parser#610513ae6035c63eab69f33299b86c43693cabb4"
dependencies = [
"pom",
]
@@ -2737,9 +2736,9 @@ dependencies = [
[[package]]
name = "path-ext"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de7a86239a8b87b5094977b64893fcf0ed768072744dd4ee0df237686b2d815"
checksum = "7603010004b5cdecf8006605bf7b6f07b0e59d3003010f52b767e91bf2582a45"
dependencies = [
"path-slash",
"walkdir",
@@ -2754,7 +2753,7 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "pdf-extract"
version = "0.8.2"
source = "git+https://github.com/toeverything/pdf-extract?branch=darksky%2Fimprove-font-decoding#e74beed894e1b8dc228c2bf078ed92814b27759f"
source = "git+https://github.com/toeverything/pdf-extract?branch=darksky%2Fimprove-font-decoding#040751a61aba51e7a28217b758c18db4415c3ee4"
dependencies = [
"adobe-cmap-parser",
"cff-parser",
@@ -2763,6 +2762,7 @@ dependencies = [
"log",
"lopdf",
"postscript",
"rust-embed",
"type1-encoding-parser",
"unicode-normalization",
]
@@ -2943,9 +2943,12 @@ checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6"
[[package]]
name = "postscript"
version = "0.14.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306"
checksum = "9a2238e788cf2c9b6edc23b83cf8ccdd4a6380cc9bf0598cc220fac42a55def6"
dependencies = [
"typeface",
]
[[package]]
name = "potential_utf"
@@ -3333,6 +3336,40 @@ dependencies = [
"realfft",
]
[[package]]
name = "rust-embed"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.101",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -4670,6 +4707,12 @@ dependencies = [
"pom",
]
[[package]]
name = "typeface"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f6b49e025f4dc953a29b83e4f5a905089117d09fa53491015d7678951b8be1"
[[package]]
name = "typenum"
version = "1.18.0"

View File

@@ -57,7 +57,7 @@ objc2-foundation = "0.3"
once_cell = "1"
ordered-float = "5"
parking_lot = "0.12"
path-ext = "0.1.1"
path-ext = "0.1.2"
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
phf = { version = "0.11", features = ["macros"] }
proptest = "1.3"

View File

@@ -4393,6 +4393,61 @@ hhh
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'h6',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Sources',
},
],
},
collapsed: true,
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:bookmark',
props: {
style: 'citation',
url,
title,
description,
icon: favicon,
footnoteIdentifier: '1',
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[4]',
flavour: 'affine:embed-linked-doc',
props: {
style: 'citation',
pageId: 'deadbeef',
footnoteIdentifier: '2',
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[5]',
flavour: 'affine:attachment',
props: {
name: 'test.txt',
sourceId: 'abcdefg',
footnoteIdentifier: '3',
style: 'citation',
},
children: [],
},
],
};
@@ -4469,6 +4524,38 @@ hhh
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'h6',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Sources',
},
],
},
collapsed: true,
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:bookmark',
props: {
style: 'citation',
url,
title,
description,
icon: favicon,
footnoteIdentifier: '1',
},
children: [],
},
],
};

View File

@@ -10,7 +10,6 @@ import {
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -36,15 +35,7 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
if (!isFootnoteDefinitionNode(o.node)) {
return;
}
@@ -73,6 +64,7 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
name: fileName,
sourceId: blobId,
footnoteIdentifier,
style: 'citation',
},
children: [],
},

View File

@@ -4,7 +4,7 @@ import {
} from '@blocksuite/affine-components/caption';
import {
getAttachmentFileIcon,
getLoadingIconWith,
LoadingIcon,
} from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import {
@@ -20,7 +20,6 @@ import {
DocModeProvider,
FileSizeLimitProvider,
TelemetryProvider,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils';
import {
@@ -304,15 +303,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}
protected resolvedState$ = computed<AttachmentResolvedStateInfo>(() => {
const theme = this.std.get(ThemeProvider).theme$.value;
const loadingIcon = getLoadingIconWith(theme);
const size = this.model.props.size;
const name = this.model.props.name$.value;
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
const resolvedState = this.resourceController.resolveStateWith({
loadingIcon,
loadingIcon: LoadingIcon(),
errorIcon: WarningIcon(),
icon: AttachmentIcon(),
title: name,

View File

@@ -47,11 +47,10 @@ export const styles = css`
.affine-attachment-content-title-icon {
display: flex;
width: 16px;
height: 16px;
align-items: center;
justify-content: center;
color: var(--affine-text-primary-color);
font-size: 16px;
}
.affine-attachment-content-title-text {
@@ -107,7 +106,7 @@ export const styles = css`
.affine-attachment-card.loading {
.affine-attachment-content-title-text {
color: var(--affine-placeholder-color);
color: ${unsafeCSSVarV2('text/placeholder')};
}
}

View File

@@ -10,7 +10,6 @@ import {
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -33,15 +32,7 @@ export const bookmarkBlockMarkdownAdapterMatcher =
toMatch: o => isUrlFootnoteDefinitionNode(o.node),
toBlockSnapshot: {
enter: (o, context) => {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
if (!isFootnoteDefinitionNode(o.node)) {
return;
}

View File

@@ -29,6 +29,15 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
};
}
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
this.gfx.selection.slots.updated.subscribe(() => {
this.requestUpdate();
})
);
}
override renderGfxBlock() {
const style = this.model.props.style$.value;
const width = EMBED_CARD_WIDTH[style];
@@ -36,12 +45,14 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
const bound = this.model.elementBound;
const scaleX = bound.w / width;
const scaleY = bound.h / height;
const isSelected = this.gfx.selection.has(this.model.id);
this.containerStyleMap = styleMap({
width: `100%`,
height: `100%`,
transform: `scale(${scaleX}, ${scaleY})`,
transformOrigin: '0 0',
pointerEvents: isSelected ? 'auto' : 'none',
});
return this.renderPageContent();

View File

@@ -1,5 +1,5 @@
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { WebIcon16 } from '@blocksuite/affine-components/icons';
import { LoadingIcon, WebIcon16 } from '@blocksuite/affine-components/icons';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getHostName } from '@blocksuite/affine-shared/utils';
@@ -60,11 +60,11 @@ export class BookmarkCard extends SignalWatcher(
: title;
const theme = this.bookmark.std.get(ThemeProvider).theme;
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const imageProxyService = this.bookmark.store.get(ImageProxyService);
const titleIcon = this.loading
? LoadingIcon
? LoadingIcon()
: icon
? html`<img src=${imageProxyService.buildUrl(icon)} alt="icon" />`
: WebIcon16;

View File

@@ -12,6 +12,7 @@ import type { BlockComponent } from '@blocksuite/std';
import { flip, offset } from '@floating-ui/dom';
import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
static override styles = css`
:host {
@@ -109,14 +110,18 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
}
override renderBlock() {
const emoji = this.model.props.emoji$.value;
return html`
<div class="affine-callout-block-container">
<div
@click=${this._toggleEmojiMenu}
contenteditable="false"
class="affine-callout-emoji-container"
style=${styleMap({
display: emoji.length === 0 ? 'none' : undefined,
})}
>
<span class="affine-callout-emoji">${this.model.props.emoji$}</span>
<span class="affine-callout-emoji">${emoji}</span>
</div>
<div class="affine-callout-children">
${this.renderChildren(this.model)}

View File

@@ -23,9 +23,9 @@ import {
createRecordDetail,
createUniComponentFromWebComponent,
type DataSource,
DataView,
dataViewCommonStyle,
type DataViewProps,
DataViewRootUILogic,
type DataViewSelection,
type DataViewWidget,
type DataViewWidgetProps,
@@ -133,8 +133,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
private _dataSource?: DataSource;
private readonly dataView = new DataView();
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
return {
dispose: this.host.event.bindHotkey(hotkeys, {
@@ -232,10 +230,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
return this.rootComponent;
}
get view() {
return this.dataView.expose;
}
private renderDatabaseOps() {
if (this.store.readonly) {
return nothing;
@@ -250,68 +244,68 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
}
private readonly dataViewRootLogic = new DataViewRootUILogic({
virtualPadding$: signal(0),
bindHotkey: this._bindHotkey,
handleEvent: this._handleEvent,
selection$: this.selection$,
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
const telemetryService = this.std.getOptional(TelemetryProvider);
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId,
});
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
const peekViewService = this.std.getOptional(PeekViewProvider);
if (peekViewService) {
const template = createRecordDetail({
...data,
openDoc: () => {},
detail: {
header: uniMap(
createUniComponentFromWebComponent(BlockRenderer),
props => ({
...props,
host: this.host,
})
),
note: uniMap(
createUniComponentFromWebComponent(NoteRenderer),
props => ({
...props,
model: this.model,
host: this.host,
})
),
},
});
return peekViewService.peek({ target, template });
} else {
return Promise.resolve();
}
},
},
});
override renderBlock() {
const peekViewService = this.std.getOptional(PeekViewProvider);
const telemetryService = this.std.getOptional(TelemetryProvider);
return html`
<div contenteditable="false" style="position: relative">
${this.dataView.render({
virtualPadding$: signal(0),
bindHotkey: this._bindHotkey,
handleEvent: this._handleEvent,
selection$: this.selection$,
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId,
});
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
if (peekViewService) {
const template = createRecordDetail({
...data,
openDoc: () => {},
detail: {
header: uniMap(
createUniComponentFromWebComponent(BlockRenderer),
props => ({
...props,
host: this.host,
})
),
note: uniMap(
createUniComponentFromWebComponent(NoteRenderer),
props => ({
...props,
model: this.model,
host: this.host,
})
),
},
});
return peekViewService.peek({ target, template });
} else {
return Promise.resolve();
}
},
},
})}
${this.dataViewRootLogic.render()}
</div>
`;
}

View File

@@ -1,15 +1,19 @@
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import type { DataViewUILogicBase } from '@blocksuite/data-view';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import type { Text } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import { css, html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
export class DatabaseTitle extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
.affine-database-title {
position: relative;
@@ -71,22 +75,23 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
`;
private readonly compositionEnd = () => {
this.isComposing$.value = false;
this.titleText.replace(0, this.titleText.length, this.input.value);
};
private readonly onBlur = () => {
this.isFocus = false;
this.isFocus$.value = false;
};
private readonly onFocus = () => {
this.isFocus = true;
if (this.database?.viewSelection$?.value) {
this.database?.setSelection(undefined);
this.isFocus$.value = true;
if (this.dataViewLogic.selection$.value) {
this.dataViewLogic.setSelection(undefined);
}
};
private readonly onInput = (e: InputEvent) => {
this.text = this.input.value;
this.text$.value = this.input.value;
if (!e.isComposing) {
this.titleText.replace(0, this.titleText.length, this.input.value);
}
@@ -102,9 +107,9 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
};
updateText = () => {
if (!this.isFocus) {
if (!this.isFocus$.value) {
this.input.value = this.titleText.toString();
this.text = this.input.value;
this.text$.value = this.input.value;
}
};
@@ -124,25 +129,25 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
}
override render() {
const isEmpty = !this.text;
const isEmpty = !this.text$.value;
const classList = classMap({
'affine-database-title': true,
ellipsis: !this.isFocus,
ellipsis: !this.isFocus$.value,
});
const untitledStyle = styleMap({
height: isEmpty ? 'auto' : 0,
opacity: isEmpty && !this.isFocus ? 1 : 0,
opacity: isEmpty && !this.isFocus$.value ? 1 : 0,
});
return html` <div
class="${classList}"
data-title-empty="${isEmpty}"
data-title-focus="${this.isFocus}"
data-title-focus="${this.isFocus$.value}"
>
<div class="text" style="${untitledStyle}">Untitled</div>
<div class="text">${this.text}</div>
<div class="text">${this.text$.value}</div>
<textarea
.disabled="${this.readonly}"
.disabled="${this.readonly$.value}"
@input="${this.onInput}"
@keydown="${this.onKeyDown}"
@copy="${stopPropagation}"
@@ -159,23 +164,24 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
@query('textarea')
private accessor input!: HTMLTextAreaElement;
@state()
accessor isComposing = false;
private readonly isComposing$ = signal(false);
private readonly isFocus$ = signal(false);
@state()
private accessor isFocus = false;
private onPressEnterKey() {
this.dataViewLogic.addRow?.('start');
}
@property({ attribute: false })
accessor onPressEnterKey: (() => void) | undefined = undefined;
get readonly$() {
return this.dataViewLogic.view.readonly$;
}
@property({ attribute: false })
accessor readonly!: boolean;
@state()
private accessor text = '';
private readonly text$ = signal('');
@property({ attribute: false })
accessor titleText!: Text;
@property({ attribute: false })
accessor dataViewLogic!: DataViewUILogicBase;
}
declare global {

View File

@@ -0,0 +1,73 @@
import { css } from '@emotion/css';
import { cssVarV2 } from '@toeverything/theme/v2';
export const databaseBlockStyles = css({
display: 'block',
borderRadius: '8px',
backgroundColor: 'var(--affine-background-primary-color)',
padding: '8px',
margin: '8px -8px -8px',
});
export const databaseBlockSelectedStyles = css({
backgroundColor: 'var(--affine-hover-color)',
borderRadius: '4px',
});
export const databaseOpsStyles = css({
padding: '2px',
borderRadius: '4px',
display: 'flex',
cursor: 'pointer',
alignItems: 'center',
height: 'max-content',
fontSize: '16px',
color: cssVarV2.icon.primary,
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
'@media print': {
display: 'none',
},
});
export const databaseHeaderBarStyles = css({
'@media print': {
display: 'none !important',
},
});
export const databaseTitleStyles = css({
overflow: 'hidden',
});
export const databaseHeaderContainerStyles = css({
marginBottom: '16px',
display: 'flex',
flexDirection: 'column',
});
export const databaseTitleRowStyles = css({
display: 'flex',
gap: '12px',
marginBottom: '8px',
alignItems: 'center',
});
export const databaseToolbarRowStyles = css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
});
export const databaseViewBarContainerStyles = css({
flex: 1,
});
export const databaseContentStyles = css({
position: 'relative',
backgroundColor: 'var(--affine-background-primary-color)',
borderRadius: '4px',
});

View File

@@ -19,15 +19,14 @@ import { getDropResult } from '@blocksuite/affine-widget-drag-handle';
import {
createRecordDetail,
createUniComponentFromWebComponent,
DataView,
dataViewCommonStyle,
type DataViewInstance,
type DataViewProps,
DataViewRootUILogic,
type DataViewSelection,
type DataViewUILogicBase,
type DataViewWidget,
type DataViewWidgetProps,
defineUniComponent,
ExternalGroupByConfigProvider,
lazy,
renderUniLit,
type SingleView,
uniMap,
@@ -44,12 +43,23 @@ import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import { Slice } from '@blocksuite/store';
import { autoUpdate } from '@floating-ui/dom';
import { computed, signal } from '@preact/signals-core';
import { css, html, nothing, unsafeCSS } from 'lit';
import { html, nothing } from 'lit';
import { popSideDetail } from './components/layout.js';
import { DatabaseConfigExtension } from './config.js';
import { EditorHostKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js';
import {
databaseBlockStyles,
databaseContentStyles,
databaseHeaderBarStyles,
databaseHeaderContainerStyles,
databaseOpsStyles,
databaseTitleRowStyles,
databaseTitleStyles,
databaseToolbarRowStyles,
databaseViewBarContainerStyles,
} from './database-block-styles.js';
import { BlockRenderer } from './detail-panel/block-renderer.js';
import { NoteRenderer } from './detail-panel/note-renderer.js';
import { DatabaseSelection } from './selection.js';
@@ -58,52 +68,7 @@ import { getSingleDocIdFromText } from './utils/title-doc.js';
import type { DatabaseViewExtensionOptions } from './view';
export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBlockModel> {
static override styles = css`
${unsafeCSS(dataViewCommonStyle('affine-database'))}
affine-database {
display: block;
border-radius: 8px;
background-color: var(--affine-background-primary-color);
padding: 8px;
margin: 8px -8px -8px;
}
.database-block-selected {
background-color: var(--affine-hover-color);
border-radius: 4px;
}
.database-ops {
padding: 2px;
border-radius: 4px;
display: flex;
cursor: pointer;
align-items: center;
height: max-content;
}
.database-ops svg {
width: 16px;
height: 16px;
color: var(--affine-icon-color);
}
.database-ops:hover {
background-color: var(--affine-hover-color);
}
@media print {
.database-ops {
display: none;
}
.database-header-bar {
display: none !important;
}
}
`;
private readonly _clickDatabaseOps = (e: MouseEvent) => {
private readonly clickDatabaseOps = (e: MouseEvent) => {
const options = this.optionsConfig.configure(this.model, {
items: [
menu.input({
@@ -155,36 +120,33 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
});
};
private _dataSource?: DatabaseBlockDataSource;
private readonly dataSource = lazy(() => {
const dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
dataSource.serviceSet(EditorHostKey, this.host);
this.std.provider
.getAll(ExternalGroupByConfigProvider)
.forEach(config => {
dataSource.serviceSet(
ExternalGroupByConfigProvider(config.name),
config
);
});
});
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && dataSource.viewManager.viewGet(id)) {
dataSource.viewManager.setCurrentView(id);
}
return dataSource;
});
private readonly dataView = new DataView();
private readonly renderTitle = (dataViewMethod: DataViewInstance) => {
const addRow = () => dataViewMethod.addRow?.('start');
private readonly renderTitle = (dataViewLogic: DataViewUILogicBase) => {
return html` <affine-database-title
style="overflow: hidden"
class="${databaseTitleStyles}"
.titleText="${this.model.props.title}"
.readonly="${this.dataSource.readonly$.value}"
.onPressEnterKey="${addRow}"
.dataViewLogic="${dataViewLogic}"
></affine-database-title>`;
};
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
return {
dispose: this.host.event.bindHotkey(hotkeys, {
blockId: this.topContenteditableElement?.blockId ?? this.blockId,
}),
};
};
_handleEvent: DataViewProps['handleEvent'] = (name, handler) => {
return {
dispose: this.host.event.add(name, handler, {
blockId: this.blockId,
}),
};
};
createTemplate = (
data: {
view: SingleView;
@@ -218,18 +180,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
headerWidget: DataViewWidget = defineUniComponent(
(props: DataViewWidgetProps) => {
return html`
<div style="margin-bottom: 16px;display:flex;flex-direction: column">
<div
style="display:flex;gap:12px;margin-bottom: 8px;align-items: center"
>
${this.renderTitle(props.dataViewInstance)}
${this.renderDatabaseOps()}
<div class="${databaseHeaderContainerStyles}">
<div class="${databaseTitleRowStyles}">
${this.renderTitle(props.dataViewLogic)} ${this.renderDatabaseOps()}
</div>
<div
style="display:flex;align-items:center;justify-content: space-between;gap: 12px"
class="database-header-bar"
>
<div style="flex:1">
<div class="${databaseToolbarRowStyles} ${databaseHeaderBarStyles}">
<div class="${databaseViewBarContainerStyles}">
${renderUniLit(widgetPresets.viewBar, {
...props,
onChangeView: id => {
@@ -284,7 +240,9 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return () => {};
};
setSelection = (selection: DataViewSelection | undefined) => {
private readonly setSelection = (
selection: DataViewSelection | undefined
) => {
if (selection) {
getSelection()?.removeAllRanges();
}
@@ -301,7 +259,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
);
};
toolsWidget: DataViewWidget = widgetPresets.createTools({
private readonly toolsWidget: DataViewWidget = widgetPresets.createTools({
table: [
widgetPresets.tools.filter,
widgetPresets.tools.sort,
@@ -318,7 +276,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
],
});
viewSelection$ = computed(() => {
private readonly viewSelection$ = computed(() => {
const databaseSelection = this.selection.value.find(
(selection): selection is DatabaseSelection => {
if (selection.blockId !== this.blockId) {
@@ -330,28 +288,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return databaseSelection?.viewSelection;
});
virtualPadding$ = signal(0);
get dataSource(): DatabaseBlockDataSource {
if (!this._dataSource) {
this._dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
dataSource.serviceSet(EditorHostKey, this.host);
this.std.provider
.getAll(ExternalGroupByConfigProvider)
.forEach(config => {
dataSource.serviceSet(
ExternalGroupByConfigProvider(config.name),
config
);
});
});
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && this.dataSource.viewManager.viewGet(id)) {
this.dataSource.viewManager.setCurrentView(id);
}
}
return this._dataSource;
}
private readonly virtualPadding$ = signal(0);
get optionsConfig(): DatabaseViewExtensionOptions {
return {
@@ -369,15 +306,15 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return this.rootComponent;
}
get view() {
return this.dataView.expose;
}
private renderDatabaseOps() {
if (this.dataSource.readonly$.value) {
if (this.dataSource.value.readonly$.value) {
return nothing;
}
return html` <div class="database-ops" @click="${this._clickDatabaseOps}">
return html` <div
data-testid="database-ops"
class="${databaseOpsStyles}"
@click="${this.clickDatabaseOps}"
>
${MoreHorizontalIcon()}
</div>`;
}
@@ -386,6 +323,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
super.connectedCallback();
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
this.classList.add(databaseBlockStyles);
this.listenFullWidthChange();
}
@@ -402,85 +340,97 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
})
);
}
override renderBlock() {
const peekViewService = this.std.getOptional(PeekViewProvider);
const telemetryService = this.std.getOptional(TelemetryProvider);
return html`
<div
contenteditable="false"
style="position: relative;background-color: var(--affine-background-primary-color);border-radius: 4px"
>
${this.dataView.render({
virtualPadding$: this.virtualPadding$,
bindHotkey: this._bindHotkey,
handleEvent: this._handleEvent,
selection$: this.viewSelection$,
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
onDrag: this.onDrag,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
private readonly dataViewRootLogic = lazy(
() =>
new DataViewRootUILogic({
virtualPadding$: this.virtualPadding$,
bindHotkey: hotkeys => {
return {
dispose: this.host.event.bindHotkey(hotkeys, {
blockId: this.topContenteditableElement?.blockId ?? this.blockId,
}),
};
},
handleEvent: (name, handler) => {
return {
dispose: this.host.event.add(name, handler, {
blockId: this.blockId,
});
}),
};
},
selection$: this.viewSelection$,
setSelection: this.setSelection,
dataSource: this.dataSource.value,
headerWidget: this.headerWidget,
onDrag: this.onDrag,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
if (peekViewService) {
const openDoc = (docId: string) => {
return peekViewService.peek({
docId,
databaseId: this.blockId,
databaseDocId: this.model.store.id,
databaseRowId: data.rowId,
target: this,
});
};
const doc = getSingleDocIdFromText(
this.model.store.getBlock(data.rowId)?.model?.text
);
if (doc) {
return openDoc(doc);
}
const abort = new AbortController();
return new Promise<void>(focusBack => {
peekViewService
.peek(
{
target,
template: this.createTemplate(data, docId => {
// abort.abort();
openDoc(docId).then(focusBack).catch(focusBack);
}),
},
{ abortSignal: abort.signal }
)
.then(focusBack)
.catch(focusBack);
},
eventTrace: (key, params) => {
const telemetryService = this.std.getOptional(TelemetryProvider);
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId,
});
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
const peekViewService = this.std.getOptional(PeekViewProvider);
if (peekViewService) {
const openDoc = (docId: string) => {
return peekViewService.peek({
docId,
databaseId: this.blockId,
databaseDocId: this.model.store.id,
databaseRowId: data.rowId,
target: this,
});
} else {
return popSideDetail(
this.createTemplate(data, () => {
//
})
);
};
const doc = getSingleDocIdFromText(
this.model.store.getBlock(data.rowId)?.model?.text
);
if (doc) {
return openDoc(doc);
}
},
const abort = new AbortController();
return new Promise<void>(focusBack => {
peekViewService
.peek(
{
target,
template: this.createTemplate(data, docId => {
// abort.abort();
openDoc(docId).then(focusBack).catch(focusBack);
}),
},
{ abortSignal: abort.signal }
)
.then(focusBack)
.catch(focusBack);
});
} else {
return popSideDetail(
this.createTemplate(data, () => {
//
})
);
}
},
})}
},
})
);
override renderBlock() {
return html`
<div contenteditable="false" class="${databaseContentStyles}">
${this.dataViewRootLogic.value.render()}
</div>
`;
}

View File

@@ -22,10 +22,7 @@ import {
GfxBlockComponent,
TextSelection,
} from '@blocksuite/std';
import {
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { computed } from '@preact/signals-core';
import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
@@ -282,69 +279,6 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
};
}
override onSelected(context: SelectedContext): void | boolean {
const { selected, multiSelect, event: e } = context;
const { editing } = this.gfx.selection;
const alreadySelected = this.gfx.selection.has(this.model.id);
if (!multiSelect && selected && (alreadySelected || editing)) {
if (this.model.isLocked()) return;
if (alreadySelected && editing) {
return;
}
this.gfx.selection.set({
elements: [this.model.id],
editing: true,
});
this.updateComplete
.then(() => {
if (!this.isConnected) {
return;
}
if (this.model.children.length === 0) {
const blockId = this.store.addBlock(
'affine:paragraph',
{ type: 'text' },
this.model.id
);
if (blockId) {
focusTextModel(this.std, blockId);
}
} else {
const rect = this.querySelector(
'.affine-block-children-container'
)?.getBoundingClientRect();
if (rect) {
const offsetY = 8 * this.gfx.viewport.zoom;
const offsetX = 2 * this.gfx.viewport.zoom;
const x = clamp(
e.clientX,
rect.left + offsetX,
rect.right - offsetX
);
const y = clamp(
e.clientY,
rect.top + offsetY,
rect.bottom - offsetY
);
handleNativeRangeAtPoint(x, y);
} else {
handleNativeRangeAtPoint(e.clientX, e.clientY);
}
}
})
.catch(console.error);
} else {
return super.onSelected(context);
}
}
override renderGfxBlock() {
const { model } = this;
const { rotate, hasMaxWidth } = model.props;
@@ -506,5 +440,73 @@ export const EdgelessTextInteraction =
},
};
},
handleSelection: context => {
const { gfx, std, view, model } = context;
return {
onSelect(context) {
const { selected, multiSelect, event: e } = context;
const { editing } = gfx.selection;
const alreadySelected = gfx.selection.has(model.id);
if (!multiSelect && selected && (alreadySelected || editing)) {
if (model.isLocked()) return;
if (alreadySelected && editing) {
return;
}
gfx.selection.set({
elements: [model.id],
editing: true,
});
view.updateComplete
.then(() => {
if (!view.isConnected) {
return;
}
if (model.children.length === 0) {
const blockId = std.store.addBlock(
'affine:paragraph',
{ type: 'text' },
model.id
);
if (blockId) {
focusTextModel(std, blockId);
}
} else {
const rect = view
.querySelector('.affine-block-children-container')
?.getBoundingClientRect();
if (rect) {
const offsetY = 8 * gfx.viewport.zoom;
const offsetX = 2 * gfx.viewport.zoom;
const x = clamp(
e.clientX,
rect.left + offsetX,
rect.right - offsetX
);
const y = clamp(
e.clientY,
rect.top + offsetY,
rect.bottom - offsetY
);
handleNativeRangeAtPoint(x, y);
} else {
handleNativeRangeAtPoint(e.clientX, e.clientY);
}
}
})
.catch(console.error);
} else {
return context.default(context);
}
},
};
},
}
);

View File

@@ -11,7 +11,6 @@ import {
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isLinkedDocFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -36,15 +35,7 @@ export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatc
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
if (!isFootnoteDefinitionNode(o.node)) {
return;
}

View File

@@ -3,6 +3,7 @@ import {
RENDER_CARD_THROTTLE_MS,
} from '@blocksuite/affine-block-embed';
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { LoadingIcon } from '@blocksuite/affine-components/icons';
import { isPeekable, Peekable } from '@blocksuite/affine-components/peek';
import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
import type {
@@ -31,6 +32,7 @@ import {
referenceToNode,
} from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx';
import { ResetIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { Text } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
@@ -337,8 +339,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
const theme = this.std.get(ThemeProvider).theme;
const {
LoadingIcon,
ReloadIcon,
LinkedDocDeletedBanner,
LinkedDocEmptyBanner,
SyncedDocErrorBanner,
@@ -347,7 +347,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
const icon = isError
? SyncedDocErrorIcon
: isLoading
? LoadingIcon
? LoadingIcon()
: this.icon$.value;
const title = isLoading ? 'Loading...' : this.title$;
const description = this.model.props.description$;
@@ -384,10 +384,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
() => html`
<div
class="affine-embed-linked-doc-block ${cardClassMap}"
style=${styleMap({
transform: `scale(${this._scale})`,
transformOrigin: '0 0',
})}
@click=${this._handleClick}
@dblclick=${this._handleDoubleClick}
>
@@ -433,7 +429,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
class="affine-embed-linked-doc-card-content-reload-button"
@click=${this.refreshData}
>
${ReloadIcon} <span>Reload</span>
${ResetIcon()} <span>Reload</span>
</div>
</div>
`

View File

@@ -124,11 +124,11 @@ export const styles = css`
align-items: center;
gap: 4px;
cursor: pointer;
color: ${unsafeCSSVarV2('button/primary')};
}
.affine-embed-linked-doc-card-content-reload-button svg {
width: 12px;
height: 12px;
fill: var(--affine-background-primary-color);
}
.affine-embed-linked-doc-card-content-reload-button > span {
display: -webkit-box;
@@ -138,7 +138,6 @@ export const styles = css`
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-brand-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;
@@ -305,7 +304,6 @@ export const styles = css`
.affine-embed-linked-doc-content-note {
-webkit-line-clamp: 16;
max-height: 320px;
}
.affine-embed-linked-doc-content-date {

View File

@@ -1,8 +1,6 @@
import {
EmbedEdgelessIcon,
EmbedPageIcon,
getLoadingIconWith,
ReloadIcon,
} from '@blocksuite/affine-components/icons';
import {
ColorScheme,
@@ -35,8 +33,6 @@ import {
} from './styles.js';
type EmbedCardImages = {
LoadingIcon: TemplateResult<1>;
ReloadIcon: TemplateResult<1>;
LinkedDocIcon: TemplateResult<1>;
LinkedDocDeletedIcon: TemplateResult<1>;
LinkedDocEmptyBanner: TemplateResult<1>;
@@ -50,12 +46,9 @@ export function getEmbedLinkedDocIcons(
style: (typeof EmbedLinkedDocStyles)[number]
): EmbedCardImages {
const small = style !== 'vertical';
const LoadingIcon = getLoadingIconWith(theme);
if (editorMode === 'page') {
if (theme === ColorScheme.Light) {
return {
LoadingIcon,
ReloadIcon,
LinkedDocIcon: EmbedPageIcon,
LinkedDocDeletedIcon,
LinkedDocEmptyBanner: small
@@ -68,8 +61,6 @@ export function getEmbedLinkedDocIcons(
};
} else {
return {
ReloadIcon,
LoadingIcon,
LinkedDocIcon: EmbedPageIcon,
LinkedDocDeletedIcon,
LinkedDocEmptyBanner: small
@@ -84,8 +75,6 @@ export function getEmbedLinkedDocIcons(
} else {
if (theme === ColorScheme.Light) {
return {
ReloadIcon,
LoadingIcon,
LinkedDocIcon: EmbedEdgelessIcon,
LinkedDocDeletedIcon,
LinkedDocEmptyBanner: small
@@ -98,8 +87,6 @@ export function getEmbedLinkedDocIcons(
};
} else {
return {
ReloadIcon,
LoadingIcon,
LinkedDocIcon: EmbedEdgelessIcon,
LinkedDocDeletedIcon,
LinkedDocEmptyBanner: small

View File

@@ -1,6 +1,8 @@
import { RENDER_CARD_THROTTLE_MS } from '@blocksuite/affine-block-embed';
import { LoadingIcon } from '@blocksuite/affine-components/icons';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { WithDisposable } from '@blocksuite/global/lit';
import { ResetIcon } from '@blocksuite/icons/lit';
import {
BlockSelection,
isGfxBlockComponent,
@@ -148,9 +150,7 @@ export class EmbedSyncedDocCard extends WithDisposable(ShadowlessElement) {
const theme = this.std.get(ThemeProvider).theme;
const {
LoadingIcon,
SyncedDocErrorIcon,
ReloadIcon,
SyncedDocEmptyBanner,
SyncedDocErrorBanner,
SyncedDocDeletedBanner,
@@ -159,7 +159,7 @@ export class EmbedSyncedDocCard extends WithDisposable(ShadowlessElement) {
const icon = error
? SyncedDocErrorIcon
: isLoading
? LoadingIcon
? LoadingIcon()
: this.block.icon$.value;
const title = isLoading ? 'Loading...' : this.block.title$;
@@ -216,7 +216,7 @@ export class EmbedSyncedDocCard extends WithDisposable(ShadowlessElement) {
class="affine-embed-synced-doc-card-content-reload-button"
@click=${() => this.block.refreshData()}
>
${ReloadIcon} <span>Reload</span>
${ResetIcon()} <span>Reload</span>
</div>
</div>
`

View File

@@ -303,11 +303,11 @@ export const cardStyles = css`
align-items: center;
gap: 4px;
cursor: pointer;
color: ${unsafeCSSVarV2('button/primary')};
}
.affine-embed-synced-doc-card-content-reload-button svg {
width: 12px;
height: 12px;
fill: var(--affine-background-primary-color);
}
.affine-embed-synced-doc-card-content-reload-button > span {
display: -webkit-box;
@@ -317,7 +317,6 @@ export const cardStyles = css`
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-brand-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;

View File

@@ -1,8 +1,6 @@
import {
EmbedEdgelessIcon,
EmbedPageIcon,
getLoadingIconWith,
ReloadIcon,
} from '@blocksuite/affine-components/icons';
import { ColorScheme } from '@blocksuite/affine-model';
import type { BlockComponent } from '@blocksuite/std';
@@ -21,11 +19,9 @@ import {
} from './styles.js';
type SyncedCardImages = {
LoadingIcon: TemplateResult<1>;
SyncedDocIcon: TemplateResult<1>;
SyncedDocErrorIcon: TemplateResult<1>;
SyncedDocDeletedIcon: TemplateResult<1>;
ReloadIcon: TemplateResult<1>;
SyncedDocEmptyBanner: TemplateResult<1>;
SyncedDocErrorBanner: TemplateResult<1>;
SyncedDocDeletedBanner: TemplateResult<1>;
@@ -35,25 +31,20 @@ export function getSyncedDocIcons(
theme: ColorScheme,
editorMode: 'page' | 'edgeless'
): SyncedCardImages {
const LoadingIcon = getLoadingIconWith(theme);
if (theme === ColorScheme.Light) {
return {
LoadingIcon,
SyncedDocIcon: editorMode === 'page' ? EmbedPageIcon : EmbedEdgelessIcon,
SyncedDocErrorIcon,
SyncedDocDeletedIcon,
ReloadIcon,
SyncedDocEmptyBanner: LightSyncedDocEmptyBanner,
SyncedDocErrorBanner: LightSyncedDocErrorBanner,
SyncedDocDeletedBanner: LightSyncedDocDeletedBanner,
};
} else {
return {
LoadingIcon,
SyncedDocIcon: editorMode === 'page' ? EmbedPageIcon : EmbedEdgelessIcon,
SyncedDocErrorIcon,
SyncedDocDeletedIcon,
ReloadIcon,
SyncedDocEmptyBanner: DarkSyncedDocEmptyBanner,
SyncedDocErrorBanner: DarkSyncedDocErrorBanner,
SyncedDocDeletedBanner: DarkSyncedDocDeletedBanner,

View File

@@ -50,12 +50,6 @@ export class EmbedBlockComponent<
_cardStyle: EmbedCardStyle = 'horizontal';
/**
* The actual rendered scale of the embed card.
* By default, it is set to 1.
*/
protected _scale = 1;
blockDraggable = true;
/**

View File

@@ -68,7 +68,6 @@ export function toEdgelessEmbedBlock<
this.blockContainerStyles = {
width: `${bound.w}px`,
};
this._scale = bound.w / this._cardWidth;
return this.renderPageContent();
}

View File

@@ -9,13 +9,11 @@ import {
EmbedCardLightHorizontalIcon,
EmbedCardLightListIcon,
EmbedCardLightVerticalIcon,
getLoadingIconWith,
} from '@blocksuite/affine-components/icons';
import { ColorScheme } from '@blocksuite/affine-model';
import type { TemplateResult } from 'lit';
type EmbedCardIcons = {
LoadingIcon: TemplateResult<1>;
EmbedCardBannerIcon: TemplateResult<1>;
EmbedCardHorizontalIcon: TemplateResult<1>;
EmbedCardListIcon: TemplateResult<1>;
@@ -24,11 +22,8 @@ type EmbedCardIcons = {
};
export function getEmbedCardIcons(theme: ColorScheme): EmbedCardIcons {
const LoadingIcon = getLoadingIconWith(theme);
if (theme === ColorScheme.Light) {
return {
LoadingIcon,
EmbedCardBannerIcon: EmbedCardLightBannerIcon,
EmbedCardHorizontalIcon: EmbedCardLightHorizontalIcon,
EmbedCardListIcon: EmbedCardLightListIcon,
@@ -37,7 +32,6 @@ export function getEmbedCardIcons(theme: ColorScheme): EmbedCardIcons {
};
} else {
return {
LoadingIcon,
EmbedCardBannerIcon: EmbedCardDarkBannerIcon,
EmbedCardHorizontalIcon: EmbedCardDarkHorizontalIcon,
EmbedCardListIcon: EmbedCardDarkListIcon,

View File

@@ -6,7 +6,6 @@ import type {
import { BlockSelection } from '@blocksuite/std';
import { html, nothing } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { EmbedBlockComponent } from '../common/embed-block-element.js';
import { FigmaIcon, styles } from './styles.js';
@@ -76,10 +75,6 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent<EmbedFigmaMode
'affine-embed-figma-block': true,
selected: this.selected$.value,
})}
style=${styleMap({
transform: `scale(${this._scale})`,
transformOrigin: '0 0',
})}
@click=${this._handleClick}
@dblclick=${this._handleDoubleClick}
>

View File

@@ -1,4 +1,4 @@
import { OpenIcon } from '@blocksuite/affine-components/icons';
import { LoadingIcon, OpenIcon } from '@blocksuite/affine-components/icons';
import type {
EmbedGithubModel,
EmbedGithubStyles,
@@ -133,8 +133,8 @@ export class EmbedGithubBlockComponent extends EmbedBlockComponent<
const loading = this.loading;
const theme = this.std.get(ThemeProvider).theme;
const imageProxyService = this.store.get(ImageProxyService);
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon : GithubIcon;
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon() : GithubIcon;
const statusIcon = status
? getGithubStatusIcon(githubType, status, statusReason)
: nothing;

View File

@@ -1,4 +1,4 @@
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { LoadingIcon } from '@blocksuite/affine-components/icons';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { EmbedIcon } from '@blocksuite/icons/lit';
import { type BlockStdScope } from '@blocksuite/std';
@@ -7,7 +7,6 @@ import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { getEmbedCardIcons } from '../../common/utils';
import { LOADING_CARD_DEFAULT_HEIGHT } from '../consts';
import type { EmbedIframeStatusCardOptions } from '../types';
@@ -156,9 +155,6 @@ export class EmbedIframeLoadingCard extends LitElement {
`;
override render() {
const theme = this.std.get(ThemeProvider).theme;
const { LoadingIcon } = getEmbedCardIcons(theme);
const { layout, width, height } = this.options;
const cardClasses = classMap({
'affine-embed-iframe-loading-card': true,
@@ -176,7 +172,7 @@ export class EmbedIframeLoadingCard extends LitElement {
return html`
<div class=${cardClasses} style=${cardStyle}>
<div class="loading-content">
<div class="loading-spinner">${LoadingIcon}</div>
<div class="loading-spinner">${LoadingIcon()}</div>
<div class="loading-text">Loading...</div>
</div>
<div class="loading-banner">

View File

@@ -1,4 +1,4 @@
import { OpenIcon } from '@blocksuite/affine-components/icons';
import { LoadingIcon, OpenIcon } from '@blocksuite/affine-components/icons';
import type { EmbedLoomModel, EmbedLoomStyles } from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
@@ -94,8 +94,8 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
const loading = this.loading;
const theme = this.std.get(ThemeProvider).theme;
const imageProxyService = this.store.get(ImageProxyService);
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon : LoomIcon;
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon() : LoomIcon;
const titleText = loading ? 'Loading...' : title;
const descriptionText = loading ? '' : description;
const bannerImage =
@@ -112,7 +112,6 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
selected: this.selected$.value,
})}
style=${styleMap({
transform: `scale(${this._scale})`,
transformOrigin: '0 0',
})}
@click=${this._handleClick}

View File

@@ -1,4 +1,4 @@
import { OpenIcon } from '@blocksuite/affine-components/icons';
import { LoadingIcon, OpenIcon } from '@blocksuite/affine-components/icons';
import type {
EmbedYoutubeModel,
EmbedYoutubeStyles,
@@ -108,8 +108,8 @@ export class EmbedYoutubeBlockComponent extends EmbedBlockComponent<
const loading = this.loading;
const theme = this.std.get(ThemeProvider).theme;
const imageProxyService = this.store.get(ImageProxyService);
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon : YoutubeIcon;
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon() : YoutubeIcon;
const titleText = loading ? 'Loading...' : title;
const descriptionText = loading ? null : description;
const bannerImage =

View File

@@ -205,10 +205,11 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
!forceMove
) {
// Clear the flag so future navigations behave normally
this.gfx.tool.setTool(PresentTool, {
...toolOptions,
restoredAfterPan: false,
});
// Here we modify the tool's activated option to avoid triggering setTool update
const currentTool = this.gfx.tool.currentTool$.peek();
if (currentTool?.activatedOption) {
currentTool.activatedOption.restoredAfterPan = false;
}
return;
}

View File

@@ -3,6 +3,7 @@ import {
DefaultTheme,
type FrameBlockModel,
FrameBlockSchema,
isTransparent,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { Bound } from '@blocksuite/global/gfx';
@@ -11,7 +12,6 @@ import {
type BoxSelectionContext,
getTopElements,
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
@@ -68,22 +68,6 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
};
}
override onSelected(context: SelectedContext): boolean | void {
const { x, y } = context.position;
if (
!context.fallback &&
// if the frame is selected by title, then ignore it because the title selection is handled by the title widget
(this.model.externalBound?.containsPoint([x, y]) ||
// otherwise if the frame has title, then ignore it because in this case the frame cannot be selected by frame body
this.model.props.title.length)
) {
return false;
}
return super.onSelected(context);
}
override onBoxSelected(context: BoxSelectionContext) {
const { box } = context;
const bound = new Bound(box.x, box.y, box.w, box.h);
@@ -189,5 +173,17 @@ export const FrameBlockInteraction =
},
};
},
handleSelection: () => {
return {
selectable(context) {
const { model } = context;
return (
context.default(context) &&
(model.isLocked() || !isTransparent(model.props.background))
);
},
};
},
}
);

View File

@@ -46,12 +46,16 @@ export class ImageBlockPageComponent extends SignalWatcher(
justify-content: center;
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
padding: 4px;
border-radius: 4px;
left: 4px;
width: 36px;
height: 36px;
padding: 5px;
border-radius: 8px;
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
& > svg {
font-size: 25.71px;
}
}
affine-page-image .affine-image-status {

View File

@@ -1,19 +1,17 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { whenHover } from '@blocksuite/affine-components/hover';
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
import { LoadingIcon } 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,
} from '@blocksuite/affine-shared/services';
import { ToolbarRegistryIdentifier } from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { computed } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -126,9 +124,6 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
}
override renderBlock() {
const theme = this.std.get(ThemeProvider).theme$.value;
const loadingIcon = getLoadingIconWith(theme);
const blobUrl = this.blobUrl;
const { size = 0 } = this.model.props;
@@ -138,7 +133,9 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
});
const resovledState = this.resourceController.resolveStateWith({
loadingIcon,
loadingIcon: LoadingIcon({
strokeColor: cssVarV2('button/pureWhiteText'),
}),
errorIcon: BrokenImageIcon(),
icon: ImageIcon(),
title: 'Image',

View File

@@ -1,13 +1,12 @@
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
import { LoadingIcon } from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import { ResourceController } from '@blocksuite/affine-components/resource';
import {
type ImageBlockModel,
ImageBlockSchema,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { cssVarV2, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { formatSize } from '@blocksuite/affine-shared/utils';
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
import { GfxBlockComponent } from '@blocksuite/std';
@@ -39,11 +38,15 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
padding: 4px;
border-radius: 4px;
width: 36px;
height: 36px;
padding: 5px;
border-radius: 8px;
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
& > svg {
font-size: 25.71px;
}
}
affine-edgeless-image .affine-image-status {
@@ -108,9 +111,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
}
override renderGfxBlock() {
const theme = this.std.get(ThemeProvider).theme$.value;
const loadingIcon = getLoadingIconWith(theme);
const blobUrl = this.blobUrl;
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
@@ -124,7 +124,9 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
});
const resovledState = this.resourceController.resolveStateWith({
loadingIcon,
loadingIcon: LoadingIcon({
strokeColor: cssVarV2('button/pureWhiteText'),
}),
errorIcon: BrokenImageIcon(),
icon: ImageIcon(),
title: 'Image',
@@ -148,7 +150,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
</div>
${when(
resovledState.loading,
() => html`<div class="loading">${loadingIcon}</div>`
() => html`<div class="loading">${resovledState.icon}</div>`
)}
${when(
resovledState.error && resovledState.description,

View File

@@ -1,4 +1,8 @@
import type { LatexProps } from '@blocksuite/affine-model';
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
@@ -48,6 +52,21 @@ export const insertLatexBlockCommand: Command<
if (blockComponent instanceof LatexBlockComponent) {
await blockComponent.updateComplete;
blockComponent.toggleEditor();
const mode = std.get(DocModeProvider).getEditorMode() ?? 'page';
const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
std.getOptional(TelemetryProvider)?.track('Latex', {
from:
mode === 'page'
? 'doc'
: ifEdgelessText
? 'edgeless text'
: 'edgeless note',
page: mode === 'page' ? 'doc' : 'edgeless',
segment: mode === 'page' ? 'doc' : 'whiteboard',
module: 'equation',
control: 'create equation',
});
}
}
return result[0];

View File

@@ -74,19 +74,16 @@ export class LatexBlockComponent extends CaptionedBlockComponent<LatexBlockModel
}
})
);
}
this.disposables.addFromEvent(this, 'click', () => {
// should not open editor or select block in readonly mode
if (this.store.readonly) {
return;
}
private _handleClick() {
if (this.store.readonly) return;
if (this.isBlockSelected) {
this.toggleEditor();
} else {
this.selectBlock();
}
});
if (this.isBlockSelected) {
this.toggleEditor();
} else {
this.selectBlock();
}
}
removeEditor(portal: HTMLDivElement) {
@@ -95,7 +92,11 @@ export class LatexBlockComponent extends CaptionedBlockComponent<LatexBlockModel
override renderBlock() {
return html`
<div contenteditable="false" class="latex-block-container">
<div
contenteditable="false"
class="latex-block-container"
@click=${this._handleClick}
>
<div class="katex"></div>
</div>
`;

View File

@@ -6,7 +6,6 @@ import {
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import type { Root } from 'mdast';
const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
@@ -66,34 +65,19 @@ const createNoteBlockMarkdownAdapterMatcher = (
}
});
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (enableCitation) {
// if there are footnoteDefinition nodes, add a heading node to the noteAst before the first footnoteDefinition node
const footnoteDefinitionIndex = noteAst.children.findIndex(child =>
isFootnoteDefinitionNode(child)
);
if (footnoteDefinitionIndex !== -1) {
noteAst.children.splice(footnoteDefinitionIndex, 0, {
type: 'heading',
depth: 6,
data: {
collapsed: true,
},
children: [{ type: 'text', value: 'Sources' }],
});
}
} else {
// Remove the footnoteDefinition node from the noteAst
noteAst.children = noteAst.children.filter(
child => !isFootnoteDefinitionNode(child)
);
// if there are footnoteDefinition nodes, add a heading node to the noteAst before the first footnoteDefinition node
const footnoteDefinitionIndex = noteAst.children.findIndex(child =>
isFootnoteDefinitionNode(child)
);
if (footnoteDefinitionIndex !== -1) {
noteAst.children.splice(footnoteDefinitionIndex, 0, {
type: 'heading',
depth: 6,
data: {
collapsed: true,
},
children: [{ type: 'text', value: 'Sources' }],
});
}
},
},

View File

@@ -26,10 +26,9 @@ import {
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context';
import { computed } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { computed, effect } from '@preact/signals-core';
import { nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { NoteConfigExtension } from '../config';
import * as styles from './edgeless-note-background.css';
@@ -150,15 +149,20 @@ export class EdgelessNoteBackground extends SignalWatcher(
return header;
}
override connectedCallback() {
super.connectedCallback();
this.classList.add(styles.background);
this.disposables.add(
effect(() => {
Object.assign(this.style, this.backgroundStyle$.value);
})
);
this.disposables.addFromEvent(this, 'pointerdown', stopPropagation);
this.disposables.addFromEvent(this, 'click', this._handleClickAtBackground);
}
override render() {
return html`<div
class=${styles.background}
style=${styleMap(this.backgroundStyle$.value)}
@pointerdown=${stopPropagation}
@click=${this._handleClickAtBackground}
>
${this.note.isPageBlock() ? this._renderHeader() : nothing}
</div>`;
return this.note.isPageBlock() ? this._renderHeader() : nothing;
}
@consume({ context: stdContext })

View File

@@ -13,7 +13,6 @@ import { toGfxBlockComponent } from '@blocksuite/std';
import {
type BoxSelectionContext,
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { html, nothing, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js';
@@ -342,69 +341,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
`;
}
override onSelected(context: SelectedContext) {
const { selected, multiSelect, event: e } = context;
const { editing } = this.gfx.selection;
const alreadySelected = this.gfx.selection.has(this.model.id);
if (!multiSelect && selected && (alreadySelected || editing)) {
if (this.model.isLocked()) return;
if (alreadySelected && editing) {
return;
}
this.gfx.selection.set({
elements: [this.model.id],
editing: true,
});
this.updateComplete
.then(() => {
if (!this.isConnected) {
return;
}
if (this.model.children.length === 0) {
const blockId = this.store.addBlock(
'affine:paragraph',
{ type: 'text' },
this.model.id
);
if (blockId) {
focusTextModel(this.std, blockId);
}
} else {
const rect = this.querySelector(
'.affine-block-children-container'
)?.getBoundingClientRect();
if (rect) {
const offsetY = 8 * this.gfx.viewport.zoom;
const offsetX = 2 * this.gfx.viewport.zoom;
const x = clamp(
e.clientX,
rect.left + offsetX,
rect.right - offsetX
);
const y = clamp(
e.clientY,
rect.top + offsetY,
rect.bottom - offsetY
);
handleNativeRangeAtPoint(x, y);
} else {
handleNativeRangeAtPoint(e.clientX, e.clientY);
}
}
})
.catch(console.error);
} else {
super.onSelected(context);
}
}
override onBoxSelected(_: BoxSelectionContext) {
return this.model.props.displayMode !== NoteDisplayMode.DocOnly;
}
@@ -493,5 +429,71 @@ export const EdgelessNoteInteraction =
},
};
},
handleSelection: ({ std, gfx, view, model }) => {
return {
onSelect(context) {
const { selected, multiSelect, event: e } = context;
const { editing } = gfx.selection;
const alreadySelected = gfx.selection.has(model.id);
if (!multiSelect && selected && (alreadySelected || editing)) {
if (model.isLocked()) return;
if (alreadySelected && editing) {
return;
}
gfx.selection.set({
elements: [model.id],
editing: true,
});
view.updateComplete
.then(() => {
if (!view.isConnected) {
return;
}
if (model.children.length === 0) {
const blockId = std.store.addBlock(
'affine:paragraph',
{ type: 'text' },
model.id
);
if (blockId) {
focusTextModel(std, blockId);
}
} else {
const rect = view
.querySelector('.affine-block-children-container')
?.getBoundingClientRect();
if (rect) {
const offsetY = 8 * gfx.viewport.zoom;
const offsetX = 2 * gfx.viewport.zoom;
const x = clamp(
e.clientX,
rect.left + offsetX,
rect.right - offsetX
);
const y = clamp(
e.clientY,
rect.top + offsetY,
rect.bottom - offsetY
);
handleNativeRangeAtPoint(x, y);
} else {
handleNativeRangeAtPoint(e.clientX, e.clientY);
}
}
})
.catch(console.error);
} else {
context.default(context);
}
},
};
},
}
);

View File

@@ -1,6 +1,9 @@
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
import { FrameTool } from '@blocksuite/affine-block-frame';
import {
FrameTool,
type PresentToolOption,
} from '@blocksuite/affine-block-frame';
import {
DefaultTool,
EdgelessLegacySlotIdentifier,
@@ -472,9 +475,6 @@ 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 &&
@@ -512,9 +512,6 @@ 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;
@@ -712,10 +709,18 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const revertToPrevTool = (ev: KeyboardEvent) => {
if (ev.code === 'Space') {
this._setEdgelessTool(
(currentTool as DefaultTool).constructor as typeof DefaultTool,
currentTool?.activatedOption
);
const toolConstructor = currentTool.constructor as typeof DefaultTool;
let finalOptions = currentTool?.activatedOption;
// Handle frameNavigator (PresentTool) restoration after space pan
if (currentTool.toolName === 'frameNavigator') {
finalOptions = {
...currentTool?.activatedOption,
restoredAfterPan: true,
} as PresentToolOption;
}
this._setEdgelessTool(toolConstructor, finalOptions);
selection.set(currentSel);
document.removeEventListener('keyup', revertToPrevTool, false);
}
@@ -728,6 +733,14 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
) {
return;
}
// If in presentation mode, disable black background during space drag
if (currentTool.toolName === 'frameNavigator') {
this.slots.navigatorSettingUpdated.next({
blackBackground: false,
});
}
this._setEdgelessTool(PanTool, { panning: false });
this.std.event.disposables.addFromEvent(

View File

@@ -16,7 +16,6 @@ import type {
GfxController,
GfxModel,
LayerManager,
PointTestOptions,
ReorderingDirection,
} from '@blocksuite/std/gfx';
import {
@@ -168,19 +167,6 @@ export class EdgelessRootService
this._initReadonlyListener();
}
/**
* This method is used to pick element in group, if the picked element is in a
* group, we will pick the group instead. If that picked group is currently selected, then
* we will pick the element itself.
*/
pickElementInGroup(
x: number,
y: number,
options?: PointTestOptions
): GfxModel | null {
return this.gfx.getElementInGroup(x, y, options);
}
removeElement(id: string | GfxModel) {
id = typeof id === 'string' ? id : id.id;

View File

@@ -154,32 +154,6 @@ export class DefaultTool extends BaseTool {
private _determineDragType(evt: PointerEventState): DefaultModeDragType {
const { x, y } = this.controller.lastMousePos$.peek();
if (this.selection.isInSelectedRect(x, y)) {
if (this.selection.selectedElements.length === 1) {
const currentHoveredElem = this._getElementInGroup(x, y);
let curSelected = this.selection.selectedElements[0];
// If one of the following condition is true, keep the selection:
// 1. if group is currently selected
// 2. if the selected element is descendant of the hovered element
// 3. not hovering any element or hovering the same element
//
// Otherwise, we update the selection to the current hovered element
const shouldKeepSelection =
isGfxGroupCompatibleModel(curSelected) ||
(isGfxGroupCompatibleModel(currentHoveredElem) &&
currentHoveredElem.hasDescendant(curSelected)) ||
!currentHoveredElem ||
currentHoveredElem === curSelected;
if (!shouldKeepSelection) {
curSelected = currentHoveredElem;
this.selection.set({
elements: [curSelected.id],
editing: false,
});
}
}
return this.selection.editing
? DefaultModeDragType.NativeEditing
: DefaultModeDragType.ContentMoving;
@@ -194,17 +168,6 @@ export class DefaultTool extends BaseTool {
}
}
private _getElementInGroup(modelX: number, modelY: number) {
const tryGetLockedAncestor = (e: GfxModel | null) => {
if (e?.isLockedByAncestor()) {
return e.groups.findLast(group => group.isLocked());
}
return e;
};
return tryGetLockedAncestor(this.gfx.getElementInGroup(modelX, modelY));
}
private initializeDragState(
dragType: DefaultModeDragType,
event: PointerEventState

View File

@@ -67,6 +67,8 @@ export class TableSelection extends BaseSelection {
static override type = 'table';
static override recoverable = true;
readonly data: TableSelectionData;
constructor({

View File

@@ -111,6 +111,7 @@ export class MenuInput extends MenuFocusable {
}}"
@input="${this.onInput}"
placeholder="${this.data.placeholder ?? ''}"
@keypress="${this.stopPropagation}"
@keydown="${this.onKeydown}"
@copy="${this.stopPropagation}"
@paste="${this.stopPropagation}"

View File

@@ -92,6 +92,7 @@ export class FilterableListComponent<Props = unknown> extends WithDisposable(
const isFlip = !!this.placement?.startsWith('top');
const _handleInputKeydown = (ev: KeyboardEvent) => {
ev.stopPropagation();
switch (ev.key) {
case 'ArrowUp': {
ev.preventDefault();

View File

@@ -1,14 +1,23 @@
import { ColorScheme } from '@blocksuite/affine-model';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
const LoadingIcon = (color: string) =>
export const LoadingIcon = ({
size = '1em',
progress = 0.2,
strokeColor = cssVarV2('loading/foreground'),
}: {
size?: string;
progress?: number;
strokeColor?: string;
} = {}) =>
html`<svg
width="16"
height="16"
viewBox="0 0 16 16"
width="${size}"
height="${size}"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
>
<style xmlns="http://www.w3.org/2000/svg">
<style>
.spinner {
transform-origin: center;
animation: spinner_animate 0.75s infinite linear;
@@ -19,21 +28,24 @@ const LoadingIcon = (color: string) =>
}
}
</style>
<path
d="M14.6666 8.00004C14.6666 11.6819 11.6818 14.6667 7.99992 14.6667C4.31802 14.6667 1.33325 11.6819 1.33325 8.00004C1.33325 4.31814 4.31802 1.33337 7.99992 1.33337C11.6818 1.33337 14.6666 4.31814 14.6666 8.00004ZM3.30003 8.00004C3.30003 10.5957 5.40424 12.6999 7.99992 12.6999C10.5956 12.6999 12.6998 10.5957 12.6998 8.00004C12.6998 5.40436 10.5956 3.30015 7.99992 3.30015C5.40424 3.30015 3.30003 5.40436 3.30003 8.00004Z"
fill="${color}"
fill-opacity="0.1"
<circle
cx="12"
cy="12"
r="8"
stroke="${cssVarV2('loading/background')}"
stroke-width="4"
/>
<path
d="M13.6833 8.00004C14.2263 8.00004 14.674 7.55745 14.5942 7.02026C14.5142 6.48183 14.3684 5.954 14.1591 5.44882C13.8241 4.63998 13.333 3.90505 12.714 3.286C12.0949 2.66694 11.36 2.17588 10.5511 1.84084C10.046 1.63159 9.51812 1.48576 8.9797 1.40576C8.44251 1.32595 7.99992 1.77363 7.99992 2.31671C7.99992 2.85979 8.44486 3.28974 8.9761 3.40253C9.25681 3.46214 9.53214 3.54746 9.79853 3.65781C10.3688 3.894 10.8869 4.2402 11.3233 4.67664C11.7598 5.11307 12.106 5.6312 12.3422 6.20143C12.4525 6.46782 12.5378 6.74315 12.5974 7.02386C12.7102 7.5551 13.1402 8.00004 13.6833 8.00004Z"
fill="#1C9EE4"
<circle
class="spinner"
cx="12"
cy="12"
r="8"
stroke="${strokeColor}"
stroke-width="4"
stroke-linecap="round"
stroke-dasharray="${2 * Math.PI * 8 * progress} ${2 *
Math.PI *
8 *
(1 - progress)}"
/>
</svg>`;
export const LightLoadingIcon = LoadingIcon('black');
export const DarkLoadingIcon = LoadingIcon('white');
export const getLoadingIconWith = (theme: ColorScheme = ColorScheme.Light) =>
theme === ColorScheme.Light ? LightLoadingIcon : DarkLoadingIcon;

View File

@@ -840,28 +840,6 @@ export const EmbedCardDarkCubeIcon = html`
</svg>
`;
export const ReloadIcon = html`<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_6505_24239)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.625 6C1.625 3.58375 3.58375 1.625 6 1.625C7.12028 1.625 8.14299 2.04656 8.91676 2.7391L8.91796 2.74017L9.625 3.37847V2C9.625 1.79289 9.79289 1.625 10 1.625C10.2071 1.625 10.375 1.79289 10.375 2V4.22222C10.375 4.42933 10.2071 4.59722 10 4.59722H7.77778C7.57067 4.59722 7.40278 4.42933 7.40278 4.22222C7.40278 4.01512 7.57067 3.84722 7.77778 3.84722H9.025L8.41657 3.29795C8.41637 3.29777 8.41617 3.29759 8.41597 3.29741C7.77447 2.7235 6.92838 2.375 6 2.375C3.99797 2.375 2.375 3.99797 2.375 6C2.375 8.00203 3.99797 9.625 6 9.625C7.72469 9.625 9.16888 8.42017 9.53518 6.80591C9.58101 6.60393 9.78189 6.47736 9.98386 6.52319C10.1858 6.56902 10.3124 6.7699 10.2666 6.97187C9.82447 8.92025 8.08257 10.375 6 10.375C3.58375 10.375 1.625 8.41625 1.625 6Z"
fill="#1E96EB"
/>
</g>
<defs>
<clipPath id="clip0_6505_24239">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>`;
export const EmbedPageIcon = icons.LinkedPageIcon({
width: '16',
height: '16',

View File

@@ -92,7 +92,7 @@ export class Slider extends WithDisposable(LitElement) {
const dispose = on(this, 'pointermove', this._onPointerMove);
this._disposables.add(once(this, 'pointerup', dispose));
this._disposables.add(once(this, 'pointerout', dispose));
this._disposables.add(once(this, 'pointerleave', dispose));
};
private readonly _onPointerMove = (e: PointerEvent) => {

View File

@@ -2,6 +2,11 @@ import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit';
export const styles = css`
:host {
display: block;
touch-action: none;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;

View File

@@ -70,3 +70,19 @@ export const dividerV = css({
backgroundColor: 'var(--affine-divider-color)',
margin: '0 8px',
});
export const dv = {
p2,
p4,
p8,
hover,
icon16,
icon20,
border,
round4,
round8,
color2,
shadow2,
dividerH,
dividerV,
};

View File

@@ -2,42 +2,43 @@ import type {
DatabaseAllEvents,
EventTraceFn,
} from '@blocksuite/affine-shared/services';
import type { DisposableMember } from '@blocksuite/global/disposable';
import { IS_MOBILE } from '@blocksuite/global/env';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import {
type Clipboard,
type EventName,
ShadowlessElement,
type UIEventHandler,
} from '@blocksuite/std';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { css, unsafeCSS } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { dataViewCommonStyle } from './common/css-variable.js';
import type { DataViewSelection, DataViewSelectionState } from './types.js';
import type { DataSource } from './data-source/index.js';
import type { DataViewSelection } from './types.js';
import { cacheComputed } from './utils/cache.js';
import { renderUniLit } from './utils/uni-component/index.js';
import type { DataViewInstance, DataViewProps } from './view/types.js';
import type { DataViewUILogicBase } from './view/data-view-base.js';
import type { SingleView } from './view-manager/single-view.js';
import type { DataViewWidget } from './widget/index.js';
type ViewProps = {
view: SingleView;
selection$: ReadonlySignal<DataViewSelectionState>;
setSelection: (selection?: DataViewSelectionState) => void;
bindHotkey: DataViewProps['bindHotkey'];
handleEvent: DataViewProps['handleEvent'];
};
export type DataViewRendererConfig = Pick<
DataViewProps,
| 'bindHotkey'
| 'handleEvent'
| 'virtualPadding$'
| 'clipboard'
| 'dataSource'
| 'headerWidget'
| 'onDrag'
| 'notification'
> & {
export type DataViewRendererConfig = {
clipboard: Clipboard;
onDrag?: (evt: MouseEvent, id: string) => () => void;
notification: {
toast: (message: string) => void;
};
virtualPadding$: ReadonlySignal<number>;
headerWidget: DataViewWidget | undefined;
handleEvent: (name: EventName, handler: UIEventHandler) => DisposableMember;
bindHotkey: (hotkeys: Record<string, UIEventHandler>) => DisposableMember;
dataSource: DataSource;
selection$: ReadonlySignal<DataViewSelection | undefined>;
setSelection: (selection: DataViewSelection | undefined) => void;
eventTrace: EventTraceFn<DatabaseAllEvents>;
@@ -52,7 +53,104 @@ export type DataViewRendererConfig = Pick<
};
};
export class DataViewRenderer extends SignalWatcher(
export class DataViewRootUILogic {
private get dataSource() {
return this.config.dataSource;
}
private get viewManager() {
return this.dataSource.viewManager;
}
private createDataViewUILogic(viewId: string): DataViewUILogicBase {
const view = this.viewManager.viewGet(viewId);
if (!view) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.DatabaseBlockError,
`View ${viewId} not found`
);
}
const pcLogic = view.meta.renderer.pcLogic;
const mobileLogic = view.meta.renderer.mobileLogic;
const logic = (IS_MOBILE ? mobileLogic : pcLogic) ?? pcLogic;
return new (logic(view))(this, view);
}
private readonly views$ = cacheComputed(this.viewManager.views$, viewId =>
this.createDataViewUILogic(viewId)
);
private readonly viewsMap$ = computed(() => {
return Object.fromEntries(
this.views$.list.value.map(logic => [logic.view.id, logic])
);
});
private readonly _uiRef = signal<DataViewRootUI>();
get selection$() {
return this.config.selection$;
}
setSelection(selection?: DataViewSelection) {
this.config.setSelection(selection);
}
constructor(public readonly config: DataViewRendererConfig) {}
get dataViewRenderer() {
return this._uiRef.value;
}
readonly currentViewId$ = computed(() => {
return this.dataSource.viewManager.currentViewId$.value;
});
readonly currentView$ = computed(() => {
const currentViewId = this.currentViewId$.value;
if (!currentViewId) {
return;
}
return this.viewsMap$.value[currentViewId];
});
focusFirstCell = () => {
this.currentView$.value?.focusFirstCell();
};
openDetailPanel = (ops: {
view: SingleView;
rowId: string;
onClose?: () => void;
}) => {
const openDetailPanel = this.config.detailPanelConfig.openDetailPanel;
const target = this.dataViewRenderer;
if (openDetailPanel && target) {
openDetailPanel(target, {
view: ops.view,
rowId: ops.rowId,
})
.catch(console.error)
.finally(ops.onClose);
}
};
setupViewChangeListener() {
let preId: string | undefined = undefined;
return this.currentViewId$.subscribe(current => {
if (current !== preId) {
this.config.setSelection(undefined);
}
preId = current;
});
}
render() {
return html` <affine-data-view-renderer
${ref(this._uiRef)}
.logic="${this}"
></affine-data-view-renderer>`;
}
}
export class DataViewRootUI extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
@@ -63,63 +161,14 @@ export class DataViewRenderer extends SignalWatcher(
}
`;
private readonly _view = signal<DataViewInstance>();
@property({ attribute: false })
accessor config!: DataViewRendererConfig;
accessor logic!: DataViewRootUILogic;
private readonly currentViewId$ = computed(() => {
return this.config.dataSource.viewManager.currentViewId$.value;
});
viewMap$ = computed(() => {
const manager = this.config.dataSource.viewManager;
return Object.fromEntries(
manager.views$.value.map(view => [view, manager.viewGet(view)])
);
});
currentViewConfig$ = computed<ViewProps | undefined>(() => {
const currentViewId = this.currentViewId$.value;
if (!currentViewId) {
return;
}
const view = this.viewMap$.value[currentViewId];
if (!view) {
return;
}
return {
view: view,
selection$: computed(() => {
const selection$ = this.config.selection$;
if (selection$.value?.viewId === currentViewId) {
return selection$.value;
}
return;
}),
setSelection: selection => {
this.config.setSelection(selection);
},
handleEvent: (name, handler) =>
this.config.handleEvent(name, context => {
return handler(context);
}),
bindHotkey: hotkeys =>
this.config.bindHotkey(
Object.fromEntries(
Object.entries(hotkeys).map(([key, fn]) => [
key,
ctx => {
return fn(ctx);
},
])
)
),
};
});
@state()
accessor currentView: string | undefined = undefined;
focusFirstCell = () => {
this.view?.focusFirstCell();
this.logic.focusFirstCell();
};
openDetailPanel = (ops: {
@@ -127,72 +176,12 @@ export class DataViewRenderer extends SignalWatcher(
rowId: string;
onClose?: () => void;
}) => {
const openDetailPanel = this.config.detailPanelConfig.openDetailPanel;
if (openDetailPanel) {
openDetailPanel(this, {
view: ops.view,
rowId: ops.rowId,
})
.catch(console.error)
.finally(ops.onClose);
}
this.logic.openDetailPanel(ops);
};
get view() {
return this._view.value;
}
private renderView(viewData?: ViewProps) {
if (!viewData) {
return;
}
const props: DataViewProps = {
dataViewEle: this,
headerWidget: this.config.headerWidget,
onDrag: this.config.onDrag,
dataSource: this.config.dataSource,
virtualPadding$: this.config.virtualPadding$,
clipboard: this.config.clipboard,
notification: this.config.notification,
view: viewData.view,
selection$: viewData.selection$,
setSelection: viewData.setSelection,
bindHotkey: viewData.bindHotkey,
handleEvent: viewData.handleEvent,
eventTrace: (key, params) => {
this.config.eventTrace(key, {
...(params as DatabaseAllEvents[typeof key]),
viewId: viewData.view.id,
viewType: viewData.view.type,
});
},
};
const renderer = viewData.view.meta.renderer;
const view =
(IS_MOBILE ? renderer.mobileView : renderer.view) ?? renderer.view;
return keyed(
viewData.view.id,
renderUniLit(
view,
{ props },
{
ref: this._view,
}
)
);
}
override connectedCallback() {
super.connectedCallback();
let preId: string | undefined = undefined;
this.disposables.add(
this.currentViewId$.subscribe(current => {
if (current !== preId) {
this.config.setSelection(undefined);
}
preId = current;
})
);
this.disposables.add(this.logic.setupViewChangeListener());
}
override render() {
@@ -201,34 +190,22 @@ export class DataViewRenderer extends SignalWatcher(
'data-view-root': true,
'prevent-reference-popup': true,
});
const currentView = this.logic.currentView$.value;
if (!currentView) {
return;
}
return html`
<div style="display: contents" class="${containerClass}">
${this.renderView(this.currentViewConfig$.value)}
${renderUniLit(currentView.renderer, {
logic: currentView,
})}
</div>
`;
}
@state()
accessor currentView: string | undefined = undefined;
}
declare global {
interface HTMLElementTagNameMap {
'affine-data-view-renderer': DataViewRenderer;
}
}
export class DataView {
private readonly _ref = createRef<DataViewRenderer>();
get expose() {
return this._ref.value?.view;
}
render(props: DataViewRendererConfig) {
return html` <affine-data-view-renderer
${ref(this._ref)}
.config="${props}"
></affine-data-view-renderer>`;
'affine-data-view-renderer': DataViewRootUI;
}
}

View File

@@ -2,7 +2,7 @@ import { DataViewPropertiesSettingView } from './common/properties.js';
import { Button } from './component/button/button.js';
import { Overflow } from './component/overflow/overflow.js';
import { MultiTagSelect, MultiTagView } from './component/tags/index.js';
import { DataViewRenderer } from './data-view.js';
import { DataViewRootUI } from './data-view.js';
import { RecordDetail } from './detail/detail.js';
import { RecordField } from './detail/field.js';
import { VariableRefView } from './expression/ref/ref-view.js';
@@ -15,7 +15,7 @@ import { AffineLitIcon, UniAnyRender, UniLit } from './index.js';
import { AnyRender } from './utils/uni-component/render-template.js';
export function coreEffects() {
customElements.define('affine-data-view-renderer', DataViewRenderer);
customElements.define('affine-data-view-renderer', DataViewRootUI);
customElements.define('any-render', AnyRender);
customElements.define(
'data-view-properties-setting',

View File

@@ -1,7 +1,7 @@
export * from './common/index.js';
export * from './component/index.js';
export { DataSourceBase } from './data-source/base.js';
export { DataView } from './data-view.js';
export { DataViewRootUILogic } from './data-view.js';
export * from './filter/index.js';
export * from './group-by';
export * from './logical/index.js';

View File

@@ -183,7 +183,6 @@ export class TypeSystem {
// eslint-disable-next-line sonarjs/no-collapsible-if
if (realArg != null) {
if (!this._unify(newCtx, realArg, arg)) {
console.log('arg', realArg, arg);
return;
}
}

View File

@@ -0,0 +1,32 @@
import { computed, type ReadonlySignal } from '@preact/signals-core';
export const cacheComputed = <T>(
ids: ReadonlySignal<string[]>,
create: (id: string) => T
) => {
const cache = new Map<string, T>();
const getOrCreate = (id: string): T => {
if (cache.has(id)) {
return cache.get(id)!;
}
const value = create(id);
if (value) {
cache.set(id, value);
}
return value;
};
return {
getOrCreate,
list: computed<T[]>(() => {
const list = ids.value;
const keys = new Set(cache.keys());
for (const [cachedId] of cache) {
keys.delete(cachedId);
}
for (const id of keys) {
cache.delete(id);
}
return list.map(id => getOrCreate(id));
}),
};
};

View File

@@ -1,2 +1,3 @@
export * from './lazy.js';
export * from './uni-component/index.js';
export * from './uni-icon.js';

View File

@@ -0,0 +1,11 @@
export const lazy = <T>(fn: () => T): { value: T } => {
let data: { value: T } | undefined;
return {
get value() {
if (!data) {
data = { value: fn() };
}
return data.value;
},
};
};

View File

@@ -1,17 +1,106 @@
import type {
DatabaseAllEvents,
DatabaseAllViewEvents,
EventTraceFn,
} from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { DisposableMember } from '@blocksuite/global/disposable';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import {
type EventName,
ShadowlessElement,
type UIEventHandler,
} from '@blocksuite/std';
import { computed } from '@preact/signals-core';
import { property } from 'lit/decorators.js';
import type { DataViewRootUILogic } from '../data-view.js';
import type { DataViewSelection } from '../types.js';
import type { SingleView } from '../view-manager/single-view.js';
import type { DataViewWidget } from '../widget/index.js';
import type { DataViewInstance, DataViewProps } from './types.js';
export abstract class DataViewBase<
T extends SingleView = SingleView,
Selection extends DataViewSelection = DataViewSelection,
> extends SignalWatcher(WithDisposable(ShadowlessElement)) {
abstract expose: DataViewInstance;
@property({ attribute: false })
accessor props!: DataViewProps<T, Selection>;
accessor props!: DataViewProps<Selection>;
}
export abstract class DataViewUIBase<
Logic extends DataViewUILogicBase = DataViewUILogicBase,
> extends SignalWatcher(WithDisposable(ShadowlessElement)) {
@property({ attribute: false })
accessor logic!: Logic;
}
export abstract class DataViewUILogicBase<
T extends SingleView = SingleView,
Selection extends DataViewSelection = DataViewSelection,
> {
constructor(
public readonly root: DataViewRootUILogic,
public readonly view: T
) {}
get headerWidget(): DataViewWidget | undefined {
return this.root.config.headerWidget;
}
bindHotkey(hotkeys: Record<string, UIEventHandler>): DisposableMember {
return this.root.config.bindHotkey(
Object.fromEntries(
Object.entries(hotkeys).map(([key, fn]) => [
key,
ctx => {
return fn(ctx);
},
])
)
);
}
handleEvent(name: EventName, handler: UIEventHandler): DisposableMember {
return this.root.config.handleEvent(name, context => {
return handler(context);
});
}
setSelection(selection?: Selection): void {
this.root.setSelection(selection);
}
selection$ = computed<Selection | undefined>(() => {
const selection$ = this.root.selection$;
if (selection$.value?.viewId === this.view.id) {
return selection$.value as Selection | undefined;
}
return;
});
eventTrace: EventTraceFn<DatabaseAllViewEvents> = (key, params) => {
this.root.config.eventTrace(key, {
...(params as DatabaseAllEvents[typeof key]),
viewId: this.view.id,
viewType: this.view.type,
});
};
abstract clearSelection: () => void;
abstract addRow: (position: InsertToPosition) => string | undefined;
abstract focusFirstCell: () => void;
abstract showIndicator: (evt: MouseEvent) => boolean;
abstract hideIndicator: () => void;
abstract moveTo: (id: string, evt: MouseEvent) => void;
abstract renderer: UniComponent<{
logic: DataViewUILogicBase<T, Selection>;
}>;
}
type Constructor<T extends abstract new (...args: any) => any> = new (
...args: ConstructorParameters<T>
) => InstanceType<T>;
export type DataViewUILogicBaseConstructor = Constructor<
typeof DataViewUILogicBase
>;

View File

@@ -2,6 +2,7 @@ import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { SingleView } from '../view-manager/single-view.js';
import type { ViewManager } from '../view-manager/view-manager.js';
import type { DataViewUILogicBaseConstructor } from './data-view-base.js';
import type { DataViewInstance, DataViewProps } from './types.js';
export type BasicViewDataType<
@@ -48,9 +49,10 @@ type DataViewComponent = UniComponent<
>;
export interface DataViewRendererConfig {
view: DataViewComponent;
mobileView?: DataViewComponent;
icon: UniComponent;
pcLogic: (view: SingleView) => DataViewUILogicBaseConstructor;
mobileLogic?: (view: SingleView) => DataViewUILogicBaseConstructor;
}
export type ViewMeta<

View File

@@ -1,3 +1,4 @@
export * from './convert.js';
export * from './data-view.js';
export * from './data-view-base.js';
export * from './types.js';

View File

@@ -4,44 +4,21 @@ import type {
} from '@blocksuite/affine-shared/services';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { Disposable } from '@blocksuite/global/disposable';
import type { Clipboard, EventName, UIEventHandler } from '@blocksuite/std';
import type { EventName, UIEventHandler } from '@blocksuite/std';
import type { ReadonlySignal } from '@preact/signals-core';
import type { DataSource } from '../common/index.js';
import type { DataViewRenderer } from '../data-view.js';
import type { DataViewSelection } from '../types.js';
import type { SingleView } from '../view-manager/index.js';
import type { DataViewWidget } from '../widget/index.js';
export interface DataViewProps<
T extends SingleView = SingleView,
Selection extends DataViewSelection = DataViewSelection,
> {
dataViewEle: DataViewRenderer;
headerWidget?: DataViewWidget;
view: T;
dataSource: DataSource;
bindHotkey: (hotkeys: Record<string, UIEventHandler>) => Disposable;
handleEvent: (name: EventName, handler: UIEventHandler) => Disposable;
setSelection: (selection?: Selection) => void;
selection$: ReadonlySignal<Selection | undefined>;
virtualPadding$: ReadonlySignal<number>;
onDrag?: (evt: MouseEvent, id: string) => () => void;
clipboard: Clipboard;
notification: {
toast: (message: string) => void;
};
eventTrace: EventTraceFn<DatabaseAllViewEvents>;
}

View File

@@ -1,8 +1,10 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { DataViewInstance } from '../view/types.js';
import type { DataViewUILogicBase } from '../view/data-view-base.js';
export type DataViewWidgetProps = {
dataViewInstance: DataViewInstance;
export type DataViewWidgetProps<
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
> = {
dataViewLogic: ViewLogic;
};
export type DataViewWidget = UniComponent<DataViewWidgetProps>;

View File

@@ -2,30 +2,27 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { property } from 'lit/decorators.js';
import type { DataViewInstance } from '../view/types.js';
import type { SingleView } from '../view-manager/index.js';
import type { DataViewUILogicBase } from '../view/data-view-base.js';
import type { DataViewWidgetProps } from './types.js';
export class WidgetBase<View extends SingleView = SingleView>
export class WidgetBase<
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
>
extends SignalWatcher(WithDisposable(ShadowlessElement))
implements DataViewWidgetProps
implements DataViewWidgetProps<ViewLogic>
{
get dataSource() {
return this.view.manager.dataSource;
return this.viewManager.dataSource;
}
get view() {
return this.dataViewInstance.view;
return this.dataViewLogic.view;
}
get viewManager() {
return this.view.manager;
}
get viewMethods() {
return this.dataViewInstance;
}
@property({ attribute: false })
accessor dataViewInstance!: DataViewInstance<View>;
accessor dataViewLogic!: ViewLogic;
}

View File

@@ -1,48 +1,7 @@
import { DataViewKanban, TableViewSelector } from './index.js';
import { MobileKanbanCard } from './kanban/mobile/card.js';
import { MobileKanbanCell } from './kanban/mobile/cell.js';
import { MobileKanbanGroup } from './kanban/mobile/group.js';
import { MobileDataViewKanban } from './kanban/mobile/kanban-view.js';
import { KanbanCard } from './kanban/pc/card.js';
import { KanbanCell } from './kanban/pc/cell.js';
import { KanbanGroup } from './kanban/pc/group.js';
import { KanbanHeader } from './kanban/pc/header.js';
import { MobileTableCell } from './table/mobile/cell.js';
import { MobileTableColumnHeader } from './table/mobile/column-header.js';
import { MobileTableGroup } from './table/mobile/group.js';
import { MobileTableHeader } from './table/mobile/header.js';
import { MobileTableRow } from './table/mobile/row.js';
import { MobileDataViewTable } from './table/mobile/table-view.js';
import { pcEffects } from './table/pc/effect.js';
import { pcVirtualEffects } from './table/pc-virtual/effect.js';
import { DataBaseColumnStats } from './table/stats/column-stats-bar.js';
import { DatabaseColumnStatsCell } from './table/stats/column-stats-column.js';
import { kanbanEffects } from './kanban/effect.js';
import { tableEffects } from './table/effect.js';
export function viewPresetsEffects() {
customElements.define('affine-data-view-kanban-card', KanbanCard);
customElements.define('mobile-kanban-card', MobileKanbanCard);
customElements.define('affine-data-view-kanban-cell', KanbanCell);
customElements.define('mobile-kanban-cell', MobileKanbanCell);
customElements.define('affine-data-view-kanban-group', KanbanGroup);
customElements.define('mobile-kanban-group', MobileKanbanGroup);
customElements.define('affine-data-view-kanban', DataViewKanban);
customElements.define('mobile-data-view-kanban', MobileDataViewKanban);
customElements.define('affine-data-view-kanban-header', KanbanHeader);
customElements.define('mobile-table-cell', MobileTableCell);
customElements.define('mobile-table-group', MobileTableGroup);
customElements.define('mobile-data-view-table', MobileDataViewTable);
customElements.define('mobile-table-header', MobileTableHeader);
customElements.define('mobile-table-column-header', MobileTableColumnHeader);
customElements.define('mobile-table-row', MobileTableRow);
customElements.define('affine-database-column-stats', DataBaseColumnStats);
customElements.define(
'affine-database-column-stats-cell',
DatabaseColumnStatsCell
);
customElements.define('affine-database-table-selector', TableViewSelector);
pcEffects();
pcVirtualEffects();
kanbanEffects();
tableEffects();
}

View File

@@ -0,0 +1,7 @@
import { mobileEffects } from './mobile/effect.js';
import { pcEffects } from './pc/effect.js';
export function kanbanEffects() {
pcEffects();
mobileEffects();
}

View File

@@ -1,5 +1,4 @@
export * from './define.js';
export * from './kanban-view-manager.js';
export * from './pc/kanban-view.js';
export * from './renderer.js';
export * from './selection.js';

View File

@@ -10,8 +10,8 @@ import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanColumn } from '../kanban-view-manager.js';
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
import { popCardMenu } from './menu.js';
const styles = css`
@@ -94,7 +94,7 @@ export class MobileKanbanCard extends SignalWatcher(
private readonly clickCenterPeek = (e: MouseEvent) => {
e.stopPropagation();
this.dataViewEle.openDetailPanel({
this.kanbanViewLogic.root.openDetailPanel({
view: this.view,
rowId: this.cardId,
});
@@ -104,10 +104,9 @@ export class MobileKanbanCard extends SignalWatcher(
e.stopPropagation();
popCardMenu(
popupTargetFromElement(e.currentTarget as HTMLElement),
this.view,
this.groupKey,
this.cardId,
this.dataViewEle
this.kanbanViewLogic
);
};
@@ -126,10 +125,10 @@ export class MobileKanbanCard extends SignalWatcher(
return html` <mobile-kanban-cell
.contentOnly="${false}"
data-column-id="${column.id}"
.view="${this.view}"
.groupKey="${this.groupKey}"
.column="${column}"
.cardId="${this.cardId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></mobile-kanban-cell>`;
}
)}
@@ -184,10 +183,10 @@ export class MobileKanbanCard extends SignalWatcher(
<mobile-kanban-cell
.contentOnly="${true}"
data-column-id="${title.id}"
.view="${this.view}"
.groupKey="${this.groupKey}"
.column="${title}"
.cardId="${this.cardId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></mobile-kanban-cell>
</div>`;
}
@@ -205,9 +204,6 @@ export class MobileKanbanCard extends SignalWatcher(
@property({ attribute: false })
accessor cardId!: string;
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor groupKey!: string;
@@ -215,7 +211,11 @@ export class MobileKanbanCard extends SignalWatcher(
accessor isFocus = false;
@property({ attribute: false })
accessor view!: KanbanSingleView;
accessor kanbanViewLogic!: MobileKanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
}
declare global {

View File

@@ -14,7 +14,7 @@ import type {
} from '../../../core/property/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import type { Property } from '../../../core/view-manager/property.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css`
mobile-kanban-cell {
@@ -53,7 +53,7 @@ export class MobileKanbanCell extends SignalWatcher(
private readonly _cell = signal<DataViewCellLifeCycle>();
isSelectionEditing$ = computed(() => {
const selection = this.kanban?.props.selection$.value;
const selection = this.kanbanViewLogic.selection$.value;
if (selection?.selectionType !== 'cell') {
return false;
}
@@ -73,8 +73,8 @@ export class MobileKanbanCell extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const setSelection = this.kanban?.props.setSelection;
const viewId = this.kanban?.props.view.id;
const setSelection = this.kanbanViewLogic.setSelection;
const viewId = this.kanbanViewLogic.view.id;
if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) {
return;
@@ -95,14 +95,6 @@ export class MobileKanbanCell extends SignalWatcher(
return this._cell.value;
}
get kanban() {
return this.closest('mobile-data-view-kanban');
}
get selection() {
return this.closest('mobile-data-view-kanban')?.props.selection$.value;
}
override connectedCallback() {
super.connectedCallback();
if (this.column.readonly$.value) return;
@@ -172,7 +164,11 @@ export class MobileKanbanCell extends SignalWatcher(
isEditing$ = signal(false);
@property({ attribute: false })
accessor view!: KanbanSingleView;
accessor kanbanViewLogic!: MobileKanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
}
declare global {

View File

@@ -0,0 +1,11 @@
import { MobileKanbanCard } from './card.js';
import { MobileKanbanCell } from './cell.js';
import { MobileKanbanGroup } from './group.js';
import { MobileKanbanViewUI } from './kanban-view-ui-logic.js';
export function mobileEffects() {
customElements.define('mobile-kanban-card', MobileKanbanCard);
customElements.define('mobile-kanban-cell', MobileKanbanCell);
customElements.define('mobile-kanban-group', MobileKanbanGroup);
customElements.define('mobile-data-view-kanban-ui', MobileKanbanViewUI);
}

View File

@@ -11,11 +11,10 @@ import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js';
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css`
mobile-kanban-group {
@@ -112,9 +111,8 @@ export class MobileKanbanGroup extends SignalWatcher(
<mobile-kanban-card
data-card-id="${row.rowId}"
.groupKey="${this.group.key}"
.dataViewEle="${this.dataViewEle}"
.view="${this.view}"
.cardId="${row.rowId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></mobile-kanban-card>
`;
}
@@ -133,14 +131,15 @@ export class MobileKanbanGroup extends SignalWatcher(
`;
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group!: Group;
@property({ attribute: false })
accessor view!: KanbanSingleView;
accessor kanbanViewLogic!: MobileKanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
}
declare global {

View File

@@ -0,0 +1,168 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { css } from '@emotion/css';
import { signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection';
const mobileKanbanViewWrapper = css({
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
});
const mobileKanbanGroups = css({
position: 'relative',
zIndex: 1,
display: 'flex',
gap: '20px',
paddingBottom: '4px',
overflowX: 'scroll',
overflowY: 'hidden',
});
const mobileAddGroup = css({
height: '32px',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
padding: '4px',
borderRadius: '4px',
fontSize: '16px',
color: `var(${unsafeCSSVarV2('icon/primary')})`,
});
export class MobileKanbanViewUILogic extends DataViewUILogicBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
ui$ = signal<MobileKanbanViewUI | undefined>(undefined);
private get readonly() {
return this.view.readonly$.value;
}
clearSelection = () => {};
addRow = (position: InsertToPosition) => {
if (this.readonly) return;
return this.view.rowAdd(position);
};
focusFirstCell = () => {};
showIndicator = (_evt: MouseEvent) => {
return false;
};
hideIndicator = () => {};
moveTo = () => {};
get groupManager() {
return this.view.groupTrait;
}
renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = this.groupManager.property$.value;
if (column) {
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
}),
],
},
});
};
return html` <div class="${mobileAddGroup}" @click="${add}">
${AddCursorIcon()}
</div>`;
};
renderer = createUniComponentFromWebComponent(MobileKanbanViewUI);
}
export class MobileKanbanViewUI extends DataViewUIBase<MobileKanbanViewUILogic> {
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.classList.add(mobileKanbanViewWrapper);
}
override render(): TemplateResult {
const groups = this.logic.groupManager.groupsDataList$.value;
if (!groups) {
return html``;
}
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.logic.headerWidget, {
dataViewLogic: this.logic,
})}
<div class="${mobileKanbanGroups}" style="${wrapperStyle}">
${repeat(
groups,
group => group.key,
group => {
return html` <mobile-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.kanbanViewLogic="${this.logic}"
.group="${group}"
></mobile-kanban-group>`;
}
)}
${this.logic.renderAddGroup()}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-kanban-ui': MobileKanbanViewUI;
}
}

View File

@@ -1,149 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { css } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection';
const styles = css`
mobile-data-view-kanban {
user-select: none;
display: flex;
flex-direction: column;
}
.mobile-kanban-groups {
position: relative;
z-index: 1;
display: flex;
gap: 20px;
padding-bottom: 4px;
overflow-x: scroll;
overflow-y: hidden;
}
.mobile-add-group {
height: 32px;
flex-shrink: 0;
display: flex;
align-items: center;
padding: 4px;
border-radius: 4px;
font-size: 16px;
color: ${unsafeCSSVarV2('icon/primary')};
}
`;
export class MobileDataViewKanban extends DataViewBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
static override styles = styles;
renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = this.groupManager.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div class="mobile-add-group" @click="${add}">
${AddCursorIcon()}
</div>`;
};
get expose(): DataViewInstance {
return {
clearSelection: () => {},
focusFirstCell: () => {},
getSelection: () => {
return this.props.selection$.value;
},
hideIndicator: () => {},
moveTo: () => {},
showIndicator: () => {
return false;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
get groupManager() {
return this.props.view.groupTrait;
}
override render() {
const groups = this.groupManager.groupsDataList$.value;
if (!groups) {
return html``;
}
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div class="mobile-kanban-groups" style="${wrapperStyle}">
${repeat(
groups,
group => group.key,
group => {
return html` <mobile-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.group="${group}"
></mobile-kanban-group>`;
}
)}
${this.renderAddGroup()}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-kanban': MobileDataViewKanban;
}
}

View File

@@ -12,18 +12,16 @@ import {
} from '@blocksuite/icons/lit';
import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { groupTraitKey } from '../../../core/group-by/trait.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
export const popCardMenu = (
ele: PopupTarget,
view: KanbanSingleView,
groupKey: string,
cardId: string,
dataViewEle: DataViewRenderer
kanbanViewLogic: MobileKanbanViewUILogic
) => {
const groupTrait = view.traitGet(groupTraitKey);
const groupTrait = kanbanViewLogic.view.traitGet(groupTraitKey);
if (!groupTrait) {
return;
}
@@ -34,8 +32,8 @@ export const popCardMenu = (
name: 'Expand Card',
prefix: ExpandFullIcon(),
select: () => {
dataViewEle.openDetailPanel({
view: view,
kanbanViewLogic.root.openDetailPanel({
view: kanbanViewLogic.view,
rowId: cardId,
});
},
@@ -81,7 +79,10 @@ export const popCardMenu = (
${MoveLeftIcon()}
</div>`,
select: () => {
view.addCard({ before: true, id: cardId }, groupKey);
kanbanViewLogic.view.addCard(
{ before: true, id: cardId },
groupKey
);
},
}),
menu.action({
@@ -92,7 +93,10 @@ export const popCardMenu = (
${MoveRightIcon()}
</div>`,
select: () => {
view.addCard({ before: false, id: cardId }, groupKey);
kanbanViewLogic.view.addCard(
{ before: false, id: cardId },
groupKey
);
},
}),
],
@@ -106,7 +110,7 @@ export const popCardMenu = (
},
prefix: DeleteIcon(),
select: () => {
view.rowsDelete([cardId]);
kanbanViewLogic.view.rowsDelete([cardId]);
},
}),
],

View File

@@ -2,15 +2,16 @@ import { popupTargetFromElement } from '@blocksuite/affine-components/context-me
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { CenterPeekIcon, MoreHorizontalIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, unsafeCSS } from 'lit';
import { property, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanColumn } from '../kanban-view-manager.js';
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
import { openDetail, popCardMenu } from './menu.js';
const styles = css`
@@ -130,7 +131,7 @@ export class KanbanCard extends SignalWatcher(
e.stopPropagation();
const selection = this.getSelection();
if (selection) {
openDetail(this.dataViewEle, this.cardId, selection);
openDetail(this.kanbanViewLogic, this.cardId, selection);
}
};
@@ -149,7 +150,7 @@ export class KanbanCard extends SignalWatcher(
],
};
popCardMenu(
this.dataViewEle,
this.kanbanViewLogic,
popupTargetFromElement(ele),
this.cardId,
selection
@@ -174,7 +175,7 @@ export class KanbanCard extends SignalWatcher(
const target = e.target as HTMLElement;
const ref = target.closest('affine-data-view-kanban-cell') ?? this;
popCardMenu(
this.dataViewEle,
this.kanbanViewLogic,
popupTargetFromElement(ref),
this.cardId,
selection
@@ -183,7 +184,7 @@ export class KanbanCard extends SignalWatcher(
};
private getSelection() {
return this.closest('affine-data-view-kanban')?.selectionController;
return this.kanbanViewLogic.selectionController;
}
private renderBody(columns: KanbanColumn[]) {
@@ -201,10 +202,10 @@ export class KanbanCard extends SignalWatcher(
return html` <affine-data-view-kanban-cell
.contentOnly="${false}"
data-column-id="${column.id}"
.view="${this.view}"
.groupKey="${this.groupKey}"
.column="${column}"
.cardId="${this.cardId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></affine-data-view-kanban-cell>`;
}
)}
@@ -259,7 +260,7 @@ export class KanbanCard extends SignalWatcher(
<affine-data-view-kanban-cell
.contentOnly="${true}"
data-column-id="${title.id}"
.view="${this.view}"
.kanbanViewLogic="${this.kanbanViewLogic}"
.groupKey="${this.groupKey}"
.column="${title}"
.cardId="${this.cardId}"
@@ -288,7 +289,7 @@ export class KanbanCard extends SignalWatcher(
if (selection) {
selection.selection = undefined;
}
this.dataViewEle.openDetailPanel({
this.kanbanViewLogic.root.openDetailPanel({
view: this.view,
rowId: this.cardId,
onClose: () => {
@@ -304,7 +305,7 @@ export class KanbanCard extends SignalWatcher(
const columns = this.view.properties$.value.filter(
v => !this.view.isInHeader(v.id)
);
this.style.border = this.isFocus
this.style.border = this.isFocus$.value
? '1px solid var(--affine-primary-color)'
: '';
return html`
@@ -316,17 +317,17 @@ export class KanbanCard extends SignalWatcher(
@property({ attribute: false })
accessor cardId!: string;
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor groupKey!: string;
@state()
accessor isFocus = false;
isFocus$ = signal(false);
@property({ attribute: false })
accessor view!: KanbanSingleView;
accessor kanbanViewLogic!: KanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
}
declare global {

View File

@@ -4,7 +4,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import type {
@@ -13,8 +13,8 @@ import type {
} from '../../../core/property/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import type { Property } from '../../../core/view-manager/property.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelection } from '../selection';
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css`
affine-data-view-kanban-cell {
@@ -62,10 +62,7 @@ export class KanbanCell extends SignalWatcher(
private readonly _cell = signal<DataViewCellLifeCycle>();
selectCurrentCell = (editing: boolean) => {
const selectionView = this.closest(
'affine-data-view-kanban'
)?.selectionController;
if (!selectionView) return;
const selectionView = this.kanbanViewLogic.selectionController;
if (selectionView) {
const selection = selectionView.selection;
if (selection && this.isSelected(selection) && editing) {
@@ -93,7 +90,7 @@ export class KanbanCell extends SignalWatcher(
}
get selection() {
return this.closest('affine-data-view-kanban')?.selectionController;
return this.kanbanViewLogic.selectionController;
}
override connectedCallback() {
@@ -103,9 +100,7 @@ export class KanbanCell extends SignalWatcher(
return;
}
e.stopPropagation();
const selectionElement = this.closest(
'affine-data-view-kanban'
)?.selectionController;
const selectionElement = this.kanbanViewLogic.selectionController;
if (!selectionElement) return;
if (e.shiftKey) return;
@@ -138,7 +133,7 @@ export class KanbanCell extends SignalWatcher(
const { view } = renderer;
this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing$.value}`;
this.style.border = this.isFocus
this.style.border = this.isFocus$.value
? '1px solid var(--affine-primary-color)'
: '';
this.style.boxShadow = this.isEditing$.value
@@ -173,11 +168,14 @@ export class KanbanCell extends SignalWatcher(
@property({ attribute: false })
accessor groupKey!: string;
@state()
accessor isFocus = false;
isFocus$ = signal(false);
@property({ attribute: false })
accessor view!: KanbanSingleView;
accessor kanbanViewLogic!: KanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
}
declare global {

View File

@@ -2,7 +2,7 @@ import type { UIEventStateContext } from '@blocksuite/std';
import type { ReactiveController } from 'lit';
import type { KanbanViewSelectionWithType } from '../../selection';
import type { DataViewKanban } from '../kanban-view.js';
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanClipboardController implements ReactiveController {
private readonly _onCopy = (
@@ -19,31 +19,35 @@ export class KanbanClipboardController implements ReactiveController {
};
private get readonly() {
return this.host.props.view.readonly$.value;
return this.logic.view.readonly$.value;
}
constructor(public host: DataViewKanban) {
host.addController(this);
get host() {
return this.logic.ui$.value;
}
constructor(public logic: KanbanViewUILogic) {}
hostConnected() {
this.host.disposables.add(
this.host.props.handleEvent('copy', ctx => {
const kanbanSelection = this.host.selectionController.selection;
if (!kanbanSelection) return false;
if (this.host) {
this.host.disposables.add(
this.logic.handleEvent('copy', ctx => {
const kanbanSelection = this.logic.selectionController.selection;
if (!kanbanSelection) return false;
this._onCopy(ctx, kanbanSelection);
return true;
})
);
this._onCopy(ctx, kanbanSelection);
return true;
})
);
this.host.disposables.add(
this.host.props.handleEvent('paste', ctx => {
if (this.readonly) return false;
this.host.disposables.add(
this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false;
this._onPaste(ctx);
return true;
})
);
this._onPaste(ctx);
return true;
})
);
}
}
}

View File

@@ -7,10 +7,14 @@ import { autoScrollOnBoundary } from '../../../../core/utils/auto-scroll.js';
import { startDrag } from '../../../../core/utils/drag.js';
import { KanbanCard } from '../card.js';
import { KanbanGroup } from '../group.js';
import type { DataViewKanban } from '../kanban-view.js';
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanDragController implements ReactiveController {
dragStart = (ele: KanbanCard, evt: PointerEvent) => {
const host = this.host;
if (!host) {
return;
}
const eleRect = ele.getBoundingClientRect();
const offsetLeft = evt.x - eleRect.left;
const offsetTop = evt.y - eleRect.top;
@@ -36,8 +40,8 @@ export class KanbanDragController implements ReactiveController {
return;
}
preview.display(evt.x - offsetLeft, evt.y - offsetTop);
if (!Rect.fromDOM(this.host).isPointIn(Point.from(evt))) {
const callback = this.host.props.onDrag;
if (!Rect.fromDOM(host).isPointIn(Point.from(evt))) {
const callback = this.logic.root.config.onDrag;
if (callback) {
this.dropPreview.remove();
return {
@@ -47,7 +51,7 @@ export class KanbanDragController implements ReactiveController {
}
return;
}
const result = this.shooIndicator(evt, ele);
const result = this.showIndicator(evt, ele);
if (result) {
return {
type: 'self',
@@ -80,19 +84,26 @@ export class KanbanDragController implements ReactiveController {
}
},
});
const cancelScroll = autoScrollOnBoundary(
this.scrollContainer,
computed(() => {
return {
left: drag.mousePosition.value.x,
right: drag.mousePosition.value.x,
top: drag.mousePosition.value.y,
bottom: drag.mousePosition.value.y,
};
})
);
const cancelScroll =
this.scrollContainer != null
? autoScrollOnBoundary(
this.scrollContainer,
computed(() => {
return {
left: drag.mousePosition.value.x,
right: drag.mousePosition.value.x,
top: drag.mousePosition.value.y,
bottom: drag.mousePosition.value.y,
};
})
)
: () => {};
};
get host() {
return this.logic.ui$.value;
}
dropPreview = createDropPreview();
getInsertPosition = (
@@ -119,7 +130,7 @@ export class KanbanDragController implements ReactiveController {
}
};
shooIndicator = (
showIndicator = (
evt: MouseEvent,
self: KanbanCard | undefined
): { group: KanbanGroup; position: InsertToPosition } | undefined => {
@@ -133,38 +144,36 @@ export class KanbanDragController implements ReactiveController {
};
get scrollContainer() {
const scrollContainer = this.host.querySelector(
'.affine-data-view-kanban-groups'
) as HTMLElement;
const scrollContainer = this.logic.scrollContainer$.value;
return scrollContainer;
}
constructor(private readonly host: DataViewKanban) {
this.host.addController(this);
}
constructor(private readonly logic: KanbanViewUILogic) {}
hostConnected() {
if (this.host.props.view.readonly$.value) {
if (this.logic.view.readonly$.value) {
return;
}
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
const event = context.get('pointerState').raw;
const target = event.target;
if (target instanceof Element) {
const cell = target.closest('affine-data-view-kanban-cell');
if (cell?.isEditing$.value) {
return;
if (this.host) {
this.host.disposables.add(
this.logic.handleEvent('dragStart', context => {
const event = context.get('pointerState').raw;
const target = event.target;
if (target instanceof Element) {
const cell = target.closest('affine-data-view-kanban-cell');
if (cell?.isEditing$.value) {
return;
}
cell?.selectCurrentCell(false);
const card = target.closest('affine-data-view-kanban-card');
if (card) {
this.dragStart(card, event);
}
}
cell?.selectCurrentCell(false);
const card = target.closest('affine-data-view-kanban-card');
if (card) {
this.dragStart(card, event);
}
}
return true;
})
);
return true;
})
);
}
}
}
@@ -174,8 +183,8 @@ const createDragPreview = (card: KanbanCard, x: number, y: number) => {
const div = document.createElement('div');
const kanbanCard = new KanbanCard();
kanbanCard.cardId = card.cardId;
kanbanCard.view = card.view;
kanbanCard.isFocus = true;
kanbanCard.kanbanViewLogic = card.kanbanViewLogic;
kanbanCard.isFocus$.value = true;
kanbanCard.style.backgroundColor = 'var(--affine-background-primary-color)';
div.append(kanbanCard);
div.className = 'with-data-view-css-variable';

View File

@@ -1,63 +1,67 @@
import type { ReactiveController } from 'lit';
import type { DataViewKanban } from '../kanban-view.js';
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanHotkeysController implements ReactiveController {
private get hasSelection() {
return !!this.host.selectionController.selection;
return !!this.logic.selectionController.selection;
}
constructor(private readonly host: DataViewKanban) {
this.host.addController(this);
constructor(public logic: KanbanViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
this.host.disposables.add(
this.host.props.bindHotkey({
Escape: () => {
this.host.selectionController.focusOut();
return true;
},
Enter: () => {
this.host.selectionController.focusIn();
},
ArrowUp: context => {
if (!this.hasSelection) return false;
if (this.host) {
this.host.disposables.add(
this.logic.bindHotkey({
Escape: () => {
this.logic.selectionController.focusOut();
return true;
},
Enter: () => {
this.logic.selectionController.focusIn();
},
ArrowUp: context => {
if (!this.hasSelection) return false;
this.host.selectionController.focusNext('up');
context.get('keyboardState').raw.preventDefault();
return true;
},
ArrowDown: context => {
if (!this.hasSelection) return false;
this.logic.selectionController.focusNext('up');
context.get('keyboardState').raw.preventDefault();
return true;
},
ArrowDown: context => {
if (!this.hasSelection) return false;
this.host.selectionController.focusNext('down');
context.get('keyboardState').raw.preventDefault();
return true;
},
Tab: context => {
if (!this.hasSelection) return false;
this.logic.selectionController.focusNext('down');
context.get('keyboardState').raw.preventDefault();
return true;
},
Tab: context => {
if (!this.hasSelection) return false;
this.host.selectionController.focusNext('down');
context.get('keyboardState').raw.preventDefault();
return true;
},
ArrowLeft: () => {
if (!this.hasSelection) return false;
this.logic.selectionController.focusNext('down');
context.get('keyboardState').raw.preventDefault();
return true;
},
ArrowLeft: () => {
if (!this.hasSelection) return false;
this.host.selectionController.focusNext('left');
return true;
},
ArrowRight: () => {
if (!this.hasSelection) return false;
this.logic.selectionController.focusNext('left');
return true;
},
ArrowRight: () => {
if (!this.hasSelection) return false;
this.host.selectionController.focusNext('right');
return true;
},
Backspace: () => {
this.host.selectionController.deleteCard();
},
})
);
this.logic.selectionController.focusNext('right');
return true;
},
Backspace: () => {
this.logic.selectionController.deleteCard();
},
})
);
}
}
}

View File

@@ -12,7 +12,7 @@ import type {
import { KanbanCard } from '../card.js';
import { KanbanCell } from '../cell.js';
import type { KanbanGroup } from '../group.js';
import type { DataViewKanban } from '../kanban-view.js';
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanSelectionController implements ReactiveController {
private _selection?: KanbanViewSelectionWithType;
@@ -47,52 +47,62 @@ export class KanbanSelectionController implements ReactiveController {
}
set selection(data: KanbanViewSelection | undefined) {
const host = this.host;
if (!host) {
return;
}
if (!data) {
this.host.props.setSelection();
this.logic.setSelection();
return;
}
const selection: KanbanViewSelectionWithType = {
...data,
viewId: this.host.props.view.id,
viewId: this.logic.view.id,
type: 'kanban',
};
if (selection.selectionType === 'cell' && selection.isEditing) {
const container = getFocusCell(this.host, selection);
const container = getFocusCell(host, selection);
const cell = container?.cell;
const isEditing = cell
? cell.beforeEnterEditMode()
? selection.isEditing
: false
: false;
this.host.props.setSelection({
this.logic.setSelection({
...selection,
isEditing,
});
} else {
this.host.props.setSelection(selection);
this.logic.setSelection(selection);
}
}
get view() {
return this.host.props.view;
return this.logic.view;
}
constructor(private readonly host: DataViewKanban) {
this.host.addController(this);
get host() {
return this.logic.ui$.value;
}
constructor(public logic: KanbanViewUILogic) {}
blur(selection: KanbanViewSelection) {
const host = this.host;
if (!host) {
return;
}
if (selection.selectionType !== 'cell') {
const selectCards = getSelectedCards(this.host, selection);
selectCards.forEach(card => (card.isFocus = false));
selectCards.forEach(card => (card.isFocus$.value = false));
return;
}
const container = getFocusCell(this.host, selection);
if (!container) {
return;
}
container.isFocus = false;
container.isFocus$.value = false;
const cell = container?.cell;
if (selection.isEditing) {
@@ -116,19 +126,23 @@ export class KanbanSelectionController implements ReactiveController {
return;
}
if (selection.selectionType === 'card') {
this.host.props.view.rowsDelete(selection.cards.map(v => v.cardId));
this.view.rowsDelete(selection.cards.map(v => v.cardId));
this.selection = undefined;
}
}
focus(selection: KanbanViewSelection) {
const host = this.host;
if (!host) {
return;
}
if (selection.selectionType !== 'cell') {
const selectCards = getSelectedCards(this.host, selection);
selectCards.forEach((card, index) => {
if (index === 0) {
card.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
card.isFocus = true;
card.isFocus$.value = true;
});
return;
}
@@ -137,7 +151,7 @@ export class KanbanSelectionController implements ReactiveController {
return;
}
container.scrollIntoView({ block: 'nearest', inline: 'nearest' });
container.isFocus = true;
container.isFocus$.value = true;
const cell = container?.cell;
if (selection.isEditing) {
if (cell?.focusCell()) {
@@ -153,10 +167,9 @@ export class KanbanSelectionController implements ReactiveController {
}
focusFirstCell() {
const group = this.host.groupManager?.groupsDataList$.value?.[0];
const group = this.logic.groups$.value?.[0];
const card = group?.rows[0];
const columnId =
card && this.host.props.view.getHeaderTitle(card.rowId)?.id;
const columnId = card && this.view.getHeaderTitle(card.rowId)?.id;
if (group && card && columnId) {
this.selection = {
selectionType: 'cell',
@@ -169,6 +182,10 @@ export class KanbanSelectionController implements ReactiveController {
}
focusIn() {
const host = this.host;
if (!host) {
return;
}
const selection = this.selection;
if (!selection) return;
if (selection.selectionType === 'cell' && selection.isEditing) return;
@@ -198,6 +215,10 @@ export class KanbanSelectionController implements ReactiveController {
}
focusNext(position: 'up' | 'down' | 'left' | 'right') {
const host = this.host;
if (!host) {
return;
}
const selection = this.selection;
if (!selection) {
return;
@@ -222,7 +243,7 @@ export class KanbanSelectionController implements ReactiveController {
}
} else if (selection.selectionType === 'card') {
// card focus
const group = this.host.querySelector(
const group = this.host?.querySelector(
`affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]`
);
const cardElements = Array.from(
@@ -292,7 +313,11 @@ export class KanbanSelectionController implements ReactiveController {
cards: KanbanCardSelectionCard[];
}
| undefined {
const group = this.host.querySelector(
const host = this.host;
if (!host) {
return;
}
const group = host.querySelector(
`affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]`
);
const kanbanCards = Array.from(
@@ -332,7 +357,7 @@ export class KanbanSelectionController implements ReactiveController {
}
const groups = Array.from(
this.host.querySelectorAll('affine-data-view-kanban-group')
this.host?.querySelectorAll('affine-data-view-kanban-group') ?? []
);
if (nextPosition === 'right') {
@@ -369,6 +394,10 @@ export class KanbanSelectionController implements ReactiveController {
groupKey?: string;
}
| undefined {
const host = this.host;
if (!host) {
return;
}
const kanbanCells = getCardCellsBySelection(this.host, selection);
const group = this.host.querySelector(
`affine-data-view-kanban-group[data-key="${selection.groupKey}"]`
@@ -426,7 +455,7 @@ export class KanbanSelectionController implements ReactiveController {
}
const groups = Array.from(
this.host.querySelectorAll('affine-data-view-kanban-group')
this.host?.querySelectorAll('affine-data-view-kanban-group') ?? []
);
if (nextPosition === 'right') {
@@ -453,8 +482,8 @@ export class KanbanSelectionController implements ReactiveController {
}
hostConnected() {
this.host.disposables.add(
this.host.props.selection$.subscribe(selection => {
this.host?.disposables.add(
this.logic.selection$.subscribe(selection => {
const old = this._selection;
if (old) {
this.blur(old);

View File

@@ -0,0 +1,11 @@
import { KanbanCard } from './card.js';
import { KanbanCell } from './cell.js';
import { KanbanGroup } from './group.js';
import { KanbanHeader } from './header.js';
export function pcEffects() {
customElements.define('affine-data-view-kanban-card', KanbanCard);
customElements.define('affine-data-view-kanban-cell', KanbanCell);
customElements.define('affine-data-view-kanban-group', KanbanGroup);
customElements.define('affine-data-view-kanban-header', KanbanHeader);
}

View File

@@ -11,11 +11,10 @@ import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js';
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css`
affine-data-view-kanban-group {
@@ -99,40 +98,34 @@ export class KanbanGroup extends SignalWatcher(
private readonly clickAddCard = () => {
const id = this.view.addCard('end', this.group.key);
requestAnimationFrame(() => {
const kanban = this.closest('affine-data-view-kanban');
if (kanban) {
const columnId =
this.view.mainProperties$.value.titleColumn ||
this.view.propertyIds$.value[0];
if (!columnId) return;
kanban.selectionController.selection = {
selectionType: 'cell',
groupKey: this.group.key,
cardId: id,
columnId,
isEditing: true,
};
}
const columnId =
this.view.mainProperties$.value.titleColumn ||
this.view.propertyIds$.value[0];
if (!columnId) return;
this.kanbanViewLogic.selectionController.selection = {
selectionType: 'cell',
groupKey: this.group.key,
cardId: id,
columnId,
isEditing: true,
};
});
};
private readonly clickAddCardInStart = () => {
const id = this.view.addCard('start', this.group.key);
requestAnimationFrame(() => {
const kanban = this.closest('affine-data-view-kanban');
if (kanban) {
const columnId =
this.view.mainProperties$.value.titleColumn ||
this.view.propertyIds$.value[0];
if (!columnId) return;
kanban.selectionController.selection = {
selectionType: 'cell',
groupKey: this.group.key,
cardId: id,
columnId,
isEditing: true,
};
}
const columnId =
this.view.mainProperties$.value.titleColumn ||
this.view.propertyIds$.value[0];
if (!columnId) return;
this.kanbanViewLogic.selectionController.selection = {
selectionType: 'cell',
groupKey: this.group.key,
cardId: id,
columnId,
isEditing: true,
};
});
};
@@ -176,8 +169,7 @@ export class KanbanGroup extends SignalWatcher(
<affine-data-view-kanban-card
data-card-id="${row.rowId}"
.groupKey="${this.group.key}"
.dataViewEle="${this.dataViewEle}"
.view="${this.view}"
.kanbanViewLogic="${this.kanbanViewLogic}"
.cardId="${row.rowId}"
></affine-data-view-kanban-card>
`;
@@ -197,14 +189,15 @@ export class KanbanGroup extends SignalWatcher(
`;
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group!: Group;
@property({ attribute: false })
accessor view!: KanbanSingleView;
accessor kanbanViewLogic!: KanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
}
declare global {

View File

@@ -0,0 +1,330 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { css } from '@emotion/css';
import { computed, signal } from '@preact/signals-core';
import { type TemplateResult } from 'lit';
import { ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import {
type GroupTrait,
groupTraitKey,
} from '../../../core/group-by/trait.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
import {
createSortContext,
sortable,
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { horizontalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection.js';
import { KanbanClipboardController } from './controller/clipboard.js';
import { KanbanDragController } from './controller/drag.js';
import { KanbanHotkeysController } from './controller/hotkeys.js';
import { KanbanSelectionController } from './controller/selection.js';
export class KanbanViewUILogic extends DataViewUILogicBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
ui$ = signal<KanbanViewUI | undefined>();
clipboardController = new KanbanClipboardController(this);
dragController = new KanbanDragController(this);
hotkeysController = new KanbanHotkeysController(this);
selectionController = new KanbanSelectionController(this);
groupTrait$ = computed(() => {
return this.view.traitGet(groupTraitKey);
});
groups$ = computed(() => {
const groupTrait = this.groupTrait$.value;
return groupTrait?.groupsDataList$.value || [];
});
private get readonly() {
return this.view.readonly$.value;
}
clearSelection = () => {
this.selectionController.clear();
};
addRow = (position: InsertToPosition) => {
if (this.readonly) return;
const rowId = this.view.rowAdd(position);
if (rowId) {
this.root.openDetailPanel({
view: this.view,
rowId,
});
}
return rowId;
};
focusFirstCell = () => {
this.selectionController.focusFirstCell();
};
showIndicator = (evt: MouseEvent) => {
return this.dragController.showIndicator(evt, undefined) != null;
};
hideIndicator = () => {
this.dragController.dropPreview.remove();
};
moveTo = (id: string, evt: MouseEvent) => {
const position = this.dragController.getInsertPosition(evt);
if (position) {
position.group.group.manager.moveCardTo(
id,
'',
position.group.group.key,
position.position
);
}
};
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = (groupHelper: GroupTrait) => {
const addGroup = groupHelper.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
}),
],
},
});
};
return html` <div
style="height: 32px;flex-shrink:0;display:flex;align-items:center;"
@click="${add}"
>
<div class="${addGroupIconStyle}">${AddCursorIcon()}</div>
</div>`;
};
scrollContainer$ = signal<HTMLElement | undefined>(undefined);
renderer = createUniComponentFromWebComponent(KanbanViewUI);
}
export class KanbanViewUI extends DataViewUIBase<KanbanViewUILogic> {
readonly sortContext = createSortContext({
activators: defaultActivators,
container: this,
onDragEnd: evt => {
const over = evt.over;
const activeId = evt.active.id;
const groupTrait = this.logic.groupTrait$.value;
const groups = groupTrait?.groupsDataList$.value;
if (over && over.id !== activeId && groups) {
const activeIndex = groups.findIndex(data => data?.key === activeId);
const overIndex = groups.findIndex(data => data?.key === over.id);
groupTrait?.moveGroupTo(
activeId,
activeIndex > overIndex
? {
before: true,
id: over.id,
}
: {
before: false,
id: over.id,
}
);
}
},
modifiers: [
({ transform }) => {
return {
...transform,
y: 0,
};
},
],
items: computed(() => {
return this.logic.groups$.value?.map(v => v?.key ?? 'default key') ?? [];
}),
strategy: horizontalListSortingStrategy,
});
private renderGroups() {
const groups = this.logic.groups$.value;
if (!groups) {
return html``;
}
return html`${groups.map(group => {
return html` <affine-data-view-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.kanbanViewLogic="${this.logic}"
.group="${group}"
></affine-data-view-kanban-group>`;
})}`;
}
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.logic.clipboardController.hostConnected();
this.logic.dragController.hostConnected();
this.logic.hotkeysController.hostConnected();
this.logic.selectionController.hostConnected();
this.classList.add('kanban-view', kanbanViewStyle);
this.style.userSelect = 'none';
this.style.display = 'flex';
this.style.flexDirection = 'column';
}
override render(): TemplateResult {
const groups = this.logic.groups$.value;
if (!groups) {
return html``;
}
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
const groupTrait = this.logic.groupTrait$.value;
return html`
${renderUniLit(this.logic.root.config.headerWidget, {
dataViewLogic: this.logic,
})}
<div
${ref(this.logic.scrollContainer$)}
class="${kanbanGroupsStyle}"
style="${wrapperStyle}"
@wheel="${this.logic.onWheel}"
>
${this.renderGroups()}
${groupTrait ? this.logic.renderAddGroup(groupTrait) : ''}
</div>
`;
}
}
const kanbanViewStyle = css({
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
});
const kanbanGroupsStyle = css({
position: 'relative',
zIndex: 1,
display: 'flex',
gap: '20px',
paddingBottom: '4px',
overflowX: 'scroll',
overflowY: 'hidden',
'&:hover': {
paddingBottom: '0px',
},
'&::-webkit-scrollbar': {
WebkitAppearance: 'none',
display: 'block',
},
'&::-webkit-scrollbar:horizontal': {
height: '4px',
},
'&::-webkit-scrollbar-thumb': {
borderRadius: '2px',
backgroundColor: 'transparent',
},
'&:hover::-webkit-scrollbar:horizontal': {
height: '8px',
},
'&:hover::-webkit-scrollbar-thumb': {
borderRadius: '16px',
backgroundColor: 'var(--affine-black-30)',
},
'&:hover::-webkit-scrollbar-track': {
backgroundColor: 'var(--affine-hover-color)',
},
});
const addGroupIconStyle = css({
padding: '4px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'var(--affine-hover-color)',
},
'& svg': {
width: '16px',
height: '16px',
fill: 'var(--affine-icon-color)',
color: 'var(--affine-icon-color)',
},
});
declare global {
interface HTMLElementTagNameMap {
'dv-kanban-view-ui': KanbanViewUI;
}
}

View File

@@ -1,300 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { computed } from '@preact/signals-core';
import { css } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
import {
createSortContext,
sortable,
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { horizontalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection';
import { KanbanClipboardController } from './controller/clipboard.js';
import { KanbanDragController } from './controller/drag.js';
import { KanbanHotkeysController } from './controller/hotkeys.js';
import { KanbanSelectionController } from './controller/selection.js';
const styles = css`
affine-data-view-kanban {
user-select: none;
display: flex;
flex-direction: column;
}
.affine-data-view-kanban-groups {
position: relative;
z-index: 1;
display: flex;
gap: 20px;
padding-bottom: 4px;
overflow-x: scroll;
overflow-y: hidden;
}
.affine-data-view-kanban-groups:hover {
padding-bottom: 0px;
}
.affine-data-view-kanban-groups::-webkit-scrollbar {
-webkit-appearance: none;
display: block;
}
.affine-data-view-kanban-groups::-webkit-scrollbar:horizontal {
height: 4px;
}
.affine-data-view-kanban-groups::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: transparent;
}
.affine-data-view-kanban-groups:hover::-webkit-scrollbar:horizontal {
height: 8px;
}
.affine-data-view-kanban-groups:hover::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: var(--affine-black-30);
}
.affine-data-view-kanban-groups:hover::-webkit-scrollbar-track {
background-color: var(--affine-hover-color);
}
.add-group-icon {
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
cursor: pointer;
}
.add-group-icon:hover {
background-color: var(--affine-hover-color);
}
.add-group-icon svg {
width: 16px;
height: 16px;
fill: var(--affine-icon-color);
color: var(--affine-icon-color);
}
`;
export class DataViewKanban extends DataViewBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
static override styles = styles;
private readonly dragController = new KanbanDragController(this);
clipboardController = new KanbanClipboardController(this);
hotkeysController = new KanbanHotkeysController(this);
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = this.groupManager.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div
style="height: 32px;flex-shrink:0;display:flex;align-items:center;"
@click="${add}"
>
<div class="add-group-icon">${AddCursorIcon()}</div>
</div>`;
};
selectionController = new KanbanSelectionController(this);
sortContext = createSortContext({
activators: defaultActivators,
container: this,
onDragEnd: evt => {
const over = evt.over;
const activeId = evt.active.id;
const groups = this.groupManager.groupsDataList$.value;
if (over && over.id !== activeId && groups) {
const activeIndex = groups.findIndex(data => data?.key === activeId);
const overIndex = groups.findIndex(data => data?.key === over.id);
this.groupManager.moveGroupTo(
activeId,
activeIndex > overIndex
? {
before: true,
id: over.id,
}
: {
before: false,
id: over.id,
}
);
}
},
modifiers: [
({ transform }) => {
return {
...transform,
y: 0,
};
},
],
items: computed(() => {
return (
this.groupManager.groupsDataList$.value?.map(
v => v?.key ?? 'default key'
) ?? []
);
}),
strategy: horizontalListSortingStrategy,
});
get expose(): DataViewInstance {
return {
clearSelection: () => {
this.selectionController.clear();
},
addRow: position => {
if (this.props.view.readonly$.value) return;
const rowId = this.props.view.rowAdd(position);
if (rowId) {
this.props.dataViewEle.openDetailPanel({
view: this.props.view,
rowId,
});
}
return rowId;
},
focusFirstCell: () => {
this.selectionController.focusFirstCell();
},
getSelection: () => {
return this.selectionController.selection;
},
hideIndicator: () => {
this.dragController.dropPreview.remove();
},
moveTo: (id, evt) => {
const position = this.dragController.getInsertPosition(evt);
if (position) {
position.group.group.manager.moveCardTo(
id,
'',
position.group.group.key,
position.position
);
}
},
showIndicator: evt => {
return this.dragController.shooIndicator(evt, undefined) != null;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
get groupManager() {
return this.props.view.groupTrait;
}
override render() {
const groups = this.groupManager.groupsDataList$.value;
if (!groups) {
return html``;
}
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div
class="affine-data-view-kanban-groups"
style="${wrapperStyle}"
@wheel="${this.onWheel}"
>
${repeat(
groups,
group => group?.key ?? 'default key',
group => {
if (!group) return;
return html` <affine-data-view-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.group="${group}"
></affine-data-view-kanban-group>`;
}
)}
${this.renderAddGroup()}
</div>
`;
}
@query('.affine-data-view-kanban-groups')
accessor groups!: HTMLElement;
}
declare global {
interface HTMLElementTagNameMap {
'affine-data-view-kanban': DataViewKanban;
}
}

View File

@@ -12,17 +12,17 @@ import {
} from '@blocksuite/icons/lit';
import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { KanbanSelectionController } from './controller/selection.js';
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
export const openDetail = (
dataViewEle: DataViewRenderer,
kanbanViewLogic: KanbanViewUILogic,
rowId: string,
selection: KanbanSelectionController
) => {
const old = selection.selection;
selection.selection = undefined;
dataViewEle.openDetailPanel({
kanbanViewLogic.root.openDetailPanel({
view: selection.view,
rowId: rowId,
onClose: () => {
@@ -32,7 +32,7 @@ export const openDetail = (
};
export const popCardMenu = (
dataViewEle: DataViewRenderer,
kanbanViewLogic: KanbanViewUILogic,
ele: PopupTarget,
rowId: string,
selection: KanbanSelectionController
@@ -42,7 +42,7 @@ export const popCardMenu = (
name: 'Expand Card',
prefix: ExpandFullIcon(),
select: () => {
openDetail(dataViewEle, rowId, selection);
openDetail(kanbanViewLogic, rowId, selection);
},
}),
menu.subMenu({

View File

@@ -1,11 +1,12 @@
import { createUniComponentFromWebComponent } from '../../core/index.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import { kanbanViewModel } from './define.js';
import { MobileDataViewKanban } from './mobile/kanban-view.js';
import { DataViewKanban } from './pc/kanban-view.js';
import { MobileKanbanViewUILogic } from './mobile/kanban-view-ui-logic.js';
import { KanbanViewUILogic } from './pc/kanban-view-ui-logic.js';
export const kanbanViewMeta = kanbanViewModel.createMeta({
icon: createIcon('DatabaseKanbanViewIcon'),
view: createUniComponentFromWebComponent(DataViewKanban),
mobileView: createUniComponentFromWebComponent(MobileDataViewKanban),
// @ts-expect-error fixme: typesafe
pcLogic: () => KanbanViewUILogic,
// @ts-expect-error fixme: typesafe
mobileLogic: () => MobileKanbanViewUILogic,
});

View File

@@ -0,0 +1,11 @@
import { mobileEffects } from './mobile/effect.js';
import { pcEffects } from './pc/effect.js';
import { pcVirtualEffects } from './pc-virtual/effect.js';
import { statsEffects } from './stats/effect.js';
export function tableEffects() {
mobileEffects();
statsEffects();
pcEffects();
pcVirtualEffects();
}

View File

@@ -1,8 +1,6 @@
export * from './define.js';
export * from './pc/effect.js';
export * from './pc/table-view.js';
export * from './pc-virtual/effect.js';
export * from './renderer.js';
export * from './selection.js';
export * from './table-view-manager.js';
export * from './table-view-selector.js';

View File

@@ -8,10 +8,10 @@ import {
type CellRenderProps,
type DataViewCellLifeCycle,
renderUniLit,
type SingleView,
} from '../../../core/index.js';
import { TableViewAreaSelection } from '../selection';
import type { TableProperty } from '../table-view-manager.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export class MobileTableCell extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -48,7 +48,7 @@ export class MobileTableCell extends SignalWatcher(
});
isSelectionEditing$ = computed(() => {
const selection = this.table?.props.selection$.value;
const selection = this.tableViewLogic.selection$.value;
if (selection?.selectionType !== 'area') {
return false;
}
@@ -68,8 +68,8 @@ export class MobileTableCell extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const setSelection = this.table?.props.setSelection;
const viewId = this.table?.props.view.id;
const setSelection = this.tableViewLogic.setSelection;
const viewId = this.tableViewLogic.view.id;
if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) {
return;
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
return this.closest('mobile-table-group')?.group?.key;
}
private get table() {
return this.closest('mobile-data-view-table');
}
override connectedCallback() {
super.connectedCallback();
if (this.column.readonly$.value) return;
@@ -160,7 +156,11 @@ export class MobileTableCell extends SignalWatcher(
accessor rowIndex!: number;
@property({ attribute: false })
accessor view!: SingleView;
accessor tableViewLogic!: MobileTableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -0,0 +1,15 @@
import { MobileTableCell } from './cell.js';
import { MobileTableColumnHeader } from './column-header.js';
import { MobileTableGroup } from './group.js';
import { MobileTableHeader } from './header.js';
import { MobileTableRow } from './row.js';
import { MobileTableViewUI } from './table-view-ui-logic.js';
export function mobileEffects() {
customElements.define('mobile-table-cell', MobileTableCell);
customElements.define('mobile-table-group', MobileTableGroup);
customElements.define('mobile-data-view-table-ui', MobileTableViewUI);
customElements.define('mobile-table-header', MobileTableHeader);
customElements.define('mobile-table-column-header', MobileTableColumnHeader);
customElements.define('mobile-table-row', MobileTableRow);
}

View File

@@ -8,17 +8,14 @@ import { PlusIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, html, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js';
import type { Row } from '../../../core/index.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { DataViewTable } from '../pc/table-view.js';
import { TableViewAreaSelection } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
const styles = css`
.data-view-table-group-add-row {
@@ -54,40 +51,10 @@ export class MobileTableGroup extends SignalWatcher(
private readonly clickAddRow = () => {
this.view.rowAdd('end', this.group?.key);
const selectionController = this.viewEle.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
selectionController.selection = TableViewAreaSelection.create({
groupKey: this.group?.key,
focus: {
rowIndex: this.rows.length - 1,
columnIndex: index,
},
isEditing: true,
});
});
};
private readonly clickAddRowInStart = () => {
this.view.rowAdd('start', this.group?.key);
const selectionController = this.viewEle.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
selectionController.selection = TableViewAreaSelection.create({
groupKey: this.group?.key,
focus: {
rowIndex: 0,
columnIndex: index,
},
isEditing: true,
});
});
};
private readonly clickGroupOptions = (e: MouseEvent) => {
@@ -150,8 +117,7 @@ export class MobileTableGroup extends SignalWatcher(
return html` <mobile-table-row
data-row-index="${idx}"
data-row-id="${row.rowId}"
.dataViewEle="${this.dataViewEle}"
.view="${this.view}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${row.rowId}"
.rowIndex="${idx}"
></mobile-table-row>`;
@@ -172,8 +138,6 @@ export class MobileTableGroup extends SignalWatcher(
${PlusIcon()}<span style="font-size: 12px">New Record</span>
</div>
</div>`}
<affine-database-column-stats .view="${this.view}" .group="${this.group}">
</affine-database-column-stats>
`;
}
@@ -181,20 +145,15 @@ export class MobileTableGroup extends SignalWatcher(
return this.renderRows(this.rows);
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null;
@property({ attribute: false })
accessor view!: TableSingleView;
accessor tableViewLogic!: MobileTableViewUILogic;
@property({ attribute: false })
accessor viewEle!: DataViewTable;
get view() {
return this.tableViewLogic.view;
}
}
declare global {

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