Compare commits

...

16 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
178 changed files with 3573 additions and 2934 deletions

View File

@@ -52,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": {

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'

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

@@ -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

@@ -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

@@ -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

@@ -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

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

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 {

View File

@@ -8,6 +8,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { cellDivider } from '../styles.js';
import type { TableSingleView } from '../table-view-manager.js';
export class MobileTableHeader extends SignalWatcher(
@@ -60,7 +61,7 @@ export class MobileTableHeader extends SignalWatcher(
.column="${column}"
.tableViewManager="${this.tableViewManager}"
></mobile-table-column-header>
<div class="cell-divider" style="height: auto;"></div>
<div class="${cellDivider}" style="height: auto;"></div>
`;
}
)}

View File

@@ -5,13 +5,13 @@ import {
} from '@blocksuite/affine-components/context-menu';
import { DeleteIcon, ExpandFullIcon } from '@blocksuite/icons/lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { SingleView } from '../../../core/index.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export const popMobileRowMenu = (
target: PopupTarget,
rowId: string,
dataViewEle: DataViewRenderer,
tableViewLogic: MobileTableViewUILogic,
view: SingleView
) => {
popFilterableSimpleMenu(target, [
@@ -21,7 +21,7 @@ export const popMobileRowMenu = (
name: 'Expand Row',
prefix: ExpandFullIcon(),
select: () => {
dataViewEle.openDetailPanel({
tableViewLogic.root.openDetailPanel({
view: view,
rowId: rowId,
});

View File

@@ -10,9 +10,9 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { TableSingleView } from '../table-view-manager.js';
import { cellDivider } from '../styles.js';
import { popMobileRowMenu } from './menu.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export class MobileTableRow extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -73,7 +73,7 @@ export class MobileTableRow extends SignalWatcher(
v => v.id,
(column, i) => {
const clickDetail = () => {
this.dataViewEle.openDetailPanel({
this.tableViewLogic.root.openDetailPanel({
view: this.view,
rowId: this.rowId,
});
@@ -83,7 +83,7 @@ export class MobileTableRow extends SignalWatcher(
popMobileRowMenu(
popupTargetFromElement(ele),
this.rowId,
this.dataViewEle,
this.tableViewLogic,
this.view
);
};
@@ -95,7 +95,7 @@ export class MobileTableRow extends SignalWatcher(
width: `${column.width$.value}px`,
border: i === 0 ? 'none' : undefined,
})}
.view="${view}"
.tableViewLogic="${this.tableViewLogic}"
.column="${column}"
.rowId="${this.rowId}"
data-row-id="${this.rowId}"
@@ -107,7 +107,7 @@ export class MobileTableRow extends SignalWatcher(
data-column-index="${i}"
>
</mobile-table-cell>
<div class="cell-divider"></div>
<div class="${cellDivider}"></div>
</div>
${!column.readonly$.value &&
column.view.mainProperties$.value.titleColumn === column.id
@@ -130,7 +130,7 @@ export class MobileTableRow extends SignalWatcher(
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
accessor tableViewLogic!: MobileTableViewUILogic;
@property({ attribute: false })
accessor rowId!: string;
@@ -138,8 +138,9 @@ export class MobileTableRow extends SignalWatcher(
@property({ attribute: false })
accessor rowIndex!: number;
@property({ attribute: false })
accessor view!: TableSingleView;
get view() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -0,0 +1,27 @@
import { css } from '@emotion/css';
import { cssVarV2 } from '@toeverything/theme/v2';
export const mobileTableViewWrapper = css({
position: 'relative',
width: '100%',
paddingBottom: '4px',
/**
* Disable horizontal scrolling to prevent crashes on iOS Safari
* See https://github.com/toeverything/AFFiNE/pull/12203
* and https://github.com/toeverything/blocksuite/pull/8784
*/
overflowX: 'hidden',
overflowY: 'hidden',
});
export const mobileTableViewContainer = css({
position: 'relative',
width: 'fit-content',
minWidth: '100%',
});
export const mobileCellDivider = css({
width: '1px',
height: '100%',
backgroundColor: cssVarV2.layer.insideBorder.border,
});

View File

@@ -0,0 +1,160 @@
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 { 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 type { GroupTrait } from '../../../core/group-by/trait.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import {
mobileTableViewContainer,
mobileTableViewWrapper,
} from './table-view-style.js';
export class MobileTableViewUILogic extends DataViewUILogicBase<
TableSingleView,
TableViewSelectionWithType
> {
ui$ = signal<MobileTableViewUI | 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 = () => {};
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="display:flex;">
<div
class="dv-hover dv-round-8"
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
@click="${add}"
>
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
renderer = createUniComponentFromWebComponent(MobileTableViewUI);
}
export class MobileTableViewUI extends DataViewUIBase<MobileTableViewUILogic> {
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.classList.add(mobileTableViewWrapper);
}
private renderTable() {
const groups = this.logic.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div style="display:flex;flex-direction: column;gap: 16px;">
${repeat(
groups,
v => v.key,
group => {
return html` <mobile-table-group
data-group-key="${group.key}"
.tableViewLogic="${this.logic}"
.group="${group}"
></mobile-table-group>`;
}
)}
${this.logic.renderAddGroup(this.logic.view.groupTrait)}
</div>
`;
}
return html` <mobile-table-group
.tableViewLogic="${this.logic}"
></mobile-table-group>`;
}
override render(): TemplateResult {
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.logic.root.config.headerWidget, {
dataViewLogic: this.logic,
})}
<div class="${mobileTableViewWrapper}" style="${wrapperStyle}">
<div class="${mobileTableViewContainer}" style="${containerStyle}">
${this.renderTable()}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-table-ui': MobileTableViewUI;
}
}

View File

@@ -1,215 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, unsafeCSS } 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 { GroupTrait } from '../../../core/group-by/trait.js';
import type { DataViewInstance } from '../../../core/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
export class MobileDataViewTable extends DataViewBase<
TableSingleView,
TableViewSelectionWithType
> {
static override styles = css`
.mobile-affine-database-table-wrapper {
position: relative;
width: 100%;
padding-bottom: 4px;
/**
* Disable horizontal scrolling to prevent crashes on iOS Safari
* See https://github.com/toeverything/AFFiNE/pull/12203
* and https://github.com/toeverything/blocksuite/pull/8784
*/
overflow-x: hidden;
overflow-y: hidden;
}
.mobile-affine-database-table-container {
position: relative;
width: fit-content;
min-width: 100%;
}
.cell-divider {
width: 1px;
height: 100%;
background-color: ${unsafeCSS(cssVarV2.layer.insideBorder.border)};
}
`;
private readonly _addRow = (
tableViewManager: TableSingleView,
position: InsertToPosition | number
) => {
if (this.readonly) return;
tableViewManager.rowAdd(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.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div style="display:flex;">
<div
class="dv-hover dv-round-8"
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
@click="${add}"
>
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
get expose(): DataViewInstance {
return {
clearSelection: () => {},
addRow: position => {
this._addRow(this.props.view, position);
},
focusFirstCell: () => {},
showIndicator: _evt => {
return false;
},
hideIndicator: () => {
// this.dragController.dropPreview.remove();
},
moveTo: (_id, _evt) => {
// const result = this.dragController.getInsertPosition(evt);
// if (result) {
// this.props.view.rowMove(
// id,
// result.position,
// undefined,
// result.groupKey,
// );
// }
},
getSelection: () => {
throw new BlockSuiteError(
ErrorCode.DatabaseBlockError,
'Not implemented'
);
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
private get readonly() {
return this.props.view.readonly$.value;
}
private renderTable() {
const groups = this.props.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div style="display:flex;flex-direction: column;gap: 16px;">
${repeat(
groups,
v => v.key,
group => {
return html` <mobile-table-group
data-group-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
.group="${group}"
></mobile-table-group>`;
}
)}
${this.renderAddGroup(this.props.view.groupTrait)}
</div>
`;
}
return html` <mobile-table-group
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
></mobile-table-group>`;
}
override render() {
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div class="mobile-affine-database-table-wrapper" style="${wrapperStyle}">
<div
class="mobile-affine-database-table-container"
style="${containerStyle}"
@wheel="${this.onWheel}"
>
${this.renderTable()}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-table': MobileDataViewTable;
}
}

View File

@@ -1,3 +1,4 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { UIEventStateContext } from '@blocksuite/std';
import type { ReactiveController } from 'lit';
@@ -9,20 +10,18 @@ import {
type TableViewSelection,
type TableViewSelectionWithType,
} from '../../selection';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table';
type JsonAreaData = string[][];
const TEXT = 'text/plain';
export class TableClipboardController implements ReactiveController {
disposables = new DisposableGroup();
private readonly _onCopy = (
tableSelection: TableViewSelectionWithType,
isCut = false
) => {
const table = this.host;
const area = getSelectedArea(tableSelection, table);
const area = getSelectedArea(tableSelection, this.logic);
if (!area) {
return;
}
@@ -44,7 +43,7 @@ export class TableClipboardController implements ReactiveController {
}
}
if (deleteRows.length) {
this.props.view.rowsDelete(deleteRows);
this.logic.view.rowsDelete(deleteRows);
}
}
this.clipboard
@@ -79,12 +78,11 @@ export class TableClipboardController implements ReactiveController {
private readonly _onPaste = async (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw;
event.stopPropagation();
const view = this.host;
const clipboardData = event.clipboardData;
if (!clipboardData) return;
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selection;
if (TableViewRowSelection.is(tableSelection)) {
return;
}
@@ -97,7 +95,7 @@ export class TableClipboardController implements ReactiveController {
if (dataString) {
// If internal format data exists, use it
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
pasteToCells(view, jsonAreaData, tableSelection);
pasteToCells(this.logic, jsonAreaData, tableSelection);
return true;
}
} catch {
@@ -115,7 +113,7 @@ export class TableClipboardController implements ReactiveController {
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
if (rows.length > 0) {
pasteToCells(view, rows, tableSelection);
pasteToCells(this.logic, rows, tableSelection);
}
}
}
@@ -124,27 +122,28 @@ export class TableClipboardController implements ReactiveController {
};
private get clipboard() {
return this.props.clipboard;
return this.logic.root.config.clipboard;
}
private get notification() {
return this.props.notification;
}
get props() {
return this.host.props;
return this.logic.root.config.notification;
}
private get readonly() {
return this.props.view.readonly$.value;
return this.logic.view.readonly$.value;
}
constructor(public host: VirtualTableView) {
host.addController(this);
constructor(public logic: VirtualTableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
get selection() {
return this.logic.selectionController.selection;
}
copy() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selection;
if (!tableSelection) {
return;
}
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
}
cut() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selection;
if (!tableSelection) {
return;
}
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
}
hostConnected() {
this.host.disposables.add(
this.props.handleEvent('copy', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.disposables.add(
this.logic.handleEvent('copy', _ctx => {
const tableSelection = this.selection;
if (!tableSelection) return false;
this._onCopy(tableSelection);
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('cut', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.disposables.add(
this.logic.handleEvent('cut', _ctx => {
const tableSelection = this.selection;
if (!tableSelection) return false;
this._onCut(tableSelection);
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('paste', ctx => {
this.disposables.add(
this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false;
this._onPaste(ctx).catch(console.error);
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
function getSelectedArea(
selection: TableViewSelection,
table: VirtualTableView
table: VirtualTableViewUILogic
): SelectedArea | undefined {
const view = table.props.view;
const view = table.view;
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rows(selection)
.map(row => {
@@ -282,7 +281,7 @@ function getTargetRangeFromSelection(
}
function pasteToCells(
table: VirtualTableView,
table: VirtualTableViewUILogic,
rows: JsonAreaData,
selection: TableViewAreaSelection
) {

View File

@@ -8,7 +8,7 @@ import * as Y from 'yjs';
import { t } from '../../../../core/index.js';
import type { TableViewAreaSelection } from '../../selection';
import type { VirtualTableView } from '../table-view';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
export class DragToFillElement extends ShadowlessElement {
static override styles = css`
@@ -55,12 +55,12 @@ declare global {
}
export function fillSelectionWithFocusCellData(
host: VirtualTableView,
logic: VirtualTableViewUILogic,
selection: TableViewAreaSelection
) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection;
const focusCell = host.selectionController.getCellContainer(
const focusCell = logic.selectionController.getCellContainer(
groupKey,
focus.rowIndex,
focus.columnIndex
@@ -85,7 +85,7 @@ export function fillSelectionWithFocusCellData(
for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue;
const cellContainer = host.selectionController.getCellContainer(
const cellContainer = logic.selectionController.getCellContainer(
groupKey,
i,
draggingColIdx

View File

@@ -3,8 +3,7 @@
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { ReactiveController } from 'lit';
// import { startDrag } from '../../../../core/utils/drag.js';
import type { VirtualTableView } from '../table-view';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export class TableDragController implements ReactiveController {
// dragStart = (row: TableRow, evt: PointerEvent) => {
@@ -91,9 +90,9 @@ export class TableDragController implements ReactiveController {
| undefined => {
const y = evt.y;
const tableRect = this.host
.querySelector('affine-data-view-table-group')
?.querySelector('affine-data-view-table-group')
?.getBoundingClientRect();
const rows = this.host.querySelectorAll('data-view-table-row');
const rows = this.host?.querySelectorAll('data-view-table-row');
if (!rows || !tableRect || y < tableRect.top) {
return;
}
@@ -127,12 +126,14 @@ export class TableDragController implements ReactiveController {
return position;
};
constructor(private readonly host: VirtualTableView) {
this.host.addController(this);
constructor(private readonly logic: VirtualTableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
if (this.host.props.view.readonly$.value) {
if (this.logic.view.readonly$.value) {
return;
}
// this.host.disposables.add(

View File

@@ -1,22 +1,26 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { ReactiveController } from 'lit';
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
import { popRowMenu } from '../row/menu';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export class TableHotkeysController implements ReactiveController {
disposables = new DisposableGroup();
get selectionController() {
return this.host.selectionController;
return this.logic.selectionController;
}
constructor(private readonly host: VirtualTableView) {
this.host.addController(this);
constructor(private readonly logic: VirtualTableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
this.host.disposables.add(
this.host.props.bindHotkey({
this.disposables.add(
this.logic.bindHotkey({
Backspace: () => {
const selection = this.selectionController.selection;
if (!selection) {
@@ -25,7 +29,7 @@ export class TableHotkeysController implements ReactiveController {
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rowsIds(selection);
this.selectionController.selection = undefined;
this.host.props.view.rowsDelete(rows);
this.logic.view.rowsDelete(rows);
return;
}
const {
@@ -334,14 +338,14 @@ export class TableHotkeysController implements ReactiveController {
context.get('keyboardState').raw.preventDefault();
this.selectionController.selection = TableViewRowSelection.create({
rows:
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap(
this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
group =>
group?.rows.map(row => ({
groupKey: group.key,
id: row.rowId,
})) ?? []
) ??
this.host.props.view.rows$.value.map(row => ({
this.logic.view.rows$.value.map(row => ({
groupKey: undefined,
id: row.rowId,
})),
@@ -377,7 +381,7 @@ export class TableHotkeysController implements ReactiveController {
rows: [row],
});
popRowMenu(
this.host.props.dataViewEle,
this.logic,
popupTargetFromElement(cell),
this.selectionController
);

View File

@@ -1,4 +1,5 @@
import { getRangeByPositions } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { computed, type ReadonlySignal } from '@preact/signals-core';
@@ -18,7 +19,7 @@ import {
type TableViewSelectionWithType,
} from '../../selection';
import type { DatabaseCellContainer } from '../row/cell';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { TableGridCell } from '../types.js';
import {
DragToFillElement,
@@ -26,6 +27,7 @@ import {
} from './drag-to-fill.js';
export class TableSelectionController implements ReactiveController {
disposables = new DisposableGroup();
private _tableViewSelection?: TableViewSelectionWithType;
private readonly getFocusCellContainer = () => {
@@ -85,12 +87,12 @@ export class TableSelectionController implements ReactiveController {
);
const cell = container?.cell;
const isEditing = cell ? cell.beforeEnterEditMode() : true;
this.host.props.setSelection({
this.logic.setSelection({
...selection,
isEditing,
});
} else {
this.host.props.setSelection(selection);
this.logic.setSelection(selection);
}
}
@@ -99,27 +101,26 @@ export class TableSelectionController implements ReactiveController {
}
get view() {
return this.host.props.view;
return this.logic.view;
}
get viewData() {
return this.view;
}
constructor(public host: VirtualTableView) {
host.addController(this);
constructor(public logic: VirtualTableViewUILogic) {
this.__selectionElement = new SelectionElement();
this.__selectionElement.controller = this;
}
get host() {
return this.logic.ui$.value;
}
private clearSelection() {
this.host.props.setSelection();
this.logic.setSelection();
}
private handleDragEvent() {
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) {
this.disposables.add(
this.logic.handleEvent('dragStart', context => {
if (this.logic.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
@@ -150,8 +151,8 @@ export class TableSelectionController implements ReactiveController {
}
private handleSelectionChange() {
this.host.disposables.add(
this.host.props.selection$.subscribe(tableSelection => {
this.disposables.add(
this.logic.selection$.subscribe(tableSelection => {
if (!this.isValidSelection(tableSelection)) {
this.selection = undefined;
return;
@@ -236,7 +237,7 @@ export class TableSelectionController implements ReactiveController {
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
: this.view.rows$.value;
requestAnimationFrame(() => {
const index = this.host.props.view.properties$.value.findIndex(
const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
this.selection = TableViewAreaSelection.create({
@@ -625,7 +626,7 @@ export class TableSelectionController implements ReactiveController {
}
get virtualScroll() {
return this.host.virtualScroll$.value;
return this.logic.virtualScroll$.value;
}
getGroup(groupKey: string | undefined) {
@@ -913,7 +914,7 @@ export class TableSelectionController implements ReactiveController {
if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData(
this.host,
this.logic,
TableViewAreaSelection.create({
groupKey: groupKey,
rowsSelection: selection.row,
@@ -1024,7 +1025,7 @@ export class SelectionElement extends SignalWatcher(
if (left == null || top == null || width == null || height == null) {
return;
}
const paddingLeft = this.controller.host.props.virtualPadding$.value;
const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
return {
left: left + paddingLeft,
top,
@@ -1054,7 +1055,7 @@ export class SelectionElement extends SignalWatcher(
if (!rect) {
return;
}
const paddingLeft = this.controller.host.props.virtualPadding$.value;
const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
return {
left: rect.left + paddingLeft,
top: rect.top,
@@ -1063,15 +1064,8 @@ export class SelectionElement extends SignalWatcher(
};
});
rowsPosition$ = computed(() => {
const selection = this.selection$.value;
if (selection?.selectionType !== 'area') {
return;
}
});
get selection$() {
return this.controller.host.props.selection$;
return this.controller.logic.selection$;
}
override render() {

View File

@@ -1,3 +1,4 @@
import { KanbanViewUI } from '../../kanban/pc/kanban-view-ui-logic';
import { DragToFillElement } from './controller/drag-to-fill';
import { SelectionElement } from './controller/selection';
import { TableGroupFooter } from './group/bottom/group-footer';
@@ -12,11 +13,12 @@ import { TableVerticalIndicator } from './group/top/header/vertical-indicator';
import { DatabaseCellContainer } from './row/cell';
import { TableRowHeader } from './row/row-header';
import { TableRowLast } from './row/row-last';
import { VirtualTableView } from './table-view';
import { TableViewUI } from './table-view-ui-logic';
import { VirtualElementWrapper } from './virtual/virtual-cell';
export function pcVirtualEffects() {
customElements.define('affine-virtual-table', VirtualTableView);
customElements.define('dv-table-view-ui-virtual', TableViewUI);
customElements.define('dv-kanban-view-ui', KanbanViewUI);
customElements.define(
'affine-database-virtual-cell-container',
DatabaseCellContainer

View File

@@ -6,29 +6,29 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { TableViewAreaSelection } from '../../../selection';
import type { VirtualTableView } from '../../table-view';
import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
import type { TableGridGroup } from '../../types';
import * as styles from './group-footer-css';
export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor gridGroup!: TableGridGroup;
group$ = computed(() => {
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find(
return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
g => g.key === this.gridGroup.groupId
);
});
get selectionController() {
return this.tableView.selectionController;
return this.tableViewLogic.selectionController;
}
get tableViewManager() {
return this.tableView.props.view;
return this.tableViewLogic.view;
}
override connectedCallback() {

View File

@@ -10,7 +10,7 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { TableViewAreaSelection } from '../../../selection';
import type { VirtualTableView } from '../../table-view';
import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
import type { TableGridGroup } from '../../types';
import * as styles from './group-header-css';
import { GroupTitle } from './group-title';
@@ -18,7 +18,7 @@ export class TableGroupHeader extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor gridGroup!: TableGridGroup;
@@ -35,7 +35,7 @@ export class TableGroupHeader extends SignalWatcher(
}
group$ = computed(() => {
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find(
return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
g => g.key === this.gridGroup.groupId
);
});
@@ -45,11 +45,11 @@ export class TableGroupHeader extends SignalWatcher(
});
get tableViewManager() {
return this.tableView.props.view;
return this.tableViewLogic.view;
}
get selectionController() {
return this.tableView.selectionController;
return this.tableViewLogic.selectionController;
}
private readonly clickAddRowInStart = () => {
@@ -123,7 +123,7 @@ export class TableGroupHeader extends SignalWatcher(
return html`
${this.renderGroupHeader()}
<virtual-table-header
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
></virtual-table-header>
`;
}

View File

@@ -6,6 +6,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { PlusIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { css } from '@emotion/css';
import { nothing } from 'lit';
import { property, query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
@@ -13,9 +14,13 @@ import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { renderUniLit } from '../../../../../../core';
import type { TableSingleView } from '../../../../table-view-manager';
import { LEFT_TOOL_BAR_WIDTH } from '../../../../consts';
import { cellDivider } from '../../../../styles';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
import * as styles from './column-header-css';
const leftBarStyle = css({
width: LEFT_TOOL_BAR_WIDTH,
});
export class VirtualTableHeader extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@@ -79,9 +84,7 @@ export class VirtualTableHeader extends SignalWatcher(
override render() {
return html`
<div class="${styles.columnHeader} database-row">
${this.readonly
? nothing
: html` <div class="data-view-table-left-bar"></div>`}
${this.readonly ? nothing : html` <div class="${leftBarStyle}"></div>`}
${repeat(
this.tableViewManager.properties$.value,
column => column.id,
@@ -97,9 +100,9 @@ export class VirtualTableHeader extends SignalWatcher(
data-column-index="${index}"
class="${styles.column} ${styles.cell}"
.column="${column}"
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
></virtual-database-header-column>
<div class="cell-divider" style="height: auto;"></div>
<div class="${cellDivider}" style="height: auto;"></div>
`;
}
)}
@@ -118,7 +121,11 @@ export class VirtualTableHeader extends SignalWatcher(
accessor scaleDiv!: HTMLDivElement;
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: VirtualTableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -13,6 +13,7 @@ import type {
TableProperty,
TableSingleView,
} from '../../../../table-view-manager';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
export class DataViewColumnPreview extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
`;
get tableViewManager(): TableSingleView {
return this.column.view as TableSingleView;
return this.tableViewLogic.view;
}
private renderGroup(rows: Row[]) {
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
)};box-shadow: var(--affine-shadow-2);"
>
<affine-database-header-column
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.column="${this.column}"
></affine-database-header-column>
${repeat(rows, (id, index) => {
const height = this.container.querySelector(
`affine-database-cell-container[data-row-id="${id}"]`
`dv-table-view-cell-container[data-row-id="${id}"]`
)?.clientHeight;
const style = styleMap({
height: height + 'px',
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
)}"
>
<div style="${style}">
<affine-database-cell-container
<dv-table-view-cell-container
.column="${this.column}"
.view="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${id}"
.columnId="${this.column.id}"
.rowIndex="${index}"
.columnIndex="${columnIndex}"
></affine-database-cell-container>
></dv-table-view-cell-container>
</div>
</div>`;
})}
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor tableViewLogic!: VirtualTableViewUILogic;
}
declare global {

View File

@@ -45,10 +45,8 @@ import {
ShowQuickSettingBarKey,
} from '../../../../../../widget-presets/quick-setting-bar/context';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts';
import type {
TableProperty,
TableSingleView,
} from '../../../../table-view-manager';
import type { TableProperty } from '../../../../table-view-manager';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
import {
getTableGroupRect,
getVerticalIndicator,
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
const sortUtils = createSortUtils(
sortTrait,
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {})
this.tableViewLogic.eventTrace ?? (() => {})
);
const sortList = sortUtils.sortList$.value;
const existingIndex = sortList.findIndex(
@@ -398,28 +396,25 @@ export class DatabaseHeaderColumn extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
const table = this.closest('affine-database-table');
if (table) {
this.disposables.add(
table.props.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (
target instanceof Element &&
this.widthDragBar.value?.contains(target)
) {
event.preventDefault();
event.stopPropagation();
this.widthDragStart(event);
return true;
}
return false;
})
);
}
this.disposables.add(
this.tableViewLogic.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (
target instanceof Element &&
this.widthDragBar.value?.contains(target)
) {
event.preventDefault();
event.stopPropagation();
this.widthDragStart(event);
return true;
}
return false;
})
);
}
override render() {
@@ -481,7 +476,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: VirtualTableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
function numberFormatConfig(column: Property): MenuConfig {

View File

@@ -10,13 +10,12 @@ import type {
CellRenderProps,
DataViewCellLifeCycle,
} from '../../../../core/property';
import type { SingleView } from '../../../../core/view-manager/single-view';
import {
TableViewAreaSelection,
TableViewRowSelection,
type TableViewSelectionWithType,
} from '../../selection';
import type { VirtualTableView } from '../table-view';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
import type { TableGridCell } from '../types';
import { popRowMenu } from './menu';
import { rowSelectedBg } from './row-header-css';
@@ -82,7 +81,7 @@ export class DatabaseCellContainer extends SignalWatcher(
}
private get selectionView() {
return this.tableView?.selectionController;
return this.tableViewLogic.selectionController;
}
get rowSelected$() {
@@ -104,11 +103,7 @@ export class DatabaseCellContainer extends SignalWatcher(
rows: [row],
});
}
popRowMenu(
this.tableView.props.dataViewEle,
popupTargetFromElement(this),
selection
);
popRowMenu(this.tableViewLogic, popupTargetFromElement(this), selection);
};
override connectedCallback() {
@@ -216,10 +211,11 @@ export class DatabaseCellContainer extends SignalWatcher(
accessor gridCell!: TableGridCell;
@property({ attribute: false })
accessor view!: SingleView;
accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
get view() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -12,19 +12,19 @@ import {
} from '@blocksuite/icons/lit';
import { html } from 'lit';
import type { DataViewRenderer } from '../../../../core/data-view';
import { TableViewRowSelection } from '../../selection';
import type { TableSelectionController } from '../controller/selection';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export const openDetail = (
dataViewEle: DataViewRenderer,
tableViewLogic: VirtualTableViewUILogic,
rowId: string,
selection: TableSelectionController
) => {
const old = selection.selection;
selection.selection = undefined;
dataViewEle.openDetailPanel({
view: selection.host.props.view,
tableViewLogic.root.openDetailPanel({
view: tableViewLogic.view,
rowId: rowId,
onClose: () => {
selection.selection = old;
@@ -33,7 +33,7 @@ export const openDetail = (
};
export const popRowMenu = (
dataViewEle: DataViewRenderer,
tableViewLogic: VirtualTableViewUILogic,
ele: PopupTarget,
selectionController: TableSelectionController
) => {
@@ -55,7 +55,7 @@ export const popRowMenu = (
${CopyIcon()}
</div>`,
select: () => {
selectionController.host.clipboardController.copy();
tableViewLogic.clipboardController.copy();
},
}),
],
@@ -85,7 +85,7 @@ export const popRowMenu = (
name: 'Expand Row',
prefix: ExpandFullIcon(),
select: () => {
openDetail(dataViewEle, row.id, selectionController);
openDetail(tableViewLogic, row.id, selectionController);
},
}),
menu.group({

View File

@@ -7,14 +7,17 @@ import { nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { TableGridCell } from '../types.js';
import * as styles from './row-header-css.js';
export class TableRowHeader extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
get view() {
return this.tableViewLogic.view;
}
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
@@ -42,7 +45,7 @@ export class TableRowHeader extends SignalWatcher(
};
get selectionController() {
return this.tableView.selectionController;
return this.tableViewLogic.selectionController;
}
get rowSelected$() {
@@ -115,10 +118,7 @@ export class TableRowHeader extends SignalWatcher(
accessor gridCell!: TableGridCell;
@property({ attribute: false })
accessor view!: TableSingleView;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
}
declare global {

View File

@@ -5,8 +5,7 @@ import { effect } from '@preact/signals-core';
import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { TableGridCell } from '../types.js';
import * as styles from './row-header-css.js';
@@ -56,10 +55,7 @@ export class TableRowLast extends SignalWatcher(
accessor gridCell!: TableGridCell;
@property({ attribute: false })
accessor view!: TableSingleView;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
}
declare global {

View File

@@ -3,19 +3,27 @@ import {
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { computed, signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import type { TemplateResult } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import * as dv from '../../../core/common/dv-css.js';
import { dv } from '../../../core/common/dv-css.js';
import {
type GroupTrait,
groupTraitKey,
} from '../../../core/group-by/trait.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import {
type TableSingleView,
TableViewRowSelection,
@@ -26,9 +34,9 @@ import { TableClipboardController } from './controller/clipboard.js';
import { TableDragController } from './controller/drag.js';
import { TableHotkeysController } from './controller/hotkeys.js';
import { TableSelectionController } from './controller/selection.js';
import { TableGroupFooter } from './group/bottom/group-footer';
import { TableGroupHeader } from './group/top/group-header';
import { DatabaseCellContainer } from './row/cell';
import { TableGroupFooter } from './group/bottom/group-footer.js';
import { TableGroupHeader } from './group/top/group-header.js';
import { DatabaseCellContainer } from './row/cell.js';
import { TableRowHeader } from './row/row-header.js';
import { TableRowLast } from './row/row-last.js';
import * as styles from './table-view-css.js';
@@ -43,15 +51,83 @@ import {
GridVirtualScroll,
} from './virtual/virtual-scroll.js';
export class VirtualTableView extends DataViewBase<
export class VirtualTableViewUILogic extends DataViewUILogicBase<
TableSingleView,
TableViewSelectionWithType
> {
ui$ = signal<TableViewUI | undefined>();
clipboardController = new TableClipboardController(this);
dragController = new TableDragController(this);
hotkeysController = new TableHotkeysController(this);
selectionController = new TableSelectionController(this);
virtualScroll$ = signal<TableGrid>();
yScrollContainer: HTMLElement | undefined;
columns$ = computed(() => {
return [
{
id: 'row-header',
width: LEFT_TOOL_BAR_WIDTH,
},
...this.view.properties$.value.map(property => ({
id: property.id,
width: property.width$.value + 1,
})),
{
id: 'row-last',
width: 40,
},
];
});
groupTrait$ = computed(() => {
return this.view.traitGet(groupTraitKey);
});
groups$ = computed(() => {
const groupTrait = this.groupTrait$.value;
if (!groupTrait?.groupsDataList$.value) {
return [
{
id: '',
rows: this.view.rowIds$.value,
},
];
}
return groupTrait.groupsDataList$.value.map(group => ({
id: group.key,
rows: group.rows.map(v => v.rowId),
}));
});
clearSelection = () => {
this.selectionController.clear();
};
addRow = (position: InsertToPosition) => {
return this.view.rowAdd(position);
};
focusFirstCell = () => {
this.selectionController.focusFirstCell();
};
showIndicator = (evt: MouseEvent) => {
return this.dragController.showIndicator(evt) != null;
};
hideIndicator = () => {
this.dragController.dropPreview.remove();
};
moveTo = (id: string, evt: MouseEvent) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
};
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
@@ -80,13 +156,12 @@ export class VirtualTableView extends DataViewBase<
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
@@ -103,92 +178,8 @@ export class VirtualTableView extends DataViewBase<
</div>`;
};
selectionController = new TableSelectionController(this);
yScrollContainer: HTMLElement | undefined;
get expose(): DataViewInstance {
return {
clearSelection: () => {
this.selectionController.clear();
},
addRow: position => {
if (this.readonly) 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();
},
showIndicator: evt => {
return this.dragController.showIndicator(evt) != null;
},
hideIndicator: () => {
this.dragController.dropPreview.remove();
},
moveTo: (id, evt) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.props.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
},
getSelection: () => {
return this.selectionController.selection;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
private get readonly() {
return this.props.view.readonly$.value;
}
columns$ = computed(() => {
return [
{
id: 'row-header',
width: LEFT_TOOL_BAR_WIDTH,
},
...this.props.view.properties$.value.map(property => ({
id: property.id,
width: property.width$.value + 1,
})),
{
id: 'row-last',
width: 40,
},
];
});
groupTrait$ = computed(() => {
return this.props.view.traitGet(groupTraitKey);
});
groups$ = computed(() => {
const groupTrait = this.groupTrait$.value;
if (!groupTrait?.groupsDataList$.value) {
return [
{
id: '',
rows: this.props.view.rowIds$.value,
},
];
}
return groupTrait.groupsDataList$.value.map(group => ({
id: group.key,
rows: group.rows.map(v => v.rowId),
}));
});
virtualScroll$ = signal<TableGrid>();
private initVirtualScroll(yScrollContainer: HTMLElement) {
this.virtualScroll$.value = new GridVirtualScroll<
initVirtualScroll(yScrollContainer: HTMLElement, ui: TableViewUI) {
const virtualScroll = new GridVirtualScroll<
TableGroupData,
TableRowData,
TableCellData
@@ -213,7 +204,7 @@ export class VirtualTableView extends DataViewBase<
return row.cells$.value.some(cell => cell.data.hover$.value);
}),
selected$: computed(() => {
const selection = this.props.selection$.value;
const selection = this.selection$.value;
if (!selection || selection.selectionType !== 'row') {
return false;
}
@@ -234,34 +225,31 @@ export class VirtualTableView extends DataViewBase<
if (cell.columnId === 'row-header') {
wrapper.style.borderBottom = `1px solid ${cssVarV2.database.border}`;
const rowHeader = new TableRowHeader();
rowHeader.view = this.props.view;
rowHeader.gridCell = cell;
rowHeader.tableView = this;
rowHeader.tableViewLogic = this;
return rowHeader;
}
if (cell.columnId === 'row-last') {
const rowLast = new TableRowLast();
rowLast.view = this.props.view;
rowLast.gridCell = cell;
rowLast.tableView = this;
rowLast.tableViewLogic = this;
return rowLast;
}
const cellContainer = new DatabaseCellContainer();
cellContainer.view = this.props.view;
cellContainer.gridCell = cell;
cellContainer.tableView = this;
cellContainer.tableViewLogic = this;
return cellContainer;
},
createGroup: {
top: gridGroup => {
const groupHeader = new TableGroupHeader();
groupHeader.tableView = this;
groupHeader.tableViewLogic = this;
groupHeader.gridGroup = gridGroup;
return groupHeader;
},
bottom: gridGroup => {
const groupFooter = new TableGroupFooter();
groupFooter.tableView = this;
groupFooter.tableViewLogic = this;
groupFooter.gridGroup = gridGroup;
return groupFooter;
},
@@ -269,26 +257,40 @@ export class VirtualTableView extends DataViewBase<
fixedRowHeight$: signal(undefined),
yScrollContainer,
});
this.yScrollContainer = yScrollContainer;
this.virtualScroll$.value = virtualScroll;
requestAnimationFrame(() => {
const virtualScroll = this.virtualScroll$.value;
if (virtualScroll) {
virtualScroll.init();
this.disposables.add(() => virtualScroll.dispose());
ui.disposables.add(() => virtualScroll.dispose());
}
});
}
renderer = createUniComponentFromWebComponent(TableViewUI);
}
export class TableViewUI extends DataViewUIBase<VirtualTableViewUILogic> {
private renderTable() {
return this.virtualScroll$.value?.content;
return this.logic.virtualScroll$.value?.content;
}
override connectedCallback(): void {
super.connectedCallback();
this.initVirtualScroll(getScrollContainer(this, 'y') ?? document.body);
this.logic.ui$.value = this;
this.logic.clipboardController.hostConnected();
this.logic.dragController.hostConnected();
this.logic.hotkeysController.hostConnected();
this.logic.selectionController.hostConnected();
const scrollContainer = getScrollContainer(this, 'y') ?? document.body;
this.logic.initVirtualScroll(scrollContainer, this);
this.classList.add(styles.tableView);
}
override render() {
const vPadding = this.props.virtualPadding$.value;
override render(): TemplateResult {
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
@@ -298,11 +300,11 @@ export class VirtualTableView extends DataViewBase<
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
${renderUniLit(this.logic.root.config.headerWidget, {
dataViewLogic: this.logic,
})}
<div class="${styles.tableContainer}" style="${wrapperStyle}">
<div class="${styles.tableBlockTable}" @wheel="${this.onWheel}">
<div class="${styles.tableBlockTable}" @wheel="${this.logic.onWheel}">
<div class="${styles.tableContainer2}" style="${containerStyle}">
${this.renderTable()}
</div>
@@ -314,6 +316,6 @@ export class VirtualTableView extends DataViewBase<
declare global {
interface HTMLElementTagNameMap {
'affine-virtual-table': VirtualTableView;
'dv-table-view-ui-virtual': TableViewUI;
}
}

View File

@@ -61,8 +61,8 @@ export class BatchTaskManager {
private run(): void {
let totalBatchCount = this.totalBatchSize;
// let skipCount = 0;
// let tasksExecuted = false;
let skipCount = 0;
let tasksExecuted = false;
const runTaskArr = this.queues.map(() => 0);
for (let i = this.queues.length - 1; i >= 0; i--) {
const queue = this.queues[i];
@@ -82,22 +82,22 @@ export class BatchTaskManager {
if (result !== false) {
totalBatchCount--;
priorityBatchCount--;
// tasksExecuted = true;
tasksExecuted = true;
runTaskArr[i] = (runTaskArr[i] ?? 0) + 1;
}
}
}
// if (tasksExecuted) {
// console.log(
// 'run task count',
// ...runTaskArr,
// 'skip count',
// skipCount,
// 'total task count',
// ...this.queues.map(arr => arr.size)
// );
// }
if (tasksExecuted) {
console.log(
'run task count',
...runTaskArr,
'skip count',
skipCount,
'total task count',
...this.queues.map(arr => arr.size)
);
}
const hasRemainingTasks = this.queues.some(queue => !queue.isEmpty());

View File

@@ -9,19 +9,18 @@ import type {
CellRenderProps,
DataViewCellLifeCycle,
} from '../../../core/property/index.js';
import type { SingleView } from '../../../core/view-manager/single-view.js';
import {
TableViewAreaSelection,
type TableViewSelectionWithType,
} from '../selection';
import type { TableProperty } from '../table-view-manager.js';
import type { TableGroup } from './group.js';
export class DatabaseCellContainer extends SignalWatcher(
import type { TableViewUILogic } from './table-view-ui-logic.js';
export class TableViewCellContainer extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
affine-database-cell-container {
dv-table-view-cell-container {
display: flex;
align-items: start;
width: 100%;
@@ -30,16 +29,16 @@ export class DatabaseCellContainer extends SignalWatcher(
outline: none;
}
affine-database-cell-container * {
dv-table-view-cell-container * {
box-sizing: border-box;
}
affine-database-cell-container uni-lit > *:first-child {
dv-table-view-cell-container uni-lit > *:first-child {
padding: 6px;
}
`;
private readonly _cell = signal<DataViewCellLifeCycle>();
private readonly _cell$ = signal<DataViewCellLifeCycle>();
@property({ attribute: false })
accessor column!: TableProperty;
@@ -55,7 +54,7 @@ export class DatabaseCellContainer extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const selectionView = this.selectionView;
const selectionView = this.selectionController;
if (selectionView) {
const selection = selectionView.selection;
if (selection && this.isSelected(selection) && editing) {
@@ -81,20 +80,15 @@ export class DatabaseCellContainer extends SignalWatcher(
};
get cell(): DataViewCellLifeCycle | undefined {
return this._cell.value;
return this._cell$.value;
}
private get groupKey() {
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
}
private get selectionView() {
return this.closest('affine-database-table')?.selectionController;
}
get table() {
const table = this.closest('affine-database-table');
return table;
private get selectionController() {
return this.tableViewLogic.selectionController;
}
override connectedCallback() {
@@ -134,7 +128,7 @@ export class DatabaseCellContainer extends SignalWatcher(
};
return renderUniLit(view, props, {
ref: this._cell,
ref: this._cell$,
style: {
display: 'contents',
},
@@ -152,12 +146,16 @@ export class DatabaseCellContainer extends SignalWatcher(
@property({ attribute: false })
accessor rowIndex!: number;
get view() {
return this.tableViewLogic.view;
}
@property({ attribute: false })
accessor view!: SingleView;
accessor tableViewLogic!: TableViewUILogic;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-cell-container': DatabaseCellContainer;
'dv-table-view-cell-container': TableViewCellContainer;
}
}

View File

@@ -9,7 +9,7 @@ import {
type TableViewSelection,
type TableViewSelectionWithType,
} from '../../selection';
import type { DataViewTable } from '../table-view.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table';
type JsonAreaData = string[][];
@@ -20,9 +20,7 @@ export class TableClipboardController implements ReactiveController {
tableSelection: TableViewSelectionWithType,
isCut = false
) => {
const table = this.host;
const area = getSelectedArea(tableSelection, table);
const area = getSelectedArea(tableSelection, this.logic);
if (!area) {
return;
}
@@ -44,7 +42,7 @@ export class TableClipboardController implements ReactiveController {
}
}
if (deleteRows.length) {
this.props.view.rowsDelete(deleteRows);
this.logic.view.rowsDelete(deleteRows);
}
}
this.clipboard
@@ -79,12 +77,11 @@ export class TableClipboardController implements ReactiveController {
private readonly _onPaste = async (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw;
event.stopPropagation();
const view = this.host;
const clipboardData = event.clipboardData;
if (!clipboardData) return;
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selectionController.selection;
if (TableViewRowSelection.is(tableSelection)) {
return;
}
@@ -97,7 +94,7 @@ export class TableClipboardController implements ReactiveController {
if (dataString) {
// If internal format data exists, use it
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
pasteToCells(view, jsonAreaData, tableSelection);
pasteToCells(this.logic, jsonAreaData, tableSelection);
return true;
}
} catch {
@@ -115,7 +112,7 @@ export class TableClipboardController implements ReactiveController {
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
if (rows.length > 0) {
pasteToCells(view, rows, tableSelection);
pasteToCells(this.logic, rows, tableSelection);
}
}
}
@@ -124,27 +121,29 @@ export class TableClipboardController implements ReactiveController {
};
private get clipboard() {
return this.props.clipboard;
return this.logic.root.config.clipboard;
}
private get notification() {
return this.props.notification;
}
get props() {
return this.host.props;
return this.logic.root.config.notification;
}
private get readonly() {
return this.props.view.readonly$.value;
return this.logic.view.readonly$.value;
}
constructor(public host: DataViewTable) {
host.addController(this);
constructor(public logic: TableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
get selectionController() {
return this.logic.selectionController;
}
copy() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selectionController.selection;
if (!tableSelection) {
return;
}
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
}
cut() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selectionController.selection;
if (!tableSelection) {
return;
}
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
}
hostConnected() {
this.host.disposables.add(
this.props.handleEvent('copy', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.host?.disposables.add(
this.logic.handleEvent('copy', _ctx => {
const tableSelection = this.selectionController.selection;
if (!tableSelection) return false;
this._onCopy(tableSelection);
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('cut', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.host?.disposables.add(
this.logic.handleEvent('cut', _ctx => {
const tableSelection = this.selectionController.selection;
if (!tableSelection) return false;
this._onCut(tableSelection);
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('paste', ctx => {
this.host?.disposables.add(
this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false;
this._onPaste(ctx).catch(console.error);
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
function getSelectedArea(
selection: TableViewSelection,
table: DataViewTable
table: TableViewUILogic
): SelectedArea | undefined {
const view = table.props.view;
const view = table.view;
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rows(selection)
.map(row => {
@@ -283,7 +282,7 @@ function getTargetRangeFromSelection(
}
function pasteToCells(
table: DataViewTable,
table: TableViewUILogic,
rows: JsonAreaData,
selection: TableViewAreaSelection
) {

View File

@@ -8,7 +8,7 @@ import * as Y from 'yjs';
import { t } from '../../../../core/index.js';
import type { TableViewAreaSelection } from '../../selection';
import type { DataViewTable } from '../table-view.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class DragToFillElement extends ShadowlessElement {
static override styles = css`
@@ -49,12 +49,12 @@ export class DragToFillElement extends ShadowlessElement {
}
export function fillSelectionWithFocusCellData(
host: DataViewTable,
logic: TableViewUILogic,
selection: TableViewAreaSelection
) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection;
const focusCell = host.selectionController.getCellContainer(
const focusCell = logic.selectionController.getCellContainer(
groupKey,
focus.rowIndex,
focus.columnIndex
@@ -78,7 +78,7 @@ export function fillSelectionWithFocusCellData(
for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue;
const cellContainer = host.selectionController.getCellContainer(
const cellContainer = logic.selectionController.getCellContainer(
groupKey,
i,
draggingColIdx

View File

@@ -5,7 +5,7 @@ import type { ReactiveController } from 'lit';
import { startDrag } from '../../../../core/utils/drag.js';
import { TableRowView } from '../row/row.js';
import type { DataViewTable } from '../table-view.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class TableDragController implements ReactiveController {
dragStart = (rowView: TableRowView, evt: PointerEvent) => {
@@ -32,8 +32,8 @@ export class TableDragController implements ReactiveController {
onDrag: () => undefined,
onMove: evt => {
preview.display(evt.x - offsetLeft, evt.y - offsetTop);
if (!this.host.contains(evt.target as Node)) {
const callback = this.host.props.onDrag;
if (!this.host?.contains(evt.target as Node)) {
const callback = this.logic.root.config.onDrag;
if (callback) {
this.dropPreview.remove();
return {
@@ -66,7 +66,7 @@ export class TableDragController implements ReactiveController {
return;
}
if (result.type === 'self') {
const row = this.host.props.view.rowGetOrCreate(rowView.rowId);
const row = this.logic.view.rowGetOrCreate(rowView.rowId);
row.move(result.position, fromGroup, result.groupKey);
}
},
@@ -88,9 +88,9 @@ export class TableDragController implements ReactiveController {
| undefined => {
const y = evt.y;
const tableRect = this.host
.querySelector('affine-data-view-table-group')
?.querySelector('affine-data-view-table-group')
?.getBoundingClientRect();
const rows = this.host.querySelectorAll('data-view-table-row');
const rows = this.host?.querySelectorAll('data-view-table-row');
if (!rows || !tableRect || y < tableRect.top) {
return;
}
@@ -124,21 +124,23 @@ export class TableDragController implements ReactiveController {
return position;
};
constructor(private readonly host: DataViewTable) {
this.host.addController(this);
constructor(private readonly logic: TableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
if (this.host.props.view.readonly$.value) {
return;
}
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
this.host?.disposables.add(
this.logic.handleEvent('dragStart', context => {
if (this.logic.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (
target instanceof Element &&
this.host.contains(target) &&
this.host?.contains(target) &&
target.closest('.data-view-table-view-drag-handler')
) {
event.preventDefault();
@@ -158,10 +160,9 @@ export class TableDragController implements ReactiveController {
const createDragPreview = (row: TableRowView, x: number, y: number) => {
const div = document.createElement('div');
const cloneRow = new TableRowView();
cloneRow.view = row.view;
cloneRow.rowIndex = row.rowIndex;
cloneRow.rowId = row.rowId;
cloneRow.dataViewEle = row.dataViewEle;
cloneRow.tableViewLogic = row.tableViewLogic;
div.append(cloneRow);
div.className = 'with-data-view-css-variable';
div.style.width = `${row.getBoundingClientRect().width}px`;

View File

@@ -3,20 +3,22 @@ import type { ReactiveController } from 'lit';
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
import { popRowMenu } from '../menu.js';
import type { DataViewTable } from '../table-view.js';
import type { TableViewUILogic } from '../table-view-ui-logic';
export class TableHotkeysController implements ReactiveController {
get selectionController() {
return this.host.selectionController;
return this.logic.selectionController;
}
constructor(private readonly host: DataViewTable) {
this.host.addController(this);
constructor(private readonly logic: TableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
this.host.disposables.add(
this.host.props.bindHotkey({
this.host?.disposables.add(
this.logic.bindHotkey({
Backspace: () => {
const selection = this.selectionController.selection;
if (!selection) {
@@ -25,7 +27,7 @@ export class TableHotkeysController implements ReactiveController {
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rowsIds(selection);
this.selectionController.selection = undefined;
this.host.props.view.rowsDelete(rows);
this.logic.view.rowsDelete(rows);
return;
}
const {
@@ -334,14 +336,14 @@ export class TableHotkeysController implements ReactiveController {
context.get('keyboardState').raw.preventDefault();
this.selectionController.selection = TableViewRowSelection.create({
rows:
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap(
this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
group =>
group?.rows.map(row => ({
groupKey: group.key,
id: row.rowId,
})) ?? []
) ??
this.host.props.view.rows$.value.map(row => ({
this.logic.view.rows$.value.map(row => ({
groupKey: undefined,
id: row.rowId,
})),
@@ -377,7 +379,7 @@ export class TableHotkeysController implements ReactiveController {
rows: [row],
});
popRowMenu(
this.host.props.dataViewEle,
this.logic,
popupTargetFromElement(cell),
this.selectionController
);

View File

@@ -18,10 +18,10 @@ import {
type TableViewSelection,
type TableViewSelectionWithType,
} from '../../selection';
import type { DatabaseCellContainer } from '../cell.js';
import type { TableViewCellContainer } from '../cell.js';
import type { TableGroup } from '../group.js';
import type { TableRowView } from '../row/row.js';
import type { DataViewTable } from '../table-view.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
import {
DragToFillElement,
fillSelectionWithFocusCellData,
@@ -87,41 +87,44 @@ export class TableSelectionController implements ReactiveController {
);
const cell = container?.cell;
const isEditing = cell ? cell.beforeEnterEditMode() : true;
this.host.props.setSelection({
this.logic.setSelection({
...selection,
isEditing,
});
} else {
this.host.props.setSelection(selection);
this.logic.setSelection(selection);
}
}
get tableContainer() {
return this.host.querySelector('.affine-database-table-container');
return this.logic.tableContainer$.value;
}
get view() {
return this.host.props.view;
return this.logic.view;
}
get viewData() {
return this.view;
}
constructor(public host: DataViewTable) {
host.addController(this);
constructor(public logic: TableViewUILogic) {
this.__selectionElement = new SelectionElement();
this.__selectionElement.controller = this;
}
get host() {
return this.logic.ui$.value;
}
private clearSelection() {
this.host.props.setSelection();
this.logic.setSelection();
}
private handleDragEvent() {
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) {
this.host?.disposables.add(
this.logic.handleEvent('dragStart', context => {
if (this.logic.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
@@ -152,8 +155,8 @@ export class TableSelectionController implements ReactiveController {
}
private handleSelectionChange() {
this.host.disposables.add(
this.host.props.selection$.subscribe(tableSelection => {
this.host?.disposables.add(
this.logic.selection$.subscribe(tableSelection => {
if (!this.isValidSelection(tableSelection)) {
this.selection = undefined;
return;
@@ -238,7 +241,7 @@ export class TableSelectionController implements ReactiveController {
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
: this.view.rows$.value;
requestAnimationFrame(() => {
const index = this.host.props.view.properties$.value.findIndex(
const index = this.logic.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
this.selection = TableViewAreaSelection.create({
@@ -254,14 +257,14 @@ export class TableSelectionController implements ReactiveController {
private resolveDragStartTarget(
target: HTMLElement
): [cell: DatabaseCellContainer | null, fillValues: boolean] {
let cell: DatabaseCellContainer | null;
): [cell: TableViewCellContainer | null, fillValues: boolean] {
let cell: TableViewCellContainer | null;
const fillValues = !!target.dataset.dragToFill;
if (fillValues) {
const focusCellContainer = this.getFocusCellContainer();
cell = focusCellContainer ?? null;
} else {
cell = target.closest('affine-database-cell-container');
cell = target.closest('dv-table-view-cell-container');
}
return [cell, fillValues];
}
@@ -295,7 +298,7 @@ export class TableSelectionController implements ReactiveController {
const rows = this.rows(groupKey);
const cells = rows
?.item(0)
.querySelectorAll('affine-database-cell-container');
.querySelectorAll('dv-table-view-cell-container');
return (x1: number, x2: number, y1: number, y2: number) => {
const rowOffsets: number[] = Array.from(rows ?? []).map(
@@ -394,7 +397,7 @@ export class TableSelectionController implements ReactiveController {
?.querySelectorAll('data-view-table-row') ?? []
);
const cells = Array.from(
row?.querySelectorAll('affine-database-cell-container') ?? []
row?.querySelectorAll('dv-table-view-cell-container') ?? []
);
if (!row || !rows || !cells) {
return;
@@ -432,7 +435,7 @@ export class TableSelectionController implements ReactiveController {
}
}
rows[rowIndex]
?.querySelectorAll('affine-database-cell-container')
?.querySelectorAll('dv-table-view-cell-container')
?.item(columnIndex)
?.selectCurrentCell(false);
}
@@ -441,10 +444,10 @@ export class TableSelectionController implements ReactiveController {
groupKey: string | undefined,
rowIndex: number,
columnIndex: number
): DatabaseCellContainer | undefined {
): TableViewCellContainer | undefined {
const row = this.rows(groupKey)?.item(rowIndex);
return row
?.querySelectorAll('affine-database-cell-container')
?.querySelectorAll('dv-table-view-cell-container')
.item(columnIndex);
}
@@ -479,7 +482,7 @@ export class TableSelectionController implements ReactiveController {
if (!topRow || !bottomRow) {
return;
}
const topCells = topRow.querySelectorAll('affine-database-cell-container');
const topCells = topRow.querySelectorAll('dv-table-view-cell-container');
const leftCell = topCells.item(left);
const rightCell = topCells.item(right);
if (!leftCell || !rightCell) {
@@ -752,7 +755,7 @@ export class TableSelectionController implements ReactiveController {
const max =
(this.rows(newSelection.groupKey)
?.item(0)
.querySelectorAll('affine-database-cell-container').length ?? 0) - 1;
.querySelectorAll('dv-table-view-cell-container').length ?? 0) - 1;
newSelection.columnsSelection.end = Math.min(
max,
newSelection.columnsSelection.end + 1
@@ -810,7 +813,7 @@ export class TableSelectionController implements ReactiveController {
startDrag(
evt: PointerEvent,
cell: DatabaseCellContainer,
cell: TableViewCellContainer,
fillValues?: boolean
) {
const groupKey = cell.closest<TableGroup>('affine-data-view-table-group')
@@ -882,7 +885,7 @@ export class TableSelectionController implements ReactiveController {
if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData(
this.host,
this.logic,
TableViewAreaSelection.create({
groupKey: groupKey,
rowsSelection: selection.row,
@@ -997,7 +1000,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
selectionRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
get selection$() {
return this.controller.host.props.selection$;
return this.controller.logic.selection$;
}
clearAreaStyle() {
@@ -1049,7 +1052,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
this.cancelSelectionUpdate();
if (
selection?.selectionType === 'area' &&
!this.controller.host.props.view.readonly$.value
!this.controller.logic.view.readonly$.value
) {
this.updateAreaSelectionStyle(
selection.groupKey,

View File

@@ -1,4 +1,4 @@
import { DatabaseCellContainer } from './cell.js';
import { TableViewCellContainer } from './cell.js';
import { DragToFillElement } from './controller/drag-to-fill.js';
import { SelectionElement } from './controller/selection.js';
import { TableGroup } from './group.js';
@@ -9,15 +9,12 @@ import { DatabaseNumberFormatBar } from './header/number-format-bar.js';
import { TableVerticalIndicator } from './header/vertical-indicator.js';
import { TableRowView } from './row/row.js';
import { RowSelectCheckbox } from './row/row-select-checkbox.js';
import { DataViewTable } from './table-view.js';
import { TableViewUI } from './table-view-ui-logic.js';
export function pcEffects() {
customElements.define('affine-database-table', DataViewTable);
customElements.define('dv-table-view-ui', TableViewUI);
customElements.define('affine-data-view-table-group', TableGroup);
customElements.define(
'affine-database-cell-container',
DatabaseCellContainer
);
customElements.define('dv-table-view-cell-container', TableViewCellContainer);
customElements.define('affine-database-column-header', DatabaseColumnHeader);
customElements.define(
'affine-data-view-column-preview',

View File

@@ -12,7 +12,6 @@ import { css, html, unsafeCSS } from 'lit';
import { property, query } 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';
@@ -21,10 +20,9 @@ import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
import { linearMove } from '../../../core/utils/wc-dnd/utils/linear-move.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import { TableViewAreaSelection } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { DataViewColumnPreview } from './header/column-renderer.js';
import { getVerticalIndicator } from './header/vertical-indicator.js';
import type { DataViewTable } from './table-view.js';
import type { TableViewUILogic } from './table-view-ui-logic.js';
const styles = css`
affine-data-view-table-group:hover .group-header-op {
@@ -71,7 +69,7 @@ export class TableGroup extends SignalWatcher(
private readonly clickAddRow = () => {
this.view.rowAdd('end', this.group?.key);
const selectionController = this.viewEle.selectionController;
const selectionController = this.tableViewLogic.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
@@ -90,7 +88,7 @@ export class TableGroup extends SignalWatcher(
private readonly clickAddRowInStart = () => {
this.view.rowAdd('start', this.group?.key);
const selectionController = this.viewEle.selectionController;
const selectionController = this.tableViewLogic.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
@@ -152,9 +150,6 @@ export class TableGroup extends SignalWatcher(
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor view!: TableSingleView;
dndContext = createDndContext({
activators: defaultActivators,
container: this,
@@ -187,6 +182,7 @@ export class TableGroup extends SignalWatcher(
preview.column = column;
preview.group = this.group;
preview.container = this;
preview.tableViewLogic = this.tableViewLogic;
preview.style.position = 'absolute';
preview.style.zIndex = '999';
const scale = this.dndContext.scale$.value;
@@ -246,7 +242,7 @@ export class TableGroup extends SignalWatcher(
return html`
<affine-database-column-header
.renderGroupHeader="${this.renderGroupHeader}"
.tableViewManager="${this.view}"
.tableViewLogic="${this.tableViewLogic}"
></affine-database-column-header>
<div class="affine-database-block-rows">
${repeat(
@@ -256,8 +252,7 @@ export class TableGroup extends SignalWatcher(
return html` <data-view-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}"
></data-view-table-row>`;
@@ -278,7 +273,10 @@ export class TableGroup 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
.tableViewLogic="${this.tableViewLogic}"
.group="${this.group}"
>
</affine-database-column-stats>
`;
}
@@ -292,14 +290,15 @@ export class TableGroup extends SignalWatcher(
return this.renderRows(this.rows);
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null;
@property({ attribute: false })
accessor viewEle!: DataViewTable;
accessor tableViewLogic!: TableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -9,8 +9,10 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js';
import { cellDivider } from '../../styles.js';
import type { TableGroup } from '../group.js';
import { tableStyle } from '../table-view-style';
import { type TableViewUILogic } from '../table-view-ui-logic.js';
import { styles } from './styles.js';
export class DatabaseColumnHeader extends SignalWatcher(
@@ -31,7 +33,6 @@ export class DatabaseColumnHeader extends SignalWatcher(
editLastColumnTitle = () => {
const columns = this.querySelectorAll('affine-database-header-column');
const column = columns.item(columns.length - 1);
column.scrollIntoView({ block: 'nearest', inline: 'nearest' });
column.editTitle();
};
@@ -88,7 +89,7 @@ export class DatabaseColumnHeader extends SignalWatcher(
<div class="affine-database-column-header database-row">
${this.readonly
? nothing
: html`<div class="data-view-table-left-bar"></div>`}
: html`<div class="${tableStyle.leftToolBarStyle}"></div>`}
${repeat(
this.tableViewManager.properties$.value,
column => column.id,
@@ -104,9 +105,9 @@ export class DatabaseColumnHeader extends SignalWatcher(
data-column-index="${index}"
class="affine-database-column database-cell"
.column="${column}"
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
></affine-database-header-column>
<div class="cell-divider" style="height: auto;"></div>
<div class="${cellDivider}" style="height: auto;"></div>
`;
}
)}
@@ -128,7 +129,11 @@ export class DatabaseColumnHeader extends SignalWatcher(
accessor scaleDiv!: HTMLDivElement;
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: TableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -13,6 +13,7 @@ import type {
TableProperty,
TableSingleView,
} from '../../table-view-manager.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class DataViewColumnPreview extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
`;
get tableViewManager(): TableSingleView {
return this.column.view as TableSingleView;
return this.tableViewLogic.view;
}
private renderGroup(rows: Row[]) {
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
)};box-shadow: var(--affine-shadow-2);"
>
<affine-database-header-column
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.column="${this.column}"
></affine-database-header-column>
${repeat(rows, (id, index) => {
const height = this.container.querySelector(
`affine-database-cell-container[data-row-id="${id}"]`
`dv-table-view-cell-container[data-row-id="${id}"]`
)?.clientHeight;
const style = styleMap({
height: height + 'px',
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
)}"
>
<div style="${style}">
<affine-database-cell-container
<dv-table-view-cell-container
.column="${this.column}"
.view="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${id}"
.columnId="${this.column.id}"
.rowIndex="${index}"
.columnIndex="${columnIndex}"
></affine-database-cell-container>
></dv-table-view-cell-container>
</div>
</div>`;
})}
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor tableViewLogic!: TableViewUILogic;
}
declare global {

View File

@@ -45,10 +45,8 @@ import {
ShowQuickSettingBarKey,
} from '../../../../widget-presets/quick-setting-bar/context.js';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js';
import type {
TableProperty,
TableSingleView,
} from '../../table-view-manager.js';
import type { TableProperty } from '../../table-view-manager.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
import {
getTableGroupRect,
getVerticalIndicator,
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
const sortUtils = createSortUtils(
sortTrait,
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {})
this.tableViewLogic.eventTrace
);
const sortList = sortUtils.sortList$.value;
const existingIndex = sortList.findIndex(
@@ -398,10 +396,10 @@ export class DatabaseHeaderColumn extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
const table = this.closest('affine-database-table');
const table = this.closest('dv-table-view-ui');
if (table) {
this.disposables.add(
table.props.handleEvent('dragStart', context => {
table.logic.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) {
return;
}
@@ -481,7 +479,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: TableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
function numberFormatConfig(column: Property): MenuConfig {

View File

@@ -12,19 +12,19 @@ import {
} from '@blocksuite/icons/lit';
import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { TableViewRowSelection } from '../selection';
import type { TableSelectionController } from './controller/selection.js';
import type { TableViewUILogic } from './table-view-ui-logic.js';
export const openDetail = (
dataViewEle: DataViewRenderer,
tableViewLogic: TableViewUILogic,
rowId: string,
selection: TableSelectionController
) => {
const old = selection.selection;
selection.selection = undefined;
dataViewEle.openDetailPanel({
view: selection.host.props.view,
tableViewLogic.root.openDetailPanel({
view: selection.logic.view,
rowId: rowId,
onClose: () => {
selection.selection = old;
@@ -33,7 +33,7 @@ export const openDetail = (
};
export const popRowMenu = (
dataViewEle: DataViewRenderer,
tableViewLogic: TableViewUILogic,
ele: PopupTarget,
selectionController: TableSelectionController
) => {
@@ -55,7 +55,7 @@ export const popRowMenu = (
${CopyIcon()}
</div>`,
select: () => {
selectionController.host.clipboardController.copy();
selectionController.logic.clipboardController.copy();
},
}),
],
@@ -85,7 +85,7 @@ export const popRowMenu = (
name: 'Expand Row',
prefix: ExpandFullIcon(),
select: () => {
openDetail(dataViewEle, row.id, selectionController);
openDetail(tableViewLogic, row.id, selectionController);
},
}),
menu.group({

View File

@@ -1,15 +1,13 @@
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { CheckBoxCheckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { computed } from '@preact/signals-core';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import {
TableViewRowSelection,
type TableViewSelectionWithType,
} from '../../selection';
import { TableViewRowSelection } from '../../selection';
import type { TableViewUILogic } from '../table-view-ui-logic';
export class RowSelectCheckbox extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -42,10 +40,10 @@ export class RowSelectCheckbox extends SignalWatcher(
accessor rowId!: string;
@property({ attribute: false })
accessor selection!: ReadonlySignal<TableViewSelectionWithType | undefined>;
accessor tableViewLogic!: TableViewUILogic;
isSelected$ = computed(() => {
const selection = this.selection.value;
const selection = this.tableViewLogic.selection$.value;
if (!selection || selection.selectionType !== 'row') {
return false;
}
@@ -58,7 +56,7 @@ export class RowSelectCheckbox extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
this.disposables.addFromEvent(this, 'click', () => {
this.closest('affine-database-table')?.selectionController.toggleRow(
this.tableViewLogic.selectionController.toggleRow(
this.rowId,
this.groupKey
);

View File

@@ -9,14 +9,14 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../../core/data-view.js';
import {
TableViewRowSelection,
type TableViewSelection,
} from '../../selection';
import type { TableSingleView } from '../../table-view-manager.js';
import { cellDivider } from '../../styles';
import type { TableGroup } from '../group.js';
import { openDetail, popRowMenu } from '../menu.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class TableRowView extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -138,7 +138,7 @@ export class TableRowView extends SignalWatcher(
}
e.preventDefault();
const ele = e.target as HTMLElement;
const cell = ele.closest('affine-database-cell-container');
const cell = ele.closest('dv-table-view-cell-container');
const row = { id: this.rowId, groupKey: this.groupKey };
if (!TableViewRowSelection.includes(selection.selection, row)) {
selection.selection = TableViewRowSelection.create({
@@ -150,7 +150,7 @@ export class TableRowView extends SignalWatcher(
(e.target as HTMLElement).closest('.database-cell') ?? // for last add btn cell
(e.target as HTMLElement);
popRowMenu(this.dataViewEle, popupTargetFromElement(target), selection);
popRowMenu(this.tableViewLogic, popupTargetFromElement(target), selection);
};
setSelection = (selection?: TableViewSelection) => {
@@ -164,7 +164,7 @@ export class TableRowView extends SignalWatcher(
}
get selectionController() {
return this.closest('affine-database-table')?.selectionController;
return this.tableViewLogic.selectionController;
}
override connectedCallback() {
@@ -193,7 +193,7 @@ export class TableRowView extends SignalWatcher(
></div>
</div>
<row-select-checkbox
.selection="${this.dataViewEle.config.selection$}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${this.rowId}"
.groupKey="${this.groupKey}"
></row-select-checkbox>
@@ -212,7 +212,11 @@ export class TableRowView extends SignalWatcher(
rows: [{ id: this.rowId, groupKey: this.groupKey }],
})
);
openDetail(this.dataViewEle, this.rowId, this.selectionController);
openDetail(
this.tableViewLogic,
this.rowId,
this.selectionController
);
};
const openMenu = (e: MouseEvent) => {
if (!this.selectionController) {
@@ -234,20 +238,20 @@ export class TableRowView extends SignalWatcher(
);
}
popRowMenu(
this.dataViewEle,
this.tableViewLogic,
popupTargetFromElement(ele),
this.selectionController
);
};
return html`
<div style="display: flex;">
<affine-database-cell-container
<dv-table-view-cell-container
class="database-cell"
style=${styleMap({
width: `${column.width$.value}px`,
border: i === 0 ? 'none' : undefined,
})}
.view="${view}"
.tableViewLogic="${this.tableViewLogic}"
.column="${column}"
.rowId="${this.rowId}"
data-row-id="${this.rowId}"
@@ -258,8 +262,8 @@ export class TableRowView extends SignalWatcher(
.columnIndex="${i}"
data-column-index="${i}"
>
</affine-database-cell-container>
<div class="cell-divider"></div>
</dv-table-view-cell-container>
<div class="${cellDivider}"></div>
</div>
${!column.readonly$.value &&
column.view.mainProperties$.value.titleColumn === column.id
@@ -282,7 +286,7 @@ export class TableRowView extends SignalWatcher(
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
accessor tableViewLogic!: TableViewUILogic;
@property({ attribute: false })
accessor rowId!: string;
@@ -290,8 +294,9 @@ export class TableRowView extends SignalWatcher(
@property({ attribute: false })
accessor rowIndex!: number;
@property({ attribute: false })
accessor view!: TableSingleView;
get view() {
return this.tableViewLogic.view;
}
}
declare global {

View File

@@ -0,0 +1,112 @@
import { css } from '@emotion/css';
import { LEFT_TOOL_BAR_WIDTH } from '../consts';
export const tableViewStyle = css({
position: 'relative',
display: 'flex',
flexDirection: 'column',
'& *': {
boxSizing: 'border-box',
},
});
export const tableWrapperStyle = css({
overflowY: 'auto',
});
export const tableScrollContainerStyle = css({
position: 'relative',
width: '100%',
paddingBottom: '4px',
zIndex: 1,
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)',
},
'.affine-database-table-container': {
position: 'relative',
width: 'fit-content',
minWidth: '100%',
},
});
export const tableGroupsContainerStyle = css({
display: 'flex',
flexDirection: 'column',
gap: '16px',
});
export const addGroupStyle = css({
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '6px 12px 6px 8px',
color: 'var(--affine-text-secondary-color)',
fontSize: '12px',
lineHeight: '20px',
position: 'sticky',
left: `${LEFT_TOOL_BAR_WIDTH}px`,
borderRadius: '8px',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});
export const addGroupIconStyle = css({
display: 'flex',
width: '16px',
height: '16px',
'& svg': {
width: '16px',
height: '16px',
fill: 'var(--affine-icon-color)',
},
});
const cellDividerStyle = css({
width: '1px',
height: '100%',
backgroundColor: 'var(--affine-border-color)',
});
const leftToolBarStyle = css({
display: 'flex',
alignItems: 'center',
position: 'sticky',
zIndex: 1,
left: 0,
width: `${LEFT_TOOL_BAR_WIDTH}px`,
flexShrink: 0,
});
export const tableStyle = {
leftToolBarStyle,
cellDividerStyle,
};

View File

@@ -0,0 +1,220 @@
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 { signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { ref } from 'lit/directives/ref.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 { GroupTrait } from '../../../core/group-by/trait.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { TableClipboardController } from './controller/clipboard.js';
import { TableDragController } from './controller/drag.js';
import { TableHotkeysController } from './controller/hotkeys.js';
import { TableSelectionController } from './controller/selection.js';
import {
addGroupIconStyle,
addGroupStyle,
tableGroupsContainerStyle,
tableScrollContainerStyle,
tableViewStyle,
tableWrapperStyle,
} from './table-view-style';
export class TableViewUILogic extends DataViewUILogicBase<
TableSingleView,
TableViewSelectionWithType
> {
ui$ = signal<TableViewUI>();
scrollContainer$ = signal<HTMLDivElement>();
tableContainer$ = signal<HTMLDivElement>();
clipboardController = new TableClipboardController(this);
dragController = new TableDragController(this);
hotkeysController = new TableHotkeysController(this);
selectionController = new TableSelectionController(this);
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) != null;
};
hideIndicator = () => {
this.dragController.dropPreview.remove();
};
moveTo = (id: string, evt: MouseEvent) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
};
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="display:flex;">
<div class="${addGroupStyle}" @click="${add}">
<div class="${addGroupIconStyle}">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
renderer = createUniComponentFromWebComponent(TableViewUI);
}
export class TableViewUI extends DataViewUIBase<TableViewUILogic> {
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('affine-database-table', tableViewStyle);
this.dataset['testid'] = 'dv-table-view';
}
private renderTable() {
const groups = this.logic.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div class="${tableGroupsContainerStyle}">
${repeat(
groups,
v => v.key,
group => {
return html` <affine-data-view-table-group
data-group-key="${group.key}"
.tableViewLogic="${this.logic}"
.group="${group}"
></affine-data-view-table-group>`;
}
)}
${this.logic.renderAddGroup(this.logic.view.groupTrait)}
</div>
`;
}
return html` <affine-data-view-table-group
.tableViewLogic="${this.logic}"
></affine-data-view-table-group>`;
}
override render(): TemplateResult {
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${this.logic.headerWidget
? renderUniLit(this.logic.headerWidget, {
dataViewLogic: this.logic,
})
: ''}
<div class="${tableWrapperStyle}" style="${wrapperStyle}">
<div
${ref(this.logic.scrollContainer$)}
class="${tableScrollContainerStyle}"
@wheel="${this.logic.onWheel}"
>
<div
${ref(this.logic.tableContainer$)}
class="affine-database-table-container"
style="${containerStyle}"
>
${this.renderTable()}
</div>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'dv-table-view-ui': TableViewUI;
}
}

View File

@@ -1,306 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, unsafeCSS } 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 { GroupTrait } from '../../../core/group-by/trait.js';
import type { DataViewInstance } from '../../../core/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { TableClipboardController } from './controller/clipboard.js';
import { TableDragController } from './controller/drag.js';
import { TableHotkeysController } from './controller/hotkeys.js';
import { TableSelectionController } from './controller/selection.js';
const styles = css`
affine-database-table {
position: relative;
display: flex;
flex-direction: column;
}
affine-database-table * {
box-sizing: border-box;
}
.affine-database-table {
overflow-y: auto;
}
.affine-database-block-title-container {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin: 2px 0 2px;
}
.affine-database-block-table {
position: relative;
width: 100%;
padding-bottom: 4px;
z-index: 1;
overflow-x: scroll;
overflow-y: hidden;
}
.affine-database-block-table:hover {
padding-bottom: 0px;
}
.affine-database-block-table::-webkit-scrollbar {
-webkit-appearance: none;
display: block;
}
.affine-database-block-table::-webkit-scrollbar:horizontal {
height: 4px;
}
.affine-database-block-table::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: transparent;
}
.affine-database-block-table:hover::-webkit-scrollbar:horizontal {
height: 8px;
}
.affine-database-block-table:hover::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: var(--affine-black-30);
}
.affine-database-block-table:hover::-webkit-scrollbar-track {
background-color: var(--affine-hover-color);
}
.affine-database-table-container {
position: relative;
width: fit-content;
min-width: 100%;
}
.affine-database-block-tag-circle {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.affine-database-block-tag {
display: inline-flex;
border-radius: 11px;
align-items: center;
padding: 0 8px;
cursor: pointer;
}
.cell-divider {
width: 1px;
height: 100%;
background-color: ${unsafeCSS(cssVarV2.layer.insideBorder.border)};
}
.data-view-table-left-bar {
display: flex;
align-items: center;
position: sticky;
z-index: 1;
left: 0;
width: ${LEFT_TOOL_BAR_WIDTH}px;
flex-shrink: 0;
}
.affine-database-block-rows {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
`;
export class DataViewTable extends DataViewBase<
TableSingleView,
TableViewSelectionWithType
> {
static override styles = styles;
clipboardController = new TableClipboardController(this);
dragController = new TableDragController(this);
hotkeysController = new TableHotkeysController(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 = (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.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div style="display:flex;">
<div
class="dv-hover dv-round-8"
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
@click="${add}"
>
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
selectionController = new TableSelectionController(this);
get expose(): DataViewInstance {
return {
clearSelection: () => {
this.selectionController.clear();
},
addRow: position => {
if (this.readonly) 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();
},
showIndicator: evt => {
return this.dragController.showIndicator(evt) != null;
},
hideIndicator: () => {
this.dragController.dropPreview.remove();
},
moveTo: (id, evt) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.props.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
},
getSelection: () => {
return this.selectionController.selection;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
private get readonly() {
return this.props.view.readonly$.value;
}
private renderTable() {
const groups = this.props.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div style="display:flex;flex-direction: column;gap: 16px;">
${repeat(
groups,
v => v.key,
group => {
return html` <affine-data-view-table-group
data-group-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
.group="${group}"
></affine-data-view-table-group>`;
}
)}
${this.renderAddGroup(this.props.view.groupTrait)}
</div>
`;
}
return html` <affine-data-view-table-group
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
></affine-data-view-table-group>`;
}
override render() {
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div class="affine-database-table" style="${wrapperStyle}">
<div class="affine-database-block-table" @wheel="${this.onWheel}">
<div
class="affine-database-table-container"
style="${containerStyle}"
>
${this.renderTable()}
</div>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-table': DataViewTable;
}
}

View File

@@ -1,14 +1,19 @@
import './pc/effect.js';
import './pc-virtual/effect.js';
import './pc/effect.js';
import { createUniComponentFromWebComponent } from '../../core/utils/uni-component/uni-component.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import { tableViewModel } from './define.js';
import { MobileDataViewTable } from './mobile/table-view.js';
import { TableViewSelector } from './table-view-selector.js';
import { MobileTableViewUILogic } from './mobile/table-view-ui-logic.js';
import { TableViewUILogic } from './pc/table-view-ui-logic.js';
import { VirtualTableViewUILogic } from './pc-virtual/table-view-ui-logic';
export const tableViewMeta = tableViewModel.createMeta({
view: createUniComponentFromWebComponent(TableViewSelector),
mobileView: createUniComponentFromWebComponent(MobileDataViewTable),
icon: createIcon('DatabaseTableViewIcon'),
pcLogic: view =>
// @ts-expect-error fixme: typesafe
view.manager.dataSource.featureFlags$.value.enable_table_virtual_scroll
? VirtualTableViewUILogic
: TableViewUILogic,
// @ts-expect-error fixme: typesafe
mobileLogic: () => MobileTableViewUILogic,
});

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