mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-08 02:23:43 +00:00
Compare commits
76 Commits
0524/mock_
...
v0.22.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9220b973c7 | ||
|
|
7eb6b268a6 | ||
|
|
dc7cd0487b | ||
|
|
7175019a0a | ||
|
|
3c0fa429c5 | ||
|
|
1e9cbdb65d | ||
|
|
192266c0fd | ||
|
|
4ad008f712 | ||
|
|
d6476db64d | ||
|
|
af3c002022 | ||
|
|
69c7767003 | ||
|
|
28d8b35600 | ||
|
|
0f1a3c212d | ||
|
|
9bf86e3f61 | ||
|
|
c649ae5628 | ||
|
|
dd1cc28194 | ||
|
|
ace5531b1f | ||
|
|
5033142a77 | ||
|
|
8d3b20ecc7 | ||
|
|
18da2fe4e6 | ||
|
|
1837c1fe84 | ||
|
|
f4cba7d6ee | ||
|
|
83caf98618 | ||
|
|
409e71ff8b | ||
|
|
b5b911b5d2 | ||
|
|
2f139bd02c | ||
|
|
eed95366c9 | ||
|
|
32c7a135f4 | ||
|
|
3e6384604c | ||
|
|
9465d0dc73 | ||
|
|
1b715e588c | ||
|
|
382c237dac | ||
|
|
3676f3b769 | ||
|
|
ed8e50bca6 | ||
|
|
bfe743b68b | ||
|
|
83a483a06d | ||
|
|
502fb96f55 | ||
|
|
1229ee134b | ||
|
|
9c5af576ee | ||
|
|
4aa9ae5e68 | ||
|
|
6a912d1031 | ||
|
|
8952ce4fb3 | ||
|
|
8b76644fc1 | ||
|
|
5fcdad46eb | ||
|
|
eb26e99ecd | ||
|
|
c2ffcb2c2c | ||
|
|
7f2b094eb5 | ||
|
|
41f0a2d01a | ||
|
|
53a23dd4bf | ||
|
|
0be30f15ea | ||
|
|
5d28657d76 | ||
|
|
9343e29fea | ||
|
|
01369954d6 | ||
|
|
9d2330fc2b | ||
|
|
051dc4296d | ||
|
|
0e8f19b92c | ||
|
|
c06c72e108 | ||
|
|
da22391910 | ||
|
|
d06bb0222f | ||
|
|
25aa5701bd | ||
|
|
8ba4584b88 | ||
|
|
7aacfee789 | ||
|
|
81be5818cc | ||
|
|
6518c5904e | ||
|
|
3d0dc64516 | ||
|
|
5de63c29f5 | ||
|
|
14a89c1e8a | ||
|
|
f619762b0c | ||
|
|
d6000ce70b | ||
|
|
20af4c35ee | ||
|
|
7d3b7a8555 | ||
|
|
e3d63896bf | ||
|
|
adbdf32d8b | ||
|
|
2192f28500 | ||
|
|
9599494e87 | ||
|
|
dfa62f7683 |
@@ -31,9 +31,13 @@
|
||||
"properties": {
|
||||
"queue": {
|
||||
"type": "object",
|
||||
"description": "The config for job queues\n@default {\"attempts\":5,\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
|
||||
"description": "The config for job queues\n@default {\"attempts\":5,\"backoff\":{\"type\":\"exponential\",\"delay\":1000},\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
|
||||
"default": {
|
||||
"attempts": 5,
|
||||
"backoff": {
|
||||
"type": "exponential",
|
||||
"delay": 1000
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": {
|
||||
"age": 86400,
|
||||
@@ -48,14 +52,14 @@
|
||||
},
|
||||
"queues.copilot": {
|
||||
"type": "object",
|
||||
"description": "The config for copilot job queue\n@default {\"concurrency\":5}",
|
||||
"description": "The config for copilot job queue\n@default {\"concurrency\":10}",
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"concurrency": 5
|
||||
"concurrency": 10
|
||||
}
|
||||
},
|
||||
"queues.doc": {
|
||||
@@ -639,6 +643,41 @@
|
||||
"apiKey": ""
|
||||
}
|
||||
},
|
||||
"providers.geminiVertex": {
|
||||
"type": "object",
|
||||
"description": "The config for the google vertex provider.\n@default {}",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The location of the google vertex provider."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project name of the google vertex provider."
|
||||
},
|
||||
"googleAuthOptions": {
|
||||
"type": "object",
|
||||
"description": "The google auth options for the google vertex provider.",
|
||||
"properties": {
|
||||
"credentials": {
|
||||
"type": "object",
|
||||
"description": "The credentials for the google vertex provider.",
|
||||
"properties": {
|
||||
"client_email": {
|
||||
"type": "string",
|
||||
"description": "The client email for the google vertex provider."
|
||||
},
|
||||
"private_key": {
|
||||
"type": "string",
|
||||
"description": "The private key for the google vertex provider."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {}
|
||||
},
|
||||
"providers.perplexity": {
|
||||
"type": "object",
|
||||
"description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}",
|
||||
@@ -653,6 +692,41 @@
|
||||
"apiKey": ""
|
||||
}
|
||||
},
|
||||
"providers.anthropicVertex": {
|
||||
"type": "object",
|
||||
"description": "The config for the google vertex provider.\n@default {}",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The location of the google vertex provider."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project name of the google vertex provider."
|
||||
},
|
||||
"googleAuthOptions": {
|
||||
"type": "object",
|
||||
"description": "The google auth options for the google vertex provider.",
|
||||
"properties": {
|
||||
"credentials": {
|
||||
"type": "object",
|
||||
"description": "The credentials for the google vertex provider.",
|
||||
"properties": {
|
||||
"client_email": {
|
||||
"type": "string",
|
||||
"description": "The client email for the google vertex provider."
|
||||
},
|
||||
"private_key": {
|
||||
"type": "string",
|
||||
"description": "The private key for the google vertex provider."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {}
|
||||
},
|
||||
"unsplash": {
|
||||
"type": "object",
|
||||
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",
|
||||
|
||||
10
.github/actions/server-test-env/action.yml
vendored
10
.github/actions/server-test-env/action.yml
vendored
@@ -29,11 +29,7 @@ runs:
|
||||
|
||||
- name: Import config
|
||||
shell: bash
|
||||
env:
|
||||
DEFAULT_CONFIG: '{}'
|
||||
run: |
|
||||
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"},"providers.anthropic":{"apiKey":"%s"},"exa":{"key":"%s"}}}' \
|
||||
"$COPILOT_FAL_API_KEY" \
|
||||
"$COPILOT_GOOGLE_API_KEY" \
|
||||
"$COPILOT_OPENAI_API_KEY" \
|
||||
"$COPILOT_PERPLEXITY_API_KEY" \
|
||||
"$COPILOT_ANTHROPIC_API_KEY" \
|
||||
"$COPILOT_EXA_API_KEY" > ./packages/backend/server/config.json
|
||||
printf '%s\n' "${SERVER_CONFIG:-$DEFAULT_CONFIG}" > ./packages/backend/server/config.json
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@@ -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'
|
||||
|
||||
14
.github/workflows/build-test.yml
vendored
14
.github/workflows/build-test.yml
vendored
@@ -1001,12 +1001,7 @@ jobs:
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
env:
|
||||
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests
|
||||
@@ -1105,12 +1100,7 @@ jobs:
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
env:
|
||||
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
14
.github/workflows/copilot-test.yml
vendored
14
.github/workflows/copilot-test.yml
vendored
@@ -81,12 +81,7 @@ jobs:
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
env:
|
||||
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests
|
||||
@@ -156,12 +151,7 @@ jobs:
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
env:
|
||||
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
57
Cargo.lock
generated
57
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
getAttachmentFileIcon,
|
||||
getLoadingIconWith,
|
||||
LoadingIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import {
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
DocModeProvider,
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
@@ -304,15 +303,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
}
|
||||
|
||||
protected resolvedState$ = computed<AttachmentResolvedStateInfo>(() => {
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
const loadingIcon = getLoadingIconWith(theme);
|
||||
|
||||
const size = this.model.props.size;
|
||||
const name = this.model.props.name$.value;
|
||||
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
|
||||
|
||||
const resolvedState = this.resourceController.resolveStateWith({
|
||||
loadingIcon,
|
||||
loadingIcon: LoadingIcon(),
|
||||
errorIcon: WarningIcon(),
|
||||
icon: AttachmentIcon(),
|
||||
title: name,
|
||||
|
||||
@@ -47,11 +47,10 @@ export const styles = css`
|
||||
|
||||
.affine-attachment-content-title-icon {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-text {
|
||||
@@ -107,7 +106,7 @@ export const styles = css`
|
||||
|
||||
.affine-attachment-card.loading {
|
||||
.affine-attachment-content-title-text {
|
||||
color: var(--affine-placeholder-color);
|
||||
color: ${unsafeCSSVarV2('text/placeholder')};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
|
||||
};
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.disposables.add(
|
||||
this.gfx.selection.slots.updated.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const style = this.model.props.style$.value;
|
||||
const width = EMBED_CARD_WIDTH[style];
|
||||
@@ -36,12 +45,14 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
|
||||
const bound = this.model.elementBound;
|
||||
const scaleX = bound.w / width;
|
||||
const scaleY = bound.h / height;
|
||||
const isSelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
this.containerStyleMap = styleMap({
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
transform: `scale(${scaleX}, ${scaleY})`,
|
||||
transformOrigin: '0 0',
|
||||
pointerEvents: isSelected ? 'auto' : 'none',
|
||||
});
|
||||
|
||||
return this.renderPageContent();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
|
||||
import { WebIcon16 } from '@blocksuite/affine-components/icons';
|
||||
import { LoadingIcon, WebIcon16 } from '@blocksuite/affine-components/icons';
|
||||
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||
@@ -60,11 +60,11 @@ export class BookmarkCard extends SignalWatcher(
|
||||
: title;
|
||||
|
||||
const theme = this.bookmark.std.get(ThemeProvider).theme;
|
||||
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const imageProxyService = this.bookmark.store.get(ImageProxyService);
|
||||
|
||||
const titleIcon = this.loading
|
||||
? LoadingIcon
|
||||
? LoadingIcon()
|
||||
: icon
|
||||
? html`<img src=${imageProxyService.buildUrl(icon)} alt="icon" />`
|
||||
: WebIcon16;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { BlockComponent } from '@blocksuite/std';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { css, html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
@@ -109,14 +110,18 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const emoji = this.model.props.emoji$.value;
|
||||
return html`
|
||||
<div class="affine-callout-block-container">
|
||||
<div
|
||||
@click=${this._toggleEmojiMenu}
|
||||
contenteditable="false"
|
||||
class="affine-callout-emoji-container"
|
||||
style=${styleMap({
|
||||
display: emoji.length === 0 ? 'none' : undefined,
|
||||
})}
|
||||
>
|
||||
<span class="affine-callout-emoji">${this.model.props.emoji$}</span>
|
||||
<span class="affine-callout-emoji">${emoji}</span>
|
||||
</div>
|
||||
<div class="affine-callout-children">
|
||||
${this.renderChildren(this.model)}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
GfxBlockComponent,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
import {
|
||||
GfxViewInteractionExtension,
|
||||
type SelectedContext,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
@@ -282,69 +279,6 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
|
||||
};
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext): void | boolean {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = this.gfx.selection;
|
||||
const alreadySelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (this.model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.children.length === 0) {
|
||||
const blockId = this.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
this.model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(this.std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = this.querySelector(
|
||||
'.affine-block-children-container'
|
||||
)?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * this.gfx.viewport.zoom;
|
||||
const offsetX = 2 * this.gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
return super.onSelected(context);
|
||||
}
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { model } = this;
|
||||
const { rotate, hasMaxWidth } = model.props;
|
||||
@@ -506,5 +440,73 @@ export const EdgelessTextInteraction =
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
handleSelection: context => {
|
||||
const { gfx, std, view, model } = context;
|
||||
return {
|
||||
onSelect(context) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = gfx.selection;
|
||||
const alreadySelected = gfx.selection.has(model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx.selection.set({
|
||||
elements: [model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
view.updateComplete
|
||||
.then(() => {
|
||||
if (!view.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.children.length === 0) {
|
||||
const blockId = std.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = view
|
||||
.querySelector('.affine-block-children-container')
|
||||
?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * gfx.viewport.zoom;
|
||||
const offsetX = 2 * gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
return context.default(context);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
RENDER_CARD_THROTTLE_MS,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
||||
import { LoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { isPeekable, Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
|
||||
import type {
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { ResetIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
@@ -337,8 +339,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const {
|
||||
LoadingIcon,
|
||||
ReloadIcon,
|
||||
LinkedDocDeletedBanner,
|
||||
LinkedDocEmptyBanner,
|
||||
SyncedDocErrorBanner,
|
||||
@@ -347,7 +347,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
const icon = isError
|
||||
? SyncedDocErrorIcon
|
||||
: isLoading
|
||||
? LoadingIcon
|
||||
? LoadingIcon()
|
||||
: this.icon$.value;
|
||||
const title = isLoading ? 'Loading...' : this.title$;
|
||||
const description = this.model.props.description$;
|
||||
@@ -384,10 +384,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
() => html`
|
||||
<div
|
||||
class="affine-embed-linked-doc-block ${cardClassMap}"
|
||||
style=${styleMap({
|
||||
transform: `scale(${this._scale})`,
|
||||
transformOrigin: '0 0',
|
||||
})}
|
||||
@click=${this._handleClick}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
>
|
||||
@@ -433,7 +429,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
class="affine-embed-linked-doc-card-content-reload-button"
|
||||
@click=${this.refreshData}
|
||||
>
|
||||
${ReloadIcon} <span>Reload</span>
|
||||
${ResetIcon()} <span>Reload</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -124,11 +124,11 @@ export const styles = css`
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
color: ${unsafeCSSVarV2('button/primary')};
|
||||
}
|
||||
.affine-embed-linked-doc-card-content-reload-button svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: var(--affine-background-primary-color);
|
||||
}
|
||||
.affine-embed-linked-doc-card-content-reload-button > span {
|
||||
display: -webkit-box;
|
||||
@@ -138,7 +138,6 @@ export const styles = css`
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-brand-color);
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
@@ -305,7 +304,6 @@ export const styles = css`
|
||||
|
||||
.affine-embed-linked-doc-content-note {
|
||||
-webkit-line-clamp: 16;
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.affine-embed-linked-doc-content-date {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
EmbedEdgelessIcon,
|
||||
EmbedPageIcon,
|
||||
getLoadingIconWith,
|
||||
ReloadIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
ColorScheme,
|
||||
@@ -35,8 +33,6 @@ import {
|
||||
} from './styles.js';
|
||||
|
||||
type EmbedCardImages = {
|
||||
LoadingIcon: TemplateResult<1>;
|
||||
ReloadIcon: TemplateResult<1>;
|
||||
LinkedDocIcon: TemplateResult<1>;
|
||||
LinkedDocDeletedIcon: TemplateResult<1>;
|
||||
LinkedDocEmptyBanner: TemplateResult<1>;
|
||||
@@ -50,12 +46,9 @@ export function getEmbedLinkedDocIcons(
|
||||
style: (typeof EmbedLinkedDocStyles)[number]
|
||||
): EmbedCardImages {
|
||||
const small = style !== 'vertical';
|
||||
const LoadingIcon = getLoadingIconWith(theme);
|
||||
if (editorMode === 'page') {
|
||||
if (theme === ColorScheme.Light) {
|
||||
return {
|
||||
LoadingIcon,
|
||||
ReloadIcon,
|
||||
LinkedDocIcon: EmbedPageIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
@@ -68,8 +61,6 @@ export function getEmbedLinkedDocIcons(
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ReloadIcon,
|
||||
LoadingIcon,
|
||||
LinkedDocIcon: EmbedPageIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
@@ -84,8 +75,6 @@ export function getEmbedLinkedDocIcons(
|
||||
} else {
|
||||
if (theme === ColorScheme.Light) {
|
||||
return {
|
||||
ReloadIcon,
|
||||
LoadingIcon,
|
||||
LinkedDocIcon: EmbedEdgelessIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
@@ -98,8 +87,6 @@ export function getEmbedLinkedDocIcons(
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ReloadIcon,
|
||||
LoadingIcon,
|
||||
LinkedDocIcon: EmbedEdgelessIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RENDER_CARD_THROTTLE_MS } from '@blocksuite/affine-block-embed';
|
||||
import { LoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ResetIcon } from '@blocksuite/icons/lit';
|
||||
import {
|
||||
BlockSelection,
|
||||
isGfxBlockComponent,
|
||||
@@ -148,9 +150,7 @@ export class EmbedSyncedDocCard extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const {
|
||||
LoadingIcon,
|
||||
SyncedDocErrorIcon,
|
||||
ReloadIcon,
|
||||
SyncedDocEmptyBanner,
|
||||
SyncedDocErrorBanner,
|
||||
SyncedDocDeletedBanner,
|
||||
@@ -159,7 +159,7 @@ export class EmbedSyncedDocCard extends WithDisposable(ShadowlessElement) {
|
||||
const icon = error
|
||||
? SyncedDocErrorIcon
|
||||
: isLoading
|
||||
? LoadingIcon
|
||||
? LoadingIcon()
|
||||
: this.block.icon$.value;
|
||||
const title = isLoading ? 'Loading...' : this.block.title$;
|
||||
|
||||
@@ -216,7 +216,7 @@ export class EmbedSyncedDocCard extends WithDisposable(ShadowlessElement) {
|
||||
class="affine-embed-synced-doc-card-content-reload-button"
|
||||
@click=${() => this.block.refreshData()}
|
||||
>
|
||||
${ReloadIcon} <span>Reload</span>
|
||||
${ResetIcon()} <span>Reload</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -303,11 +303,11 @@ export const cardStyles = css`
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
color: ${unsafeCSSVarV2('button/primary')};
|
||||
}
|
||||
.affine-embed-synced-doc-card-content-reload-button svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: var(--affine-background-primary-color);
|
||||
}
|
||||
.affine-embed-synced-doc-card-content-reload-button > span {
|
||||
display: -webkit-box;
|
||||
@@ -317,7 +317,6 @@ export const cardStyles = css`
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-brand-color);
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
EmbedEdgelessIcon,
|
||||
EmbedPageIcon,
|
||||
getLoadingIconWith,
|
||||
ReloadIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { ColorScheme } from '@blocksuite/affine-model';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
@@ -21,11 +19,9 @@ import {
|
||||
} from './styles.js';
|
||||
|
||||
type SyncedCardImages = {
|
||||
LoadingIcon: TemplateResult<1>;
|
||||
SyncedDocIcon: TemplateResult<1>;
|
||||
SyncedDocErrorIcon: TemplateResult<1>;
|
||||
SyncedDocDeletedIcon: TemplateResult<1>;
|
||||
ReloadIcon: TemplateResult<1>;
|
||||
SyncedDocEmptyBanner: TemplateResult<1>;
|
||||
SyncedDocErrorBanner: TemplateResult<1>;
|
||||
SyncedDocDeletedBanner: TemplateResult<1>;
|
||||
@@ -35,25 +31,20 @@ export function getSyncedDocIcons(
|
||||
theme: ColorScheme,
|
||||
editorMode: 'page' | 'edgeless'
|
||||
): SyncedCardImages {
|
||||
const LoadingIcon = getLoadingIconWith(theme);
|
||||
if (theme === ColorScheme.Light) {
|
||||
return {
|
||||
LoadingIcon,
|
||||
SyncedDocIcon: editorMode === 'page' ? EmbedPageIcon : EmbedEdgelessIcon,
|
||||
SyncedDocErrorIcon,
|
||||
SyncedDocDeletedIcon,
|
||||
ReloadIcon,
|
||||
SyncedDocEmptyBanner: LightSyncedDocEmptyBanner,
|
||||
SyncedDocErrorBanner: LightSyncedDocErrorBanner,
|
||||
SyncedDocDeletedBanner: LightSyncedDocDeletedBanner,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
LoadingIcon,
|
||||
SyncedDocIcon: editorMode === 'page' ? EmbedPageIcon : EmbedEdgelessIcon,
|
||||
SyncedDocErrorIcon,
|
||||
SyncedDocDeletedIcon,
|
||||
ReloadIcon,
|
||||
SyncedDocEmptyBanner: DarkSyncedDocEmptyBanner,
|
||||
SyncedDocErrorBanner: DarkSyncedDocErrorBanner,
|
||||
SyncedDocDeletedBanner: DarkSyncedDocDeletedBanner,
|
||||
|
||||
@@ -50,12 +50,6 @@ export class EmbedBlockComponent<
|
||||
|
||||
_cardStyle: EmbedCardStyle = 'horizontal';
|
||||
|
||||
/**
|
||||
* The actual rendered scale of the embed card.
|
||||
* By default, it is set to 1.
|
||||
*/
|
||||
protected _scale = 1;
|
||||
|
||||
blockDraggable = true;
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,6 @@ export function toEdgelessEmbedBlock<
|
||||
this.blockContainerStyles = {
|
||||
width: `${bound.w}px`,
|
||||
};
|
||||
this._scale = bound.w / this._cardWidth;
|
||||
|
||||
return this.renderPageContent();
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@ import {
|
||||
EmbedCardLightHorizontalIcon,
|
||||
EmbedCardLightListIcon,
|
||||
EmbedCardLightVerticalIcon,
|
||||
getLoadingIconWith,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { ColorScheme } from '@blocksuite/affine-model';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
type EmbedCardIcons = {
|
||||
LoadingIcon: TemplateResult<1>;
|
||||
EmbedCardBannerIcon: TemplateResult<1>;
|
||||
EmbedCardHorizontalIcon: TemplateResult<1>;
|
||||
EmbedCardListIcon: TemplateResult<1>;
|
||||
@@ -24,11 +22,8 @@ type EmbedCardIcons = {
|
||||
};
|
||||
|
||||
export function getEmbedCardIcons(theme: ColorScheme): EmbedCardIcons {
|
||||
const LoadingIcon = getLoadingIconWith(theme);
|
||||
|
||||
if (theme === ColorScheme.Light) {
|
||||
return {
|
||||
LoadingIcon,
|
||||
EmbedCardBannerIcon: EmbedCardLightBannerIcon,
|
||||
EmbedCardHorizontalIcon: EmbedCardLightHorizontalIcon,
|
||||
EmbedCardListIcon: EmbedCardLightListIcon,
|
||||
@@ -37,7 +32,6 @@ export function getEmbedCardIcons(theme: ColorScheme): EmbedCardIcons {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
LoadingIcon,
|
||||
EmbedCardBannerIcon: EmbedCardDarkBannerIcon,
|
||||
EmbedCardHorizontalIcon: EmbedCardDarkHorizontalIcon,
|
||||
EmbedCardListIcon: EmbedCardDarkListIcon,
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { html, nothing } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { EmbedBlockComponent } from '../common/embed-block-element.js';
|
||||
import { FigmaIcon, styles } from './styles.js';
|
||||
@@ -76,10 +75,6 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent<EmbedFigmaMode
|
||||
'affine-embed-figma-block': true,
|
||||
selected: this.selected$.value,
|
||||
})}
|
||||
style=${styleMap({
|
||||
transform: `scale(${this._scale})`,
|
||||
transformOrigin: '0 0',
|
||||
})}
|
||||
@click=${this._handleClick}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import { LoadingIcon, OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import type {
|
||||
EmbedGithubModel,
|
||||
EmbedGithubStyles,
|
||||
@@ -133,8 +133,8 @@ export class EmbedGithubBlockComponent extends EmbedBlockComponent<
|
||||
const loading = this.loading;
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const imageProxyService = this.store.get(ImageProxyService);
|
||||
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const titleIcon = loading ? LoadingIcon : GithubIcon;
|
||||
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const titleIcon = loading ? LoadingIcon() : GithubIcon;
|
||||
const statusIcon = status
|
||||
? getGithubStatusIcon(githubType, status, statusReason)
|
||||
: nothing;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { LoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { EmbedIcon } from '@blocksuite/icons/lit';
|
||||
import { type BlockStdScope } from '@blocksuite/std';
|
||||
@@ -7,7 +7,6 @@ import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { getEmbedCardIcons } from '../../common/utils';
|
||||
import { LOADING_CARD_DEFAULT_HEIGHT } from '../consts';
|
||||
import type { EmbedIframeStatusCardOptions } from '../types';
|
||||
|
||||
@@ -156,9 +155,6 @@ export class EmbedIframeLoadingCard extends LitElement {
|
||||
`;
|
||||
|
||||
override render() {
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const { LoadingIcon } = getEmbedCardIcons(theme);
|
||||
|
||||
const { layout, width, height } = this.options;
|
||||
const cardClasses = classMap({
|
||||
'affine-embed-iframe-loading-card': true,
|
||||
@@ -176,7 +172,7 @@ export class EmbedIframeLoadingCard extends LitElement {
|
||||
return html`
|
||||
<div class=${cardClasses} style=${cardStyle}>
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner">${LoadingIcon}</div>
|
||||
<div class="loading-spinner">${LoadingIcon()}</div>
|
||||
<div class="loading-text">Loading...</div>
|
||||
</div>
|
||||
<div class="loading-banner">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import { LoadingIcon, OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import type { EmbedLoomModel, EmbedLoomStyles } from '@blocksuite/affine-model';
|
||||
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
@@ -94,8 +94,8 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
|
||||
const loading = this.loading;
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const imageProxyService = this.store.get(ImageProxyService);
|
||||
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const titleIcon = loading ? LoadingIcon : LoomIcon;
|
||||
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const titleIcon = loading ? LoadingIcon() : LoomIcon;
|
||||
const titleText = loading ? 'Loading...' : title;
|
||||
const descriptionText = loading ? '' : description;
|
||||
const bannerImage =
|
||||
@@ -112,7 +112,6 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
|
||||
selected: this.selected$.value,
|
||||
})}
|
||||
style=${styleMap({
|
||||
transform: `scale(${this._scale})`,
|
||||
transformOrigin: '0 0',
|
||||
})}
|
||||
@click=${this._handleClick}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import { LoadingIcon, OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import type {
|
||||
EmbedYoutubeModel,
|
||||
EmbedYoutubeStyles,
|
||||
@@ -108,8 +108,8 @@ export class EmbedYoutubeBlockComponent extends EmbedBlockComponent<
|
||||
const loading = this.loading;
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const imageProxyService = this.store.get(ImageProxyService);
|
||||
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const titleIcon = loading ? LoadingIcon : YoutubeIcon;
|
||||
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const titleIcon = loading ? LoadingIcon() : YoutubeIcon;
|
||||
const titleText = loading ? 'Loading...' : title;
|
||||
const descriptionText = loading ? null : description;
|
||||
const bannerImage =
|
||||
|
||||
@@ -205,10 +205,11 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
|
||||
!forceMove
|
||||
) {
|
||||
// Clear the flag so future navigations behave normally
|
||||
this.gfx.tool.setTool(PresentTool, {
|
||||
...toolOptions,
|
||||
restoredAfterPan: false,
|
||||
});
|
||||
// Here we modify the tool's activated option to avoid triggering setTool update
|
||||
const currentTool = this.gfx.tool.currentTool$.peek();
|
||||
if (currentTool?.activatedOption) {
|
||||
currentTool.activatedOption.restoredAfterPan = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DefaultTheme,
|
||||
type FrameBlockModel,
|
||||
FrameBlockSchema,
|
||||
isTransparent,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
type BoxSelectionContext,
|
||||
getTopElements,
|
||||
GfxViewInteractionExtension,
|
||||
type SelectedContext,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
@@ -68,22 +68,6 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
};
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext): boolean | void {
|
||||
const { x, y } = context.position;
|
||||
|
||||
if (
|
||||
!context.fallback &&
|
||||
// if the frame is selected by title, then ignore it because the title selection is handled by the title widget
|
||||
(this.model.externalBound?.containsPoint([x, y]) ||
|
||||
// otherwise if the frame has title, then ignore it because in this case the frame cannot be selected by frame body
|
||||
this.model.props.title.length)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.onSelected(context);
|
||||
}
|
||||
|
||||
override onBoxSelected(context: BoxSelectionContext) {
|
||||
const { box } = context;
|
||||
const bound = new Bound(box.x, box.y, box.w, box.h);
|
||||
@@ -189,5 +173,17 @@ export const FrameBlockInteraction =
|
||||
},
|
||||
};
|
||||
},
|
||||
handleSelection: () => {
|
||||
return {
|
||||
selectable(context) {
|
||||
const { model } = context;
|
||||
|
||||
return (
|
||||
context.default(context) &&
|
||||
(model.isLocked() || !isTransparent(model.props.background))
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -46,12 +46,16 @@ export class ImageBlockPageComponent extends SignalWatcher(
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
left: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
|
||||
|
||||
& > svg {
|
||||
font-size: 25.71px;
|
||||
}
|
||||
}
|
||||
|
||||
affine-page-image .affine-image-status {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import { whenHover } from '@blocksuite/affine-components/hover';
|
||||
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
|
||||
import { LoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { ResourceController } from '@blocksuite/affine-components/resource';
|
||||
import type { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import { ImageSelection } from '@blocksuite/affine-shared/selection';
|
||||
import {
|
||||
ThemeProvider,
|
||||
ToolbarRegistryIdentifier,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { ToolbarRegistryIdentifier } from '@blocksuite/affine-shared/services';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
@@ -126,9 +124,6 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
const loadingIcon = getLoadingIconWith(theme);
|
||||
|
||||
const blobUrl = this.blobUrl;
|
||||
const { size = 0 } = this.model.props;
|
||||
|
||||
@@ -138,7 +133,9 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
});
|
||||
|
||||
const resovledState = this.resourceController.resolveStateWith({
|
||||
loadingIcon,
|
||||
loadingIcon: LoadingIcon({
|
||||
strokeColor: cssVarV2('button/pureWhiteText'),
|
||||
}),
|
||||
errorIcon: BrokenImageIcon(),
|
||||
icon: ImageIcon(),
|
||||
title: 'Image',
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
|
||||
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
|
||||
import { LoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { ResourceController } from '@blocksuite/affine-components/resource';
|
||||
import {
|
||||
type ImageBlockModel,
|
||||
ImageBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { cssVarV2, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
|
||||
import { GfxBlockComponent } from '@blocksuite/std';
|
||||
@@ -39,11 +38,15 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
|
||||
|
||||
& > svg {
|
||||
font-size: 25.71px;
|
||||
}
|
||||
}
|
||||
|
||||
affine-edgeless-image .affine-image-status {
|
||||
@@ -108,9 +111,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
const loadingIcon = getLoadingIconWith(theme);
|
||||
|
||||
const blobUrl = this.blobUrl;
|
||||
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
|
||||
|
||||
@@ -124,7 +124,9 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
});
|
||||
|
||||
const resovledState = this.resourceController.resolveStateWith({
|
||||
loadingIcon,
|
||||
loadingIcon: LoadingIcon({
|
||||
strokeColor: cssVarV2('button/pureWhiteText'),
|
||||
}),
|
||||
errorIcon: BrokenImageIcon(),
|
||||
icon: ImageIcon(),
|
||||
title: 'Image',
|
||||
@@ -148,7 +150,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
</div>
|
||||
${when(
|
||||
resovledState.loading,
|
||||
() => html`<div class="loading">${loadingIcon}</div>`
|
||||
() => html`<div class="loading">${resovledState.icon}</div>`
|
||||
)}
|
||||
${when(
|
||||
resovledState.error && resovledState.description,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { LatexProps } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
@@ -48,6 +52,21 @@ export const insertLatexBlockCommand: Command<
|
||||
if (blockComponent instanceof LatexBlockComponent) {
|
||||
await blockComponent.updateComplete;
|
||||
blockComponent.toggleEditor();
|
||||
|
||||
const mode = std.get(DocModeProvider).getEditorMode() ?? 'page';
|
||||
const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
|
||||
std.getOptional(TelemetryProvider)?.track('Latex', {
|
||||
from:
|
||||
mode === 'page'
|
||||
? 'doc'
|
||||
: ifEdgelessText
|
||||
? 'edgeless text'
|
||||
: 'edgeless note',
|
||||
page: mode === 'page' ? 'doc' : 'edgeless',
|
||||
segment: mode === 'page' ? 'doc' : 'whiteboard',
|
||||
module: 'equation',
|
||||
control: 'create equation',
|
||||
});
|
||||
}
|
||||
}
|
||||
return result[0];
|
||||
|
||||
@@ -74,19 +74,16 @@ export class LatexBlockComponent extends CaptionedBlockComponent<LatexBlockModel
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.disposables.addFromEvent(this, 'click', () => {
|
||||
// should not open editor or select block in readonly mode
|
||||
if (this.store.readonly) {
|
||||
return;
|
||||
}
|
||||
private _handleClick() {
|
||||
if (this.store.readonly) return;
|
||||
|
||||
if (this.isBlockSelected) {
|
||||
this.toggleEditor();
|
||||
} else {
|
||||
this.selectBlock();
|
||||
}
|
||||
});
|
||||
if (this.isBlockSelected) {
|
||||
this.toggleEditor();
|
||||
} else {
|
||||
this.selectBlock();
|
||||
}
|
||||
}
|
||||
|
||||
removeEditor(portal: HTMLDivElement) {
|
||||
@@ -95,7 +92,11 @@ export class LatexBlockComponent extends CaptionedBlockComponent<LatexBlockModel
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div contenteditable="false" class="latex-block-container">
|
||||
<div
|
||||
contenteditable="false"
|
||||
class="latex-block-container"
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<div class="katex"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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' }],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,10 +26,9 @@ import {
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { consume } from '@lit/context';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { computed, effect } from '@preact/signals-core';
|
||||
import { nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { NoteConfigExtension } from '../config';
|
||||
import * as styles from './edgeless-note-background.css';
|
||||
@@ -150,15 +149,20 @@ export class EdgelessNoteBackground extends SignalWatcher(
|
||||
return header;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.add(styles.background);
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
Object.assign(this.style, this.backgroundStyle$.value);
|
||||
})
|
||||
);
|
||||
this.disposables.addFromEvent(this, 'pointerdown', stopPropagation);
|
||||
this.disposables.addFromEvent(this, 'click', this._handleClickAtBackground);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div
|
||||
class=${styles.background}
|
||||
style=${styleMap(this.backgroundStyle$.value)}
|
||||
@pointerdown=${stopPropagation}
|
||||
@click=${this._handleClickAtBackground}
|
||||
>
|
||||
${this.note.isPageBlock() ? this._renderHeader() : nothing}
|
||||
</div>`;
|
||||
return this.note.isPageBlock() ? this._renderHeader() : nothing;
|
||||
}
|
||||
|
||||
@consume({ context: stdContext })
|
||||
|
||||
@@ -13,7 +13,6 @@ import { toGfxBlockComponent } from '@blocksuite/std';
|
||||
import {
|
||||
type BoxSelectionContext,
|
||||
GfxViewInteractionExtension,
|
||||
type SelectedContext,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import { html, nothing, type PropertyValues } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
@@ -342,69 +341,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
`;
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = this.gfx.selection;
|
||||
const alreadySelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (this.model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.children.length === 0) {
|
||||
const blockId = this.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
this.model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(this.std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = this.querySelector(
|
||||
'.affine-block-children-container'
|
||||
)?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * this.gfx.viewport.zoom;
|
||||
const offsetX = 2 * this.gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
super.onSelected(context);
|
||||
}
|
||||
}
|
||||
|
||||
override onBoxSelected(_: BoxSelectionContext) {
|
||||
return this.model.props.displayMode !== NoteDisplayMode.DocOnly;
|
||||
}
|
||||
@@ -493,5 +429,71 @@ export const EdgelessNoteInteraction =
|
||||
},
|
||||
};
|
||||
},
|
||||
handleSelection: ({ std, gfx, view, model }) => {
|
||||
return {
|
||||
onSelect(context) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = gfx.selection;
|
||||
const alreadySelected = gfx.selection.has(model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx.selection.set({
|
||||
elements: [model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
view.updateComplete
|
||||
.then(() => {
|
||||
if (!view.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.children.length === 0) {
|
||||
const blockId = std.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = view
|
||||
.querySelector('.affine-block-children-container')
|
||||
?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * gfx.viewport.zoom;
|
||||
const offsetX = 2 * gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
context.default(context);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
|
||||
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
|
||||
import { FrameTool } from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
FrameTool,
|
||||
type PresentToolOption,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
DefaultTool,
|
||||
EdgelessLegacySlotIdentifier,
|
||||
@@ -472,9 +475,6 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
const selection = gfx.selection;
|
||||
|
||||
if (event.code === 'Space' && !event.repeat) {
|
||||
const currentToolName =
|
||||
this.rootComponent.gfx.tool.currentToolName$.peek();
|
||||
if (currentToolName === 'frameNavigator') return false;
|
||||
this._space(event);
|
||||
} else if (
|
||||
!selection.editing &&
|
||||
@@ -512,9 +512,6 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
ctx => {
|
||||
const event = ctx.get('keyboardState').raw;
|
||||
if (event.code === 'Space' && !event.repeat) {
|
||||
const currentToolName =
|
||||
this.rootComponent.gfx.tool.currentToolName$.peek();
|
||||
if (currentToolName === 'frameNavigator') return false;
|
||||
this._space(event);
|
||||
}
|
||||
return false;
|
||||
@@ -712,10 +709,18 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
|
||||
const revertToPrevTool = (ev: KeyboardEvent) => {
|
||||
if (ev.code === 'Space') {
|
||||
this._setEdgelessTool(
|
||||
(currentTool as DefaultTool).constructor as typeof DefaultTool,
|
||||
currentTool?.activatedOption
|
||||
);
|
||||
const toolConstructor = currentTool.constructor as typeof DefaultTool;
|
||||
let finalOptions = currentTool?.activatedOption;
|
||||
|
||||
// Handle frameNavigator (PresentTool) restoration after space pan
|
||||
if (currentTool.toolName === 'frameNavigator') {
|
||||
finalOptions = {
|
||||
...currentTool?.activatedOption,
|
||||
restoredAfterPan: true,
|
||||
} as PresentToolOption;
|
||||
}
|
||||
|
||||
this._setEdgelessTool(toolConstructor, finalOptions);
|
||||
selection.set(currentSel);
|
||||
document.removeEventListener('keyup', revertToPrevTool, false);
|
||||
}
|
||||
@@ -728,6 +733,14 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If in presentation mode, disable black background during space drag
|
||||
if (currentTool.toolName === 'frameNavigator') {
|
||||
this.slots.navigatorSettingUpdated.next({
|
||||
blackBackground: false,
|
||||
});
|
||||
}
|
||||
|
||||
this._setEdgelessTool(PanTool, { panning: false });
|
||||
|
||||
this.std.event.disposables.addFromEvent(
|
||||
|
||||
@@ -16,7 +16,6 @@ import type {
|
||||
GfxController,
|
||||
GfxModel,
|
||||
LayerManager,
|
||||
PointTestOptions,
|
||||
ReorderingDirection,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import {
|
||||
@@ -168,19 +167,6 @@ export class EdgelessRootService
|
||||
this._initReadonlyListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to pick element in group, if the picked element is in a
|
||||
* group, we will pick the group instead. If that picked group is currently selected, then
|
||||
* we will pick the element itself.
|
||||
*/
|
||||
pickElementInGroup(
|
||||
x: number,
|
||||
y: number,
|
||||
options?: PointTestOptions
|
||||
): GfxModel | null {
|
||||
return this.gfx.getElementInGroup(x, y, options);
|
||||
}
|
||||
|
||||
removeElement(id: string | GfxModel) {
|
||||
id = typeof id === 'string' ? id : id.id;
|
||||
|
||||
|
||||
@@ -154,32 +154,6 @@ export class DefaultTool extends BaseTool {
|
||||
private _determineDragType(evt: PointerEventState): DefaultModeDragType {
|
||||
const { x, y } = this.controller.lastMousePos$.peek();
|
||||
if (this.selection.isInSelectedRect(x, y)) {
|
||||
if (this.selection.selectedElements.length === 1) {
|
||||
const currentHoveredElem = this._getElementInGroup(x, y);
|
||||
let curSelected = this.selection.selectedElements[0];
|
||||
|
||||
// If one of the following condition is true, keep the selection:
|
||||
// 1. if group is currently selected
|
||||
// 2. if the selected element is descendant of the hovered element
|
||||
// 3. not hovering any element or hovering the same element
|
||||
//
|
||||
// Otherwise, we update the selection to the current hovered element
|
||||
const shouldKeepSelection =
|
||||
isGfxGroupCompatibleModel(curSelected) ||
|
||||
(isGfxGroupCompatibleModel(currentHoveredElem) &&
|
||||
currentHoveredElem.hasDescendant(curSelected)) ||
|
||||
!currentHoveredElem ||
|
||||
currentHoveredElem === curSelected;
|
||||
|
||||
if (!shouldKeepSelection) {
|
||||
curSelected = currentHoveredElem;
|
||||
this.selection.set({
|
||||
elements: [curSelected.id],
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.selection.editing
|
||||
? DefaultModeDragType.NativeEditing
|
||||
: DefaultModeDragType.ContentMoving;
|
||||
@@ -194,17 +168,6 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
}
|
||||
|
||||
private _getElementInGroup(modelX: number, modelY: number) {
|
||||
const tryGetLockedAncestor = (e: GfxModel | null) => {
|
||||
if (e?.isLockedByAncestor()) {
|
||||
return e.groups.findLast(group => group.isLocked());
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
return tryGetLockedAncestor(this.gfx.getElementInGroup(modelX, modelY));
|
||||
}
|
||||
|
||||
private initializeDragState(
|
||||
dragType: DefaultModeDragType,
|
||||
event: PointerEventState
|
||||
|
||||
@@ -67,6 +67,8 @@ export class TableSelection extends BaseSelection {
|
||||
|
||||
static override type = 'table';
|
||||
|
||||
static override recoverable = true;
|
||||
|
||||
readonly data: TableSelectionData;
|
||||
|
||||
constructor({
|
||||
|
||||
@@ -111,6 +111,7 @@ export class MenuInput extends MenuFocusable {
|
||||
}}"
|
||||
@input="${this.onInput}"
|
||||
placeholder="${this.data.placeholder ?? ''}"
|
||||
@keypress="${this.stopPropagation}"
|
||||
@keydown="${this.onKeydown}"
|
||||
@copy="${this.stopPropagation}"
|
||||
@paste="${this.stopPropagation}"
|
||||
|
||||
@@ -92,6 +92,7 @@ export class FilterableListComponent<Props = unknown> extends WithDisposable(
|
||||
const isFlip = !!this.placement?.startsWith('top');
|
||||
|
||||
const _handleInputKeydown = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
switch (ev.key) {
|
||||
case 'ArrowUp': {
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { ColorScheme } from '@blocksuite/affine-model';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
|
||||
const LoadingIcon = (color: string) =>
|
||||
export const LoadingIcon = ({
|
||||
size = '1em',
|
||||
progress = 0.2,
|
||||
strokeColor = cssVarV2('loading/foreground'),
|
||||
}: {
|
||||
size?: string;
|
||||
progress?: number;
|
||||
strokeColor?: string;
|
||||
} = {}) =>
|
||||
html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="${size}"
|
||||
height="${size}"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
>
|
||||
<style xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.spinner {
|
||||
transform-origin: center;
|
||||
animation: spinner_animate 0.75s infinite linear;
|
||||
@@ -19,21 +28,24 @@ const LoadingIcon = (color: string) =>
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M14.6666 8.00004C14.6666 11.6819 11.6818 14.6667 7.99992 14.6667C4.31802 14.6667 1.33325 11.6819 1.33325 8.00004C1.33325 4.31814 4.31802 1.33337 7.99992 1.33337C11.6818 1.33337 14.6666 4.31814 14.6666 8.00004ZM3.30003 8.00004C3.30003 10.5957 5.40424 12.6999 7.99992 12.6999C10.5956 12.6999 12.6998 10.5957 12.6998 8.00004C12.6998 5.40436 10.5956 3.30015 7.99992 3.30015C5.40424 3.30015 3.30003 5.40436 3.30003 8.00004Z"
|
||||
fill="${color}"
|
||||
fill-opacity="0.1"
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="8"
|
||||
stroke="${cssVarV2('loading/background')}"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
d="M13.6833 8.00004C14.2263 8.00004 14.674 7.55745 14.5942 7.02026C14.5142 6.48183 14.3684 5.954 14.1591 5.44882C13.8241 4.63998 13.333 3.90505 12.714 3.286C12.0949 2.66694 11.36 2.17588 10.5511 1.84084C10.046 1.63159 9.51812 1.48576 8.9797 1.40576C8.44251 1.32595 7.99992 1.77363 7.99992 2.31671C7.99992 2.85979 8.44486 3.28974 8.9761 3.40253C9.25681 3.46214 9.53214 3.54746 9.79853 3.65781C10.3688 3.894 10.8869 4.2402 11.3233 4.67664C11.7598 5.11307 12.106 5.6312 12.3422 6.20143C12.4525 6.46782 12.5378 6.74315 12.5974 7.02386C12.7102 7.5551 13.1402 8.00004 13.6833 8.00004Z"
|
||||
fill="#1C9EE4"
|
||||
<circle
|
||||
class="spinner"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="8"
|
||||
stroke="${strokeColor}"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="${2 * Math.PI * 8 * progress} ${2 *
|
||||
Math.PI *
|
||||
8 *
|
||||
(1 - progress)}"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
export const LightLoadingIcon = LoadingIcon('black');
|
||||
|
||||
export const DarkLoadingIcon = LoadingIcon('white');
|
||||
|
||||
export const getLoadingIconWith = (theme: ColorScheme = ColorScheme.Light) =>
|
||||
theme === ColorScheme.Light ? LightLoadingIcon : DarkLoadingIcon;
|
||||
|
||||
@@ -840,28 +840,6 @@ export const EmbedCardDarkCubeIcon = html`
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const ReloadIcon = html`<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_6505_24239)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M1.625 6C1.625 3.58375 3.58375 1.625 6 1.625C7.12028 1.625 8.14299 2.04656 8.91676 2.7391L8.91796 2.74017L9.625 3.37847V2C9.625 1.79289 9.79289 1.625 10 1.625C10.2071 1.625 10.375 1.79289 10.375 2V4.22222C10.375 4.42933 10.2071 4.59722 10 4.59722H7.77778C7.57067 4.59722 7.40278 4.42933 7.40278 4.22222C7.40278 4.01512 7.57067 3.84722 7.77778 3.84722H9.025L8.41657 3.29795C8.41637 3.29777 8.41617 3.29759 8.41597 3.29741C7.77447 2.7235 6.92838 2.375 6 2.375C3.99797 2.375 2.375 3.99797 2.375 6C2.375 8.00203 3.99797 9.625 6 9.625C7.72469 9.625 9.16888 8.42017 9.53518 6.80591C9.58101 6.60393 9.78189 6.47736 9.98386 6.52319C10.1858 6.56902 10.3124 6.7699 10.2666 6.97187C9.82447 8.92025 8.08257 10.375 6 10.375C3.58375 10.375 1.625 8.41625 1.625 6Z"
|
||||
fill="#1E96EB"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6505_24239">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>`;
|
||||
|
||||
export const EmbedPageIcon = icons.LinkedPageIcon({
|
||||
width: '16',
|
||||
height: '16',
|
||||
|
||||
@@ -92,7 +92,7 @@ export class Slider extends WithDisposable(LitElement) {
|
||||
|
||||
const dispose = on(this, 'pointermove', this._onPointerMove);
|
||||
this._disposables.add(once(this, 'pointerup', dispose));
|
||||
this._disposables.add(once(this, 'pointerout', dispose));
|
||||
this._disposables.add(once(this, 'pointerleave', dispose));
|
||||
};
|
||||
|
||||
private readonly _onPointerMove = (e: PointerEvent) => {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { css } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
:host([disabled]) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
32
blocksuite/affine/data-view/src/core/utils/cache.ts
Normal file
32
blocksuite/affine/data-view/src/core/utils/cache.ts
Normal 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));
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './lazy.js';
|
||||
export * from './uni-component/index.js';
|
||||
export * from './uni-icon.js';
|
||||
|
||||
11
blocksuite/affine/data-view/src/core/utils/lazy.ts
Normal file
11
blocksuite/affine/data-view/src/core/utils/lazy.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './convert.js';
|
||||
export * from './data-view.js';
|
||||
export * from './data-view-base.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mobileEffects } from './mobile/effect.js';
|
||||
import { pcEffects } from './pc/effect.js';
|
||||
|
||||
export function kanbanEffects() {
|
||||
pcEffects();
|
||||
mobileEffects();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
11
blocksuite/affine/data-view/src/view-presets/table/effect.ts
Normal file
11
blocksuite/affine/data-view/src/view-presets/table/effect.ts
Normal 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();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -8,17 +8,14 @@ import { PlusIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { GroupTitle } from '../../../core/group-by/group-title.js';
|
||||
import type { Group } from '../../../core/group-by/trait.js';
|
||||
import type { Row } from '../../../core/index.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import type { DataViewTable } from '../pc/table-view.js';
|
||||
import { TableViewAreaSelection } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
.data-view-table-group-add-row {
|
||||
@@ -54,40 +51,10 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
selectionController.selection = TableViewAreaSelection.create({
|
||||
groupKey: this.group?.key,
|
||||
focus: {
|
||||
rowIndex: this.rows.length - 1,
|
||||
columnIndex: index,
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
selectionController.selection = TableViewAreaSelection.create({
|
||||
groupKey: this.group?.key,
|
||||
focus: {
|
||||
rowIndex: 0,
|
||||
columnIndex: index,
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -150,8 +117,7 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
return html` <mobile-table-row
|
||||
data-row-index="${idx}"
|
||||
data-row-id="${row.rowId}"
|
||||
.dataViewEle="${this.dataViewEle}"
|
||||
.view="${this.view}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.rowId="${row.rowId}"
|
||||
.rowIndex="${idx}"
|
||||
></mobile-table-row>`;
|
||||
@@ -172,8 +138,6 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
${PlusIcon()}<span style="font-size: 12px">New Record</span>
|
||||
</div>
|
||||
</div>`}
|
||||
<affine-database-column-stats .view="${this.view}" .group="${this.group}">
|
||||
</affine-database-column-stats>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -181,20 +145,15 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
return this.renderRows(this.rows);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group: Group | undefined = undefined;
|
||||
|
||||
@query('.affine-database-block-rows')
|
||||
accessor rowsContainer: HTMLElement | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
accessor tableViewLogic!: MobileTableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor viewEle!: DataViewTable;
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user