mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 11:09:01 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c696d278b | |||
| 372fc126b5 | |||
| 6d57c01dd4 | |||
| 3d2d399796 | |||
| 6483f36723 | |||
| df2ecf2bec | |||
| 148c718a12 | |||
| c4af1e77d0 | |||
| 6a0eb80903 | |||
| 77392efaa2 | |||
| 927b4f4430 | |||
| 9ec1d08d98 | |||
| 86cd92a878 | |||
| ab28213df2 | |||
| 39cb1afedb | |||
| 1eb9e62075 | |||
| ef5f96bfb6 | |||
| b9c70985a1 | |||
| 66db63c845 | |||
| 32a29657e4 | |||
| 1aa0cd27d5 | |||
| 58bbb017a0 | |||
| c91a4eb0aa | |||
| 5590cdd8f1 | |||
| de00040389 | |||
| 1b881cfb01 | |||
| 6e190b9703 | |||
| acf92aa3da | |||
| 9f0d4536c7 | |||
| 9a651a5b53 | |||
| d4c5b40284 | |||
| 85def83f5e | |||
| f610d7b8af | |||
| 9e5d132bd0 | |||
| 7ae564238d | |||
| 9abbfa3ab4 | |||
| 793823a9f9 | |||
| 2d5b9022fd | |||
| b847de4980 | |||
| 274319dd6c | |||
| eb49ffaedb | |||
| a045786c6a | |||
| ace4b844fd | |||
| d5dd680855 | |||
| f4e7595f4b | |||
| 88339b4022 | |||
| d49ecfbecc | |||
| 87dfd2b77d | |||
| c43e1bcc4e | |||
| cf456c888f | |||
| f5f959692a | |||
| 9220b973c7 | |||
| 7eb6b268a6 | |||
| dc7cd0487b | |||
| 7175019a0a | |||
| 3c0fa429c5 | |||
| 1e9cbdb65d | |||
| 192266c0fd | |||
| 4ad008f712 | |||
| d6476db64d | |||
| af3c002022 | |||
| 69c7767003 | |||
| 28d8b35600 | |||
| 0f1a3c212d | |||
| 9bf86e3f61 | |||
| c649ae5628 | |||
| dd1cc28194 |
@@ -52,14 +52,14 @@
|
||||
},
|
||||
"queues.copilot": {
|
||||
"type": "object",
|
||||
"description": "The config for copilot job queue\n@default {\"concurrency\":5}",
|
||||
"description": "The config for copilot job queue\n@default {\"concurrency\":10}",
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"concurrency": 5
|
||||
"concurrency": 10
|
||||
}
|
||||
},
|
||||
"queues.doc": {
|
||||
|
||||
@@ -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 }}
|
||||
+4
-3
@@ -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
|
||||
@@ -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'
|
||||
|
||||
Generated
+50
-7
@@ -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"
|
||||
|
||||
+1
-1
@@ -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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
|
||||
@@ -64,6 +64,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
return this.resourceController.blobUrl$.value;
|
||||
}
|
||||
|
||||
get filetype() {
|
||||
const name = this.model.props.name$.value;
|
||||
return name.split('.').pop() ?? '';
|
||||
}
|
||||
|
||||
protected containerStyleMap = styleMap({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
@@ -212,13 +217,23 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
);
|
||||
};
|
||||
|
||||
protected renderReloadButton = () => {
|
||||
protected renderNormalButton = (needUpload: boolean) => {
|
||||
const label = needUpload ? 'retry' : 'reload';
|
||||
const run = async () => {
|
||||
if (needUpload) {
|
||||
await this.resourceController.upload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
};
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="affine-attachment-content-button"
|
||||
@click=${(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
this.refreshData();
|
||||
run().catch(console.error);
|
||||
|
||||
{
|
||||
const mode =
|
||||
@@ -230,21 +245,28 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
segment,
|
||||
page: `${segment} editor`,
|
||||
module: 'attachment',
|
||||
control: 'reload',
|
||||
control: label,
|
||||
category: 'card',
|
||||
type: this.model.props.name.split('.').pop() ?? '',
|
||||
type: this.filetype,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
${ResetIcon()} Reload
|
||||
${ResetIcon()} ${label}
|
||||
</button>
|
||||
`;
|
||||
};
|
||||
|
||||
protected renderWithHorizontal(
|
||||
classInfo: ClassInfo,
|
||||
{ icon, title, description, kind, state }: AttachmentResolvedStateInfo
|
||||
{
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state,
|
||||
needUpload,
|
||||
}: AttachmentResolvedStateInfo
|
||||
) {
|
||||
return html`
|
||||
<div class=${classMap(classInfo)}>
|
||||
@@ -261,7 +283,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
${description}
|
||||
</div>
|
||||
${choose(state, [
|
||||
['error', this.renderReloadButton],
|
||||
['error', () => this.renderNormalButton(needUpload)],
|
||||
['error:oversize', this.renderUpgradeButton],
|
||||
])}
|
||||
</div>
|
||||
@@ -274,7 +296,14 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
protected renderWithVertical(
|
||||
classInfo: ClassInfo,
|
||||
{ icon, title, description, kind, state }: AttachmentResolvedStateInfo
|
||||
{
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state,
|
||||
needUpload,
|
||||
}: AttachmentResolvedStateInfo
|
||||
) {
|
||||
return html`
|
||||
<div class=${classMap(classInfo)}>
|
||||
@@ -294,7 +323,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
<div class="affine-attachment-banner">
|
||||
${kind}
|
||||
${choose(state, [
|
||||
['error', this.renderReloadButton],
|
||||
['error', () => this.renderNormalButton(needUpload)],
|
||||
['error:oversize', this.renderUpgradeButton],
|
||||
])}
|
||||
</div>
|
||||
@@ -305,7 +334,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
protected resolvedState$ = computed<AttachmentResolvedStateInfo>(() => {
|
||||
const size = this.model.props.size;
|
||||
const name = this.model.props.name$.value;
|
||||
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
|
||||
const kind = getAttachmentFileIcon(this.filetype);
|
||||
|
||||
const resolvedState = this.resourceController.resolveStateWith({
|
||||
loadingIcon: LoadingIcon(),
|
||||
@@ -359,11 +388,16 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
const message = resolvedState.description;
|
||||
if (!message) return null;
|
||||
|
||||
const needUpload = resolvedState.needUpload;
|
||||
const action = () =>
|
||||
needUpload ? this.resourceController.upload() : this.reload();
|
||||
|
||||
return html`
|
||||
<affine-resource-status
|
||||
class="affine-attachment-embed-status"
|
||||
.message=${message}
|
||||
.reload=${() => this.reload()}
|
||||
.needUpload=${needUpload}
|
||||
.action=${action}
|
||||
></affine-resource-status>
|
||||
`;
|
||||
})}
|
||||
@@ -372,10 +406,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
private readonly _renderCitation = () => {
|
||||
const { name, footnoteIdentifier } = this.model.props;
|
||||
const fileType = name.split('.').pop() ?? '';
|
||||
const fileTypeIcon = getAttachmentFileIcon(fileType);
|
||||
const icon = getAttachmentFileIcon(this.filetype);
|
||||
|
||||
return html`<affine-citation-card
|
||||
.icon=${fileTypeIcon}
|
||||
.icon=${icon}
|
||||
.citationTitle=${name}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
.active=${this.selected$.value}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
|
||||
import { openSingleFileWith } from '@blocksuite/affine-shared/utils';
|
||||
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
|
||||
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
@@ -21,7 +21,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
|
||||
model.store.schema.flavourSchemaMap.has('affine:attachment'),
|
||||
action: ({ std, model }) => {
|
||||
(async () => {
|
||||
const file = await openFileOrFiles();
|
||||
const file = await openSingleFileWith();
|
||||
if (!file) return;
|
||||
|
||||
await addSiblingAttachmentBlocks(std, [file], model);
|
||||
@@ -44,7 +44,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
|
||||
model.store.schema.flavourSchemaMap.has('affine:attachment'),
|
||||
action: ({ std, model }) => {
|
||||
(async () => {
|
||||
const file = await openFileOrFiles();
|
||||
const file = await openSingleFileWith();
|
||||
if (!file) return;
|
||||
|
||||
await addSiblingAttachmentBlocks(std, [file], model);
|
||||
|
||||
@@ -91,6 +91,7 @@ export const styles = css`
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
line-height: 20px;
|
||||
|
||||
svg {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -35,14 +35,10 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
|
||||
|
||||
.code-toolbar-button {
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
background-color: ${unsafeCSSVarV2('segment/background')};
|
||||
background-color: ${unsafeCSSVarV2('button/secondary')};
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
margin-left: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
private _currentOpenMenu: AbortController | null = null;
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
showPopFilterableList,
|
||||
} from '@blocksuite/affine-components/filterable-list';
|
||||
import { ArrowDownIcon } from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
@@ -73,6 +77,18 @@ export class LanguageListButton extends WithDisposable(
|
||||
this.blockComponent.store.transact(() => {
|
||||
this.blockComponent.model.props.language$.value = item.name;
|
||||
});
|
||||
|
||||
const std = this.blockComponent.std;
|
||||
const mode =
|
||||
std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
|
||||
const telemetryService = std.getOptional(TelemetryProvider);
|
||||
if (!telemetryService) return;
|
||||
telemetryService.track('codeBlockLanguageSelect', {
|
||||
page: mode,
|
||||
segment: 'code block',
|
||||
module: 'language selector',
|
||||
control: item.name,
|
||||
});
|
||||
},
|
||||
active: item => item.name === this.blockComponent.model.props.language,
|
||||
items: this._sortedBundledLanguages,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
@@ -9,6 +13,10 @@ import { CodeBlockPreviewIdentifier } from '../../code-preview-extension';
|
||||
|
||||
export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.preview-toggle-container {
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
@@ -55,6 +63,17 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
this.blockComponent.store.updateBlock(this.blockComponent.model, {
|
||||
preview: value,
|
||||
});
|
||||
|
||||
const std = this.blockComponent.std;
|
||||
const mode = std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
|
||||
const telemetryService = std.getOptional(TelemetryProvider);
|
||||
if (!telemetryService) return;
|
||||
telemetryService.track('htmlBlockTogglePreview', {
|
||||
page: mode,
|
||||
segment: 'code block',
|
||||
module: 'code toolbar container',
|
||||
control: 'preview toggle button',
|
||||
});
|
||||
};
|
||||
|
||||
get preview() {
|
||||
|
||||
@@ -117,13 +117,12 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Clipboard Group
|
||||
export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
type: 'clipboard',
|
||||
export const toggleGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
type: 'toggle',
|
||||
items: [
|
||||
{
|
||||
type: 'wrap',
|
||||
generate: ({ blockComponent, close }) => {
|
||||
generate: ({ blockComponent }) => {
|
||||
return {
|
||||
action: () => {},
|
||||
render: () => {
|
||||
@@ -134,7 +133,6 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
<editor-menu-action
|
||||
@click=${() => {
|
||||
blockComponent.setWrap(!wrapped);
|
||||
close();
|
||||
}}
|
||||
aria-label=${label}
|
||||
>
|
||||
@@ -155,7 +153,7 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
when: ({ std }) =>
|
||||
std.getOptional(CodeBlockConfigExtension.identifier)?.showLineNumbers ??
|
||||
true,
|
||||
generate: ({ blockComponent, close }) => {
|
||||
generate: ({ blockComponent }) => {
|
||||
return {
|
||||
action: () => {},
|
||||
render: () => {
|
||||
@@ -167,8 +165,6 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
blockComponent.store.updateBlock(blockComponent.model, {
|
||||
lineNumber: !lineNumber,
|
||||
});
|
||||
|
||||
close();
|
||||
}}
|
||||
aria-label=${label}
|
||||
>
|
||||
@@ -184,6 +180,13 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Clipboard Group
|
||||
export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
type: 'clipboard',
|
||||
items: [
|
||||
{
|
||||
type: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
@@ -233,6 +236,7 @@ export const deleteGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
};
|
||||
|
||||
export const MORE_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
|
||||
toggleGroup,
|
||||
clipboardGroup,
|
||||
deleteGroup,
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ActionPlacement,
|
||||
DocDisplayMetaProvider,
|
||||
EditorSettingProvider,
|
||||
FeatureFlagService,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
@@ -216,12 +215,7 @@ const conversionsActionGroup = {
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
|
||||
if (
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_embed_doc_with_alias') &&
|
||||
isGfxBlockComponent(block)
|
||||
) {
|
||||
if (isGfxBlockComponent(block)) {
|
||||
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
|
||||
editorSetting?.set?.(
|
||||
'docCanvasPreferView',
|
||||
|
||||
@@ -17,7 +17,6 @@ import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
ActionPlacement,
|
||||
EditorSettingProvider,
|
||||
FeatureFlagService,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
@@ -163,12 +162,7 @@ const conversionsActionGroup = {
|
||||
label: 'Card view',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
|
||||
if (
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_embed_doc_with_alias') &&
|
||||
isGfxBlockComponent(block)
|
||||
) {
|
||||
if (isGfxBlockComponent(block)) {
|
||||
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
|
||||
editorSetting?.set?.(
|
||||
'docCanvasPreferView',
|
||||
@@ -296,8 +290,6 @@ const builtinSurfaceToolbarConfig = {
|
||||
label: 'Insert to page',
|
||||
tooltip: 'Insert to page',
|
||||
icon: InsertIntoPageIcon(),
|
||||
when: ({ std }) =>
|
||||
std.get(FeatureFlagService).getFlag('enable_embed_doc_with_alias'),
|
||||
run: ctx => {
|
||||
const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
|
||||
if (!model) return;
|
||||
@@ -334,8 +326,6 @@ const builtinSurfaceToolbarConfig = {
|
||||
tooltip:
|
||||
'Duplicate as note to create an editable copy, the original remains unchanged.',
|
||||
icon: DuplicateIcon(),
|
||||
when: ({ std }) =>
|
||||
std.get(FeatureFlagService).getFlag('enable_embed_doc_with_alias'),
|
||||
run: ctx => {
|
||||
const { gfx } = ctx;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -51,7 +51,10 @@ export class ImageBlockPageComponent extends SignalWatcher(
|
||||
height: 36px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
|
||||
background: ${unsafeCSSVarV2(
|
||||
'loading/imageLoadingBackground',
|
||||
'#92929238'
|
||||
)};
|
||||
|
||||
& > svg {
|
||||
font-size: 25.71px;
|
||||
@@ -356,7 +359,9 @@ export class ImageBlockPageComponent extends SignalWatcher(
|
||||
? ImageSelectedRect(this._doc.readonly)
|
||||
: null;
|
||||
|
||||
const { loading, error, icon, description } = this.state;
|
||||
const blobUrl = this.block.blobUrl;
|
||||
const caption = this.block.model.props.caption$.value ?? 'Image';
|
||||
const { loading, error, icon, description, needUpload } = this.state;
|
||||
|
||||
return html`
|
||||
<div class="resizable-img" style=${styleMap(imageSize)}>
|
||||
@@ -364,8 +369,8 @@ export class ImageBlockPageComponent extends SignalWatcher(
|
||||
class="drag-target"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
src=${this.block.blobUrl}
|
||||
alt=${this.block.model.props.caption$.value ?? 'Image'}
|
||||
src=${blobUrl}
|
||||
alt=${caption}
|
||||
@error=${this._handleError}
|
||||
/>
|
||||
|
||||
@@ -374,12 +379,16 @@ export class ImageBlockPageComponent extends SignalWatcher(
|
||||
|
||||
${when(loading, () => html`<div class="loading">${icon}</div>`)}
|
||||
${when(
|
||||
error && description,
|
||||
Boolean(error && description),
|
||||
() =>
|
||||
html`<affine-resource-status
|
||||
class="affine-image-status"
|
||||
.message=${description}
|
||||
.reload=${() => this.block.refreshData()}
|
||||
.needUpload=${needUpload}
|
||||
.action=${() =>
|
||||
needUpload
|
||||
? this.block.resourceController.upload()
|
||||
: this.block.refreshData()}
|
||||
></affine-resource-status>`
|
||||
)}
|
||||
`;
|
||||
|
||||
@@ -135,6 +135,7 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
const resovledState = this.resourceController.resolveStateWith({
|
||||
loadingIcon: LoadingIcon({
|
||||
strokeColor: cssVarV2('button/pureWhiteText'),
|
||||
ringColor: cssVarV2('loading/imageLoadingLayer', '#ffffff8f'),
|
||||
}),
|
||||
errorIcon: BrokenImageIcon(),
|
||||
icon: ImageIcon(),
|
||||
|
||||
@@ -42,7 +42,10 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
height: 36px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
|
||||
background: ${unsafeCSSVarV2(
|
||||
'loading/imageLoadingBackground',
|
||||
'#92929238'
|
||||
)};
|
||||
|
||||
& > svg {
|
||||
font-size: 25.71px;
|
||||
@@ -126,6 +129,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
const resovledState = this.resourceController.resolveStateWith({
|
||||
loadingIcon: LoadingIcon({
|
||||
strokeColor: cssVarV2('button/pureWhiteText'),
|
||||
ringColor: cssVarV2('loading/imageLoadingLayer', '#ffffff8f'),
|
||||
}),
|
||||
errorIcon: BrokenImageIcon(),
|
||||
icon: ImageIcon(),
|
||||
@@ -133,6 +137,8 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
description: formatSize(size),
|
||||
});
|
||||
|
||||
const { loading, icon, description, error, needUpload } = resovledState;
|
||||
|
||||
return html`
|
||||
<div class="affine-image-container" style=${containerStyleMap}>
|
||||
${when(
|
||||
@@ -148,17 +154,18 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
||||
@error=${this._handleError}
|
||||
/>
|
||||
</div>
|
||||
${when(loading, () => html`<div class="loading">${icon}</div>`)}
|
||||
${when(
|
||||
resovledState.loading,
|
||||
() => html`<div class="loading">${resovledState.icon}</div>`
|
||||
)}
|
||||
${when(
|
||||
resovledState.error && resovledState.description,
|
||||
Boolean(error && description),
|
||||
() =>
|
||||
html`<affine-resource-status
|
||||
class="affine-image-status"
|
||||
.message=${resovledState.description}
|
||||
.reload=${() => this.refreshData()}
|
||||
.message=${description}
|
||||
.needUpload=${needUpload}
|
||||
.action=${() =>
|
||||
needUpload
|
||||
? this.resourceController.upload()
|
||||
: this.refreshData()}
|
||||
></affine-resource-status>`
|
||||
)}
|
||||
`,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"katex": "^0.16.11",
|
||||
|
||||
@@ -58,7 +58,6 @@ export class LatexBlockComponent extends CaptionedBlockComponent<LatexBlockModel
|
||||
try {
|
||||
katex.render(latex, katexContainer, {
|
||||
displayMode: true,
|
||||
output: 'mathml',
|
||||
});
|
||||
} catch {
|
||||
katexContainer.replaceChildren();
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -40,6 +40,11 @@ export const listBlockStyles = css`
|
||||
font-size: var(--affine-font-base);
|
||||
}
|
||||
|
||||
affine-list code {
|
||||
font-size: calc(var(--affine-font-base) - 3px);
|
||||
padding: 0px 4px 2px;
|
||||
}
|
||||
|
||||
.affine-list-block-container {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
|
||||
@@ -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' }],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -400,7 +400,7 @@ export const EdgelessNoteInteraction =
|
||||
|
||||
onResizeMove(context): void {
|
||||
const { originalBound, newBound, lockRatio, constraint } = context;
|
||||
const { minWidth, minHeight } = constraint;
|
||||
const { minWidth, minHeight, maxHeight, maxWidth } = constraint;
|
||||
|
||||
let scale = initialScale;
|
||||
let edgelessProp = { ...model.props.edgeless };
|
||||
@@ -411,8 +411,8 @@ export const EdgelessNoteInteraction =
|
||||
edgelessProp.scale = scale;
|
||||
}
|
||||
|
||||
newBound.w = clamp(newBound.w, minWidth, Number.MAX_SAFE_INTEGER);
|
||||
newBound.h = clamp(newBound.h, minHeight, Number.MAX_SAFE_INTEGER);
|
||||
newBound.w = clamp(newBound.w, minWidth * scale, maxWidth);
|
||||
newBound.h = clamp(newBound.h, minHeight * scale, maxHeight);
|
||||
|
||||
if (newBound.h > minHeight * scale) {
|
||||
edgelessProp.collapse = true;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@@ -43,6 +43,23 @@ type RendererOptions = {
|
||||
surfaceModel: SurfaceBlockModel;
|
||||
};
|
||||
|
||||
enum UpdateType {
|
||||
ELEMENT_ADDED = 'element-added',
|
||||
ELEMENT_REMOVED = 'element-removed',
|
||||
ELEMENT_UPDATED = 'element-updated',
|
||||
VIEWPORT_CHANGED = 'viewport-changed',
|
||||
SIZE_CHANGED = 'size-changed',
|
||||
ZOOM_STATE_CHANGED = 'zoom-state-changed',
|
||||
}
|
||||
|
||||
interface IncrementalUpdateState {
|
||||
dirtyElementIds: Set<string>;
|
||||
viewportDirty: boolean;
|
||||
sizeDirty: boolean;
|
||||
usePlaceholderDirty: boolean;
|
||||
pendingUpdates: Map<string, UpdateType[]>;
|
||||
}
|
||||
|
||||
const PLACEHOLDER_RESET_STYLES = {
|
||||
border: 'none',
|
||||
borderRadius: '0',
|
||||
@@ -141,6 +158,18 @@ export class DomRenderer {
|
||||
|
||||
private _sizeUpdatedRafId: number | null = null;
|
||||
|
||||
private readonly _updateState: IncrementalUpdateState = {
|
||||
dirtyElementIds: new Set(),
|
||||
viewportDirty: false,
|
||||
sizeDirty: false,
|
||||
usePlaceholderDirty: false,
|
||||
pendingUpdates: new Map(),
|
||||
};
|
||||
|
||||
private _lastViewportBounds: Bound | null = null;
|
||||
private _lastZoom: number | null = null;
|
||||
private _lastUsePlaceholder: boolean = false;
|
||||
|
||||
rootElement: HTMLElement;
|
||||
|
||||
private readonly _elementsMap = new Map<string, HTMLElement>();
|
||||
@@ -186,6 +215,7 @@ export class DomRenderer {
|
||||
private _initViewport() {
|
||||
this._disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
@@ -195,6 +225,7 @@ export class DomRenderer {
|
||||
if (this._sizeUpdatedRafId) return;
|
||||
this._sizeUpdatedRafId = requestConnectedFrame(() => {
|
||||
this._sizeUpdatedRafId = null;
|
||||
this._markSizeDirty();
|
||||
this._resetSize();
|
||||
this._render();
|
||||
this.refresh();
|
||||
@@ -208,6 +239,7 @@ export class DomRenderer {
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this._markUsePlaceholderDirty();
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
@@ -307,6 +339,292 @@ export class DomRenderer {
|
||||
}
|
||||
|
||||
private _render() {
|
||||
this._renderIncremental();
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(payload => {
|
||||
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay(overlay: Overlay) {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement) {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
}
|
||||
|
||||
generateColorProperty(color: Color, fallback?: Color) {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
}
|
||||
|
||||
getColorValue(color: Color, fallback?: Color, real?: boolean) {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getPropertyValue(property: string) {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
}
|
||||
|
||||
removeOverlay(overlay: Overlay) {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a specific element as dirty for incremental updates
|
||||
* @param elementId - The ID of the element to mark as dirty
|
||||
* @param updateType - The type of update (optional, defaults to ELEMENT_UPDATED)
|
||||
*/
|
||||
markElementDirty(
|
||||
elementId: string,
|
||||
updateType: UpdateType = UpdateType.ELEMENT_UPDATED
|
||||
) {
|
||||
this._markElementDirty(elementId, updateType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a full re-render of all elements
|
||||
*/
|
||||
forceFullRender() {
|
||||
this._updateState.viewportDirty = true;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private _markElementDirty(elementId: string, updateType: UpdateType) {
|
||||
this._updateState.dirtyElementIds.add(elementId);
|
||||
const currentUpdates =
|
||||
this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (!currentUpdates.includes(updateType)) {
|
||||
currentUpdates.push(updateType);
|
||||
this._updateState.pendingUpdates.set(elementId, currentUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private _markViewportDirty() {
|
||||
this._updateState.viewportDirty = true;
|
||||
}
|
||||
|
||||
private _markSizeDirty() {
|
||||
this._updateState.sizeDirty = true;
|
||||
}
|
||||
|
||||
private _markUsePlaceholderDirty() {
|
||||
this._updateState.usePlaceholderDirty = true;
|
||||
}
|
||||
|
||||
private _clearUpdateState() {
|
||||
this._updateState.dirtyElementIds.clear();
|
||||
this._updateState.viewportDirty = false;
|
||||
this._updateState.sizeDirty = false;
|
||||
this._updateState.usePlaceholderDirty = false;
|
||||
this._updateState.pendingUpdates.clear();
|
||||
}
|
||||
|
||||
private _isViewportChanged(): boolean {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
|
||||
if (!this._lastViewportBounds || !this._lastZoom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
this._lastViewportBounds.x !== viewportBounds.x ||
|
||||
this._lastViewportBounds.y !== viewportBounds.y ||
|
||||
this._lastViewportBounds.w !== viewportBounds.w ||
|
||||
this._lastViewportBounds.h !== viewportBounds.h ||
|
||||
this._lastZoom !== zoom
|
||||
);
|
||||
}
|
||||
|
||||
private _isUsePlaceholderChanged(): boolean {
|
||||
return this._lastUsePlaceholder !== this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _updateLastState() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
this._lastViewportBounds = {
|
||||
x: viewportBounds.x,
|
||||
y: viewportBounds.y,
|
||||
w: viewportBounds.w,
|
||||
h: viewportBounds.h,
|
||||
} as Bound;
|
||||
this._lastZoom = zoom;
|
||||
this._lastUsePlaceholder = this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _renderIncremental() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
|
||||
const needsFullRender =
|
||||
this._isViewportChanged() ||
|
||||
this._isUsePlaceholderChanged() ||
|
||||
this._updateState.sizeDirty ||
|
||||
this._updateState.viewportDirty ||
|
||||
this._updateState.usePlaceholderDirty;
|
||||
|
||||
if (needsFullRender) {
|
||||
this._renderFull();
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update dirty elements
|
||||
const elementsFromGrid = this.grid.search(viewportBounds, {
|
||||
filter: ['canvas', 'local'],
|
||||
}) as SurfaceElementModel[];
|
||||
|
||||
const visibleElementIds = new Set<string>();
|
||||
|
||||
// 1. Update dirty elements
|
||||
for (const elementModel of elementsFromGrid) {
|
||||
const display = (elementModel.display ?? true) && !elementModel.hidden;
|
||||
if (
|
||||
display &&
|
||||
intersects(getBoundWithRotation(elementModel), viewportBounds)
|
||||
) {
|
||||
visibleElementIds.add(elementModel.id);
|
||||
|
||||
// Only update dirty elements
|
||||
if (this._updateState.dirtyElementIds.has(elementModel.id)) {
|
||||
if (
|
||||
this.usePlaceholder &&
|
||||
!(elementModel as GfxCompatibleInterface).forceFullRender
|
||||
) {
|
||||
this._renderOrUpdatePlaceholder(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
} else {
|
||||
this._renderOrUpdateFullElement(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove elements that are no longer in the grid
|
||||
for (const elementId of this._updateState.dirtyElementIds) {
|
||||
const updateTypes = this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (
|
||||
updateTypes.includes(UpdateType.ELEMENT_REMOVED) ||
|
||||
!visibleElementIds.has(elementId)
|
||||
) {
|
||||
const domElem = this._elementsMap.get(elementId);
|
||||
if (domElem) {
|
||||
domElem.remove();
|
||||
this._elementsMap.delete(elementId);
|
||||
elementsToRemove.push(domElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Notify changes
|
||||
if (addedElements.length > 0 || elementsToRemove.length > 0) {
|
||||
this.elementsUpdated.next({
|
||||
elements: Array.from(this._elementsMap.values()),
|
||||
added: addedElements,
|
||||
removed: elementsToRemove,
|
||||
});
|
||||
}
|
||||
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
}
|
||||
|
||||
private _renderFull() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
@@ -387,100 +705,4 @@ export class DomRenderer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay(overlay: Overlay) {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement) {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
}
|
||||
|
||||
generateColorProperty(color: Color, fallback?: Color) {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
}
|
||||
|
||||
getColorValue(color: Color, fallback?: Color, real?: boolean) {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getPropertyValue(property: string) {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
}
|
||||
|
||||
removeOverlay(overlay: Overlay) {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ export abstract class Overlay extends Extension {
|
||||
]);
|
||||
}
|
||||
|
||||
clear() {}
|
||||
clear() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ export class TableSelection extends BaseSelection {
|
||||
|
||||
static override type = 'table';
|
||||
|
||||
static override recoverable = true;
|
||||
|
||||
readonly data: TableSelectionData;
|
||||
|
||||
constructor({
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
|
||||
@@ -4,10 +4,12 @@ import { html } from 'lit';
|
||||
export const LoadingIcon = ({
|
||||
size = '1em',
|
||||
progress = 0.2,
|
||||
ringColor = cssVarV2('loading/background'),
|
||||
strokeColor = cssVarV2('loading/foreground'),
|
||||
}: {
|
||||
size?: string;
|
||||
progress?: number;
|
||||
ringColor?: string;
|
||||
strokeColor?: string;
|
||||
} = {}) =>
|
||||
html`<svg
|
||||
@@ -28,13 +30,7 @@ export const LoadingIcon = ({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="8"
|
||||
stroke="${cssVarV2('loading/background')}"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="8" stroke="${ringColor}" stroke-width="4" />
|
||||
<circle
|
||||
class="spinner"
|
||||
cx="12"
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ResolvedStateInfoPart = {
|
||||
error: boolean;
|
||||
state: StateKind;
|
||||
url: string | null;
|
||||
needUpload: boolean;
|
||||
};
|
||||
|
||||
export type ResolvedStateInfo = StateInfo & ResolvedStateInfoPart;
|
||||
@@ -41,6 +42,7 @@ export class ResourceController implements Disposable {
|
||||
readonly resolvedState$ = computed<ResolvedStateInfoPart>(() => {
|
||||
const url = this.blobUrl$.value;
|
||||
const {
|
||||
needUpload = false,
|
||||
uploading = false,
|
||||
downloading = false,
|
||||
overSize = false,
|
||||
@@ -57,7 +59,13 @@ export class ResourceController implements Disposable {
|
||||
|
||||
const loading = state === 'uploading' || state === 'loading';
|
||||
|
||||
return { error: hasError, loading, state, url };
|
||||
return {
|
||||
error: hasError,
|
||||
needUpload,
|
||||
loading,
|
||||
state,
|
||||
url,
|
||||
};
|
||||
});
|
||||
|
||||
private engine?: BlobEngine;
|
||||
@@ -92,7 +100,8 @@ export class ResourceController implements Disposable {
|
||||
errorIcon?: TemplateResult;
|
||||
} & StateInfo
|
||||
): ResolvedStateInfo {
|
||||
const { error, loading, state, url } = this.resolvedState$.value;
|
||||
const { error, loading, state, url, needUpload } =
|
||||
this.resolvedState$.value;
|
||||
|
||||
const { icon, title, description, loadingIcon, errorIcon } = info;
|
||||
|
||||
@@ -104,11 +113,11 @@ export class ResourceController implements Disposable {
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
needUpload,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
result.icon = loadingIcon ?? icon;
|
||||
result.title = state === 'uploading' ? 'Uploading...' : 'Loading...';
|
||||
} else if (error) {
|
||||
result.icon = errorIcon ?? icon;
|
||||
result.description = this.state$.value.errorMessage ?? description;
|
||||
@@ -130,13 +139,15 @@ export class ResourceController implements Disposable {
|
||||
if (!blobState$) return;
|
||||
|
||||
const subscription = blobState$.subscribe(state => {
|
||||
let { uploading, downloading } = state;
|
||||
if (state.overSize || state.errorMessage) {
|
||||
let { uploading, downloading, errorMessage } = state;
|
||||
if (state.overSize) {
|
||||
uploading = false;
|
||||
downloading = false;
|
||||
} else if ((uploading || downloading) && errorMessage) {
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
this.updateState({ ...state, uploading, downloading });
|
||||
this.updateState({ ...state, uploading, downloading, errorMessage });
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
@@ -178,6 +189,9 @@ export class ResourceController implements Disposable {
|
||||
}
|
||||
|
||||
async refreshUrlWith(type?: string) {
|
||||
// Resets the state.
|
||||
this.state$.value = {};
|
||||
|
||||
const url = await this.createUrlWith(type);
|
||||
if (!url) return;
|
||||
|
||||
@@ -191,6 +205,21 @@ export class ResourceController implements Disposable {
|
||||
URL.revokeObjectURL(prevUrl);
|
||||
}
|
||||
|
||||
// Re-upload to the server.
|
||||
async upload() {
|
||||
const blobId = this.blobId$.peek();
|
||||
if (!blobId) return;
|
||||
|
||||
const state = this.state$.peek();
|
||||
if (!state.needUpload) return;
|
||||
if (state.uploading) return;
|
||||
|
||||
// Resets the state.
|
||||
this.state$.value = {};
|
||||
|
||||
return await this.engine?.upload(blobId);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
const url = this.blobUrl$.peek();
|
||||
if (!url) return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
fontBaseStyle,
|
||||
panelBaseColorsStyle,
|
||||
} from '@blocksuite/affine-shared/styles';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import {
|
||||
createButtonPopper,
|
||||
stopPropagation,
|
||||
@@ -15,7 +15,8 @@ import { property, query } from 'lit/decorators.js';
|
||||
|
||||
@requiredProperties({
|
||||
message: PropTypes.string,
|
||||
reload: PropTypes.instanceOf(Function),
|
||||
needUpload: PropTypes.boolean,
|
||||
action: PropTypes.instanceOf(Function),
|
||||
})
|
||||
export class ResourceStatus extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
@@ -32,7 +33,7 @@ export class ResourceStatus extends WithDisposable(LitElement) {
|
||||
cursor: pointer;
|
||||
color: ${unsafeCSSVarV2('button/pureWhiteText')};
|
||||
background: ${unsafeCSSVarV2('status/error')};
|
||||
box-shadow: var(--affine-overlay-shadow);
|
||||
box-shadow: ${unsafeCSSVar('overlayShadow')};
|
||||
}
|
||||
|
||||
${panelBaseColorsStyle('.popper')}
|
||||
@@ -43,28 +44,36 @@ export class ResourceStatus extends WithDisposable(LitElement) {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
width: 260px;
|
||||
font-size: var(--affine-font-sm);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
font-size: ${unsafeCSSVar('fontSm')};
|
||||
|
||||
&[data-show] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-feature-settings:
|
||||
'liga' off,
|
||||
'clig' off;
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
button.reload {
|
||||
button.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 12px;
|
||||
@@ -102,23 +111,35 @@ export class ResourceStatus extends WithDisposable(LitElement) {
|
||||
this._popper?.toggle();
|
||||
});
|
||||
this.disposables.addFromEvent(
|
||||
this._reloadButton,
|
||||
this._actionButton,
|
||||
'click',
|
||||
(_: MouseEvent) => {
|
||||
this._popper?.hide();
|
||||
this.reload();
|
||||
this.action();
|
||||
}
|
||||
);
|
||||
this.disposables.add(() => this._popper?.dispose());
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { message, needUpload } = this;
|
||||
const { type, label } = needUpload
|
||||
? {
|
||||
type: 'Upload',
|
||||
label: 'Retry',
|
||||
}
|
||||
: {
|
||||
type: 'Download',
|
||||
label: 'Reload',
|
||||
};
|
||||
|
||||
return html`
|
||||
<button class="status">${InformationIcon()}</button>
|
||||
<div class="popper">
|
||||
<div class="content">${this.message}</div>
|
||||
<div class="header">${type} failed</div>
|
||||
<div class="content">${message}</div>
|
||||
<div class="footer">
|
||||
<button class="reload">Reload</button>
|
||||
<button class="action">${label}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -130,12 +151,15 @@ export class ResourceStatus extends WithDisposable(LitElement) {
|
||||
@query('button.status')
|
||||
private accessor _trigger!: HTMLButtonElement;
|
||||
|
||||
@query('button.reload')
|
||||
private accessor _reloadButton!: HTMLButtonElement;
|
||||
@query('button.action')
|
||||
private accessor _actionButton!: HTMLButtonElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor message!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reload!: () => void;
|
||||
accessor needUpload!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor action!: () => void;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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