mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-09 19:18:07 +00:00
Compare commits
16 Commits
v0.22.0-ca
...
v0.22.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 }}
|
||||
@@ -1,11 +1,12 @@
|
||||
{{- if eq .Values.global.deployment.platform "gcp" -}}
|
||||
apiVersion: monitoring.googleapis.com/v1
|
||||
kind: ClusterPodMonitoring
|
||||
kind: PodMonitoring
|
||||
metadata:
|
||||
name: "{{ include "doc.fullname" . }}"
|
||||
name: "{{ .Release.Name }}-monitoring"
|
||||
spec:
|
||||
selector:
|
||||
{{- include "doc.selectorLabels" . | nindent 4 }}
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
endpoints:
|
||||
- port: 9464
|
||||
interval: 30s
|
||||
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@@ -138,6 +138,7 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
env:
|
||||
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
|
||||
AFFINE_PRO_LICENSE_AES_KEY: ${{ secrets.AFFINE_PRO_LICENSE_AES_KEY }}
|
||||
with:
|
||||
target: ${{ matrix.targets.name }}
|
||||
package: '@affine/server-native'
|
||||
|
||||
57
Cargo.lock
generated
57
Cargo.lock
generated
@@ -20,8 +20,7 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
[[package]]
|
||||
name = "adobe-cmap-parser"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
|
||||
source = "git+https://github.com/darkskygit/adobe-cmap-parser#610513ae6035c63eab69f33299b86c43693cabb4"
|
||||
dependencies = [
|
||||
"pom",
|
||||
]
|
||||
@@ -2737,9 +2736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "path-ext"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de7a86239a8b87b5094977b64893fcf0ed768072744dd4ee0df237686b2d815"
|
||||
checksum = "7603010004b5cdecf8006605bf7b6f07b0e59d3003010f52b767e91bf2582a45"
|
||||
dependencies = [
|
||||
"path-slash",
|
||||
"walkdir",
|
||||
@@ -2754,7 +2753,7 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
||||
[[package]]
|
||||
name = "pdf-extract"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/toeverything/pdf-extract?branch=darksky%2Fimprove-font-decoding#e74beed894e1b8dc228c2bf078ed92814b27759f"
|
||||
source = "git+https://github.com/toeverything/pdf-extract?branch=darksky%2Fimprove-font-decoding#040751a61aba51e7a28217b758c18db4415c3ee4"
|
||||
dependencies = [
|
||||
"adobe-cmap-parser",
|
||||
"cff-parser",
|
||||
@@ -2763,6 +2762,7 @@ dependencies = [
|
||||
"log",
|
||||
"lopdf",
|
||||
"postscript",
|
||||
"rust-embed",
|
||||
"type1-encoding-parser",
|
||||
"unicode-normalization",
|
||||
]
|
||||
@@ -2943,9 +2943,12 @@ checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6"
|
||||
|
||||
[[package]]
|
||||
name = "postscript"
|
||||
version = "0.14.1"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306"
|
||||
checksum = "9a2238e788cf2c9b6edc23b83cf8ccdd4a6380cc9bf0598cc220fac42a55def6"
|
||||
dependencies = [
|
||||
"typeface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
@@ -3333,6 +3336,40 @@ dependencies = [
|
||||
"realfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.101",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@@ -4670,6 +4707,12 @@ dependencies = [
|
||||
"pom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeface"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f6b49e025f4dc953a29b83e4f5a905089117d09fa53491015d7678951b8be1"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
|
||||
@@ -57,7 +57,7 @@ objc2-foundation = "0.3"
|
||||
once_cell = "1"
|
||||
ordered-float = "5"
|
||||
parking_lot = "0.12"
|
||||
path-ext = "0.1.1"
|
||||
path-ext = "0.1.2"
|
||||
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
proptest = "1.3"
|
||||
|
||||
@@ -4393,6 +4393,61 @@ hhh
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[2]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'h6',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'Sources',
|
||||
},
|
||||
],
|
||||
},
|
||||
collapsed: true,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[3]',
|
||||
flavour: 'affine:bookmark',
|
||||
props: {
|
||||
style: 'citation',
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
icon: favicon,
|
||||
footnoteIdentifier: '1',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[4]',
|
||||
flavour: 'affine:embed-linked-doc',
|
||||
props: {
|
||||
style: 'citation',
|
||||
pageId: 'deadbeef',
|
||||
footnoteIdentifier: '2',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[5]',
|
||||
flavour: 'affine:attachment',
|
||||
props: {
|
||||
name: 'test.txt',
|
||||
sourceId: 'abcdefg',
|
||||
footnoteIdentifier: '3',
|
||||
style: 'citation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -4469,6 +4524,38 @@ hhh
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[2]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'h6',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'Sources',
|
||||
},
|
||||
],
|
||||
},
|
||||
collapsed: true,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[3]',
|
||||
flavour: 'affine:bookmark',
|
||||
props: {
|
||||
style: 'citation',
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
icon: favicon,
|
||||
footnoteIdentifier: '1',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
@@ -36,15 +35,7 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
|
||||
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { provider } = context;
|
||||
let enableCitation = false;
|
||||
try {
|
||||
const featureFlagService = provider?.get(FeatureFlagService);
|
||||
enableCitation = !!featureFlagService?.getFlag('enable_citation');
|
||||
} catch {
|
||||
enableCitation = false;
|
||||
}
|
||||
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
|
||||
if (!isFootnoteDefinitionNode(o.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,6 +64,7 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
|
||||
name: fileName,
|
||||
sourceId: blobId,
|
||||
footnoteIdentifier,
|
||||
style: 'citation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
createRecordDetail,
|
||||
createUniComponentFromWebComponent,
|
||||
type DataSource,
|
||||
DataView,
|
||||
dataViewCommonStyle,
|
||||
type DataViewProps,
|
||||
DataViewRootUILogic,
|
||||
type DataViewSelection,
|
||||
type DataViewWidget,
|
||||
type DataViewWidgetProps,
|
||||
@@ -133,8 +133,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
|
||||
|
||||
private _dataSource?: DataSource;
|
||||
|
||||
private readonly dataView = new DataView();
|
||||
|
||||
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
|
||||
return {
|
||||
dispose: this.host.event.bindHotkey(hotkeys, {
|
||||
@@ -232,10 +230,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
|
||||
return this.rootComponent;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.dataView.expose;
|
||||
}
|
||||
|
||||
private renderDatabaseOps() {
|
||||
if (this.store.readonly) {
|
||||
return nothing;
|
||||
@@ -250,68 +244,68 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
|
||||
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
private readonly dataViewRootLogic = new DataViewRootUILogic({
|
||||
virtualPadding$: signal(0),
|
||||
bindHotkey: this._bindHotkey,
|
||||
handleEvent: this._handleEvent,
|
||||
selection$: this.selection$,
|
||||
setSelection: this.setSelection,
|
||||
dataSource: this.dataSource,
|
||||
headerWidget: this.headerWidget,
|
||||
clipboard: this.std.clipboard,
|
||||
notification: {
|
||||
toast: message => {
|
||||
const notification = this.std.getOptional(NotificationProvider);
|
||||
if (notification) {
|
||||
notification.toast(message);
|
||||
} else {
|
||||
toast(this.host, message);
|
||||
}
|
||||
},
|
||||
},
|
||||
eventTrace: (key, params) => {
|
||||
const telemetryService = this.std.getOptional(TelemetryProvider);
|
||||
telemetryService?.track(key, {
|
||||
...(params as TelemetryEventMap[typeof key]),
|
||||
blockId: this.blockId,
|
||||
});
|
||||
},
|
||||
detailPanelConfig: {
|
||||
openDetailPanel: (target, data) => {
|
||||
const peekViewService = this.std.getOptional(PeekViewProvider);
|
||||
if (peekViewService) {
|
||||
const template = createRecordDetail({
|
||||
...data,
|
||||
openDoc: () => {},
|
||||
detail: {
|
||||
header: uniMap(
|
||||
createUniComponentFromWebComponent(BlockRenderer),
|
||||
props => ({
|
||||
...props,
|
||||
host: this.host,
|
||||
})
|
||||
),
|
||||
note: uniMap(
|
||||
createUniComponentFromWebComponent(NoteRenderer),
|
||||
props => ({
|
||||
...props,
|
||||
model: this.model,
|
||||
host: this.host,
|
||||
})
|
||||
),
|
||||
},
|
||||
});
|
||||
return peekViewService.peek({ target, template });
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
override renderBlock() {
|
||||
const peekViewService = this.std.getOptional(PeekViewProvider);
|
||||
const telemetryService = this.std.getOptional(TelemetryProvider);
|
||||
return html`
|
||||
<div contenteditable="false" style="position: relative">
|
||||
${this.dataView.render({
|
||||
virtualPadding$: signal(0),
|
||||
bindHotkey: this._bindHotkey,
|
||||
handleEvent: this._handleEvent,
|
||||
selection$: this.selection$,
|
||||
setSelection: this.setSelection,
|
||||
dataSource: this.dataSource,
|
||||
headerWidget: this.headerWidget,
|
||||
clipboard: this.std.clipboard,
|
||||
notification: {
|
||||
toast: message => {
|
||||
const notification = this.std.getOptional(NotificationProvider);
|
||||
if (notification) {
|
||||
notification.toast(message);
|
||||
} else {
|
||||
toast(this.host, message);
|
||||
}
|
||||
},
|
||||
},
|
||||
eventTrace: (key, params) => {
|
||||
telemetryService?.track(key, {
|
||||
...(params as TelemetryEventMap[typeof key]),
|
||||
blockId: this.blockId,
|
||||
});
|
||||
},
|
||||
detailPanelConfig: {
|
||||
openDetailPanel: (target, data) => {
|
||||
if (peekViewService) {
|
||||
const template = createRecordDetail({
|
||||
...data,
|
||||
openDoc: () => {},
|
||||
detail: {
|
||||
header: uniMap(
|
||||
createUniComponentFromWebComponent(BlockRenderer),
|
||||
props => ({
|
||||
...props,
|
||||
host: this.host,
|
||||
})
|
||||
),
|
||||
note: uniMap(
|
||||
createUniComponentFromWebComponent(NoteRenderer),
|
||||
props => ({
|
||||
...props,
|
||||
model: this.model,
|
||||
host: this.host,
|
||||
})
|
||||
),
|
||||
},
|
||||
});
|
||||
return peekViewService.peek({ target, template });
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
${this.dataViewRootLogic.render()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import type { DataViewUILogicBase } from '@blocksuite/data-view';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import type { Text } from '@blocksuite/store';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { DatabaseBlockComponent } from '../../database-block.js';
|
||||
|
||||
export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
|
||||
export class DatabaseTitle extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.affine-database-title {
|
||||
position: relative;
|
||||
@@ -71,22 +75,23 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
|
||||
`;
|
||||
|
||||
private readonly compositionEnd = () => {
|
||||
this.isComposing$.value = false;
|
||||
this.titleText.replace(0, this.titleText.length, this.input.value);
|
||||
};
|
||||
|
||||
private readonly onBlur = () => {
|
||||
this.isFocus = false;
|
||||
this.isFocus$.value = false;
|
||||
};
|
||||
|
||||
private readonly onFocus = () => {
|
||||
this.isFocus = true;
|
||||
if (this.database?.viewSelection$?.value) {
|
||||
this.database?.setSelection(undefined);
|
||||
this.isFocus$.value = true;
|
||||
if (this.dataViewLogic.selection$.value) {
|
||||
this.dataViewLogic.setSelection(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly onInput = (e: InputEvent) => {
|
||||
this.text = this.input.value;
|
||||
this.text$.value = this.input.value;
|
||||
if (!e.isComposing) {
|
||||
this.titleText.replace(0, this.titleText.length, this.input.value);
|
||||
}
|
||||
@@ -102,9 +107,9 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
|
||||
};
|
||||
|
||||
updateText = () => {
|
||||
if (!this.isFocus) {
|
||||
if (!this.isFocus$.value) {
|
||||
this.input.value = this.titleText.toString();
|
||||
this.text = this.input.value;
|
||||
this.text$.value = this.input.value;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,25 +129,25 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
override render() {
|
||||
const isEmpty = !this.text;
|
||||
const isEmpty = !this.text$.value;
|
||||
|
||||
const classList = classMap({
|
||||
'affine-database-title': true,
|
||||
ellipsis: !this.isFocus,
|
||||
ellipsis: !this.isFocus$.value,
|
||||
});
|
||||
const untitledStyle = styleMap({
|
||||
height: isEmpty ? 'auto' : 0,
|
||||
opacity: isEmpty && !this.isFocus ? 1 : 0,
|
||||
opacity: isEmpty && !this.isFocus$.value ? 1 : 0,
|
||||
});
|
||||
return html` <div
|
||||
class="${classList}"
|
||||
data-title-empty="${isEmpty}"
|
||||
data-title-focus="${this.isFocus}"
|
||||
data-title-focus="${this.isFocus$.value}"
|
||||
>
|
||||
<div class="text" style="${untitledStyle}">Untitled</div>
|
||||
<div class="text">${this.text}</div>
|
||||
<div class="text">${this.text$.value}</div>
|
||||
<textarea
|
||||
.disabled="${this.readonly}"
|
||||
.disabled="${this.readonly$.value}"
|
||||
@input="${this.onInput}"
|
||||
@keydown="${this.onKeyDown}"
|
||||
@copy="${stopPropagation}"
|
||||
@@ -159,23 +164,24 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
|
||||
@query('textarea')
|
||||
private accessor input!: HTMLTextAreaElement;
|
||||
|
||||
@state()
|
||||
accessor isComposing = false;
|
||||
private readonly isComposing$ = signal(false);
|
||||
private readonly isFocus$ = signal(false);
|
||||
|
||||
@state()
|
||||
private accessor isFocus = false;
|
||||
private onPressEnterKey() {
|
||||
this.dataViewLogic.addRow?.('start');
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onPressEnterKey: (() => void) | undefined = undefined;
|
||||
get readonly$() {
|
||||
return this.dataViewLogic.view.readonly$;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor readonly!: boolean;
|
||||
|
||||
@state()
|
||||
private accessor text = '';
|
||||
private readonly text$ = signal('');
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor titleText!: Text;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewLogic!: DataViewUILogicBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
|
||||
export const databaseBlockStyles = css({
|
||||
display: 'block',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
padding: '8px',
|
||||
margin: '8px -8px -8px',
|
||||
});
|
||||
|
||||
export const databaseBlockSelectedStyles = css({
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
borderRadius: '4px',
|
||||
});
|
||||
|
||||
export const databaseOpsStyles = css({
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
cursor: 'pointer',
|
||||
alignItems: 'center',
|
||||
height: 'max-content',
|
||||
fontSize: '16px',
|
||||
color: cssVarV2.icon.primary,
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
|
||||
'@media print': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
export const databaseHeaderBarStyles = css({
|
||||
'@media print': {
|
||||
display: 'none !important',
|
||||
},
|
||||
});
|
||||
|
||||
export const databaseTitleStyles = css({
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const databaseHeaderContainerStyles = css({
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const databaseTitleRowStyles = css({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
marginBottom: '8px',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const databaseToolbarRowStyles = css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const databaseViewBarContainerStyles = css({
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const databaseContentStyles = css({
|
||||
position: 'relative',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
borderRadius: '4px',
|
||||
});
|
||||
@@ -19,15 +19,14 @@ import { getDropResult } from '@blocksuite/affine-widget-drag-handle';
|
||||
import {
|
||||
createRecordDetail,
|
||||
createUniComponentFromWebComponent,
|
||||
DataView,
|
||||
dataViewCommonStyle,
|
||||
type DataViewInstance,
|
||||
type DataViewProps,
|
||||
DataViewRootUILogic,
|
||||
type DataViewSelection,
|
||||
type DataViewUILogicBase,
|
||||
type DataViewWidget,
|
||||
type DataViewWidgetProps,
|
||||
defineUniComponent,
|
||||
ExternalGroupByConfigProvider,
|
||||
lazy,
|
||||
renderUniLit,
|
||||
type SingleView,
|
||||
uniMap,
|
||||
@@ -44,12 +43,23 @@ import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { autoUpdate } from '@floating-ui/dom';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
import { html, nothing } from 'lit';
|
||||
|
||||
import { popSideDetail } from './components/layout.js';
|
||||
import { DatabaseConfigExtension } from './config.js';
|
||||
import { EditorHostKey } from './context/host-context.js';
|
||||
import { DatabaseBlockDataSource } from './data-source.js';
|
||||
import {
|
||||
databaseBlockStyles,
|
||||
databaseContentStyles,
|
||||
databaseHeaderBarStyles,
|
||||
databaseHeaderContainerStyles,
|
||||
databaseOpsStyles,
|
||||
databaseTitleRowStyles,
|
||||
databaseTitleStyles,
|
||||
databaseToolbarRowStyles,
|
||||
databaseViewBarContainerStyles,
|
||||
} from './database-block-styles.js';
|
||||
import { BlockRenderer } from './detail-panel/block-renderer.js';
|
||||
import { NoteRenderer } from './detail-panel/note-renderer.js';
|
||||
import { DatabaseSelection } from './selection.js';
|
||||
@@ -58,52 +68,7 @@ import { getSingleDocIdFromText } from './utils/title-doc.js';
|
||||
import type { DatabaseViewExtensionOptions } from './view';
|
||||
|
||||
export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBlockModel> {
|
||||
static override styles = css`
|
||||
${unsafeCSS(dataViewCommonStyle('affine-database'))}
|
||||
affine-database {
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
background-color: var(--affine-background-primary-color);
|
||||
padding: 8px;
|
||||
margin: 8px -8px -8px;
|
||||
}
|
||||
|
||||
.database-block-selected {
|
||||
background-color: var(--affine-hover-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.database-ops {
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
.database-ops svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
|
||||
.database-ops:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
@media print {
|
||||
.database-ops {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.database-header-bar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _clickDatabaseOps = (e: MouseEvent) => {
|
||||
private readonly clickDatabaseOps = (e: MouseEvent) => {
|
||||
const options = this.optionsConfig.configure(this.model, {
|
||||
items: [
|
||||
menu.input({
|
||||
@@ -155,36 +120,33 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
});
|
||||
};
|
||||
|
||||
private _dataSource?: DatabaseBlockDataSource;
|
||||
private readonly dataSource = lazy(() => {
|
||||
const dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
|
||||
dataSource.serviceSet(EditorHostKey, this.host);
|
||||
this.std.provider
|
||||
.getAll(ExternalGroupByConfigProvider)
|
||||
.forEach(config => {
|
||||
dataSource.serviceSet(
|
||||
ExternalGroupByConfigProvider(config.name),
|
||||
config
|
||||
);
|
||||
});
|
||||
});
|
||||
const id = currentViewStorage.getCurrentView(this.model.id);
|
||||
if (id && dataSource.viewManager.viewGet(id)) {
|
||||
dataSource.viewManager.setCurrentView(id);
|
||||
}
|
||||
return dataSource;
|
||||
});
|
||||
|
||||
private readonly dataView = new DataView();
|
||||
|
||||
private readonly renderTitle = (dataViewMethod: DataViewInstance) => {
|
||||
const addRow = () => dataViewMethod.addRow?.('start');
|
||||
private readonly renderTitle = (dataViewLogic: DataViewUILogicBase) => {
|
||||
return html` <affine-database-title
|
||||
style="overflow: hidden"
|
||||
class="${databaseTitleStyles}"
|
||||
.titleText="${this.model.props.title}"
|
||||
.readonly="${this.dataSource.readonly$.value}"
|
||||
.onPressEnterKey="${addRow}"
|
||||
.dataViewLogic="${dataViewLogic}"
|
||||
></affine-database-title>`;
|
||||
};
|
||||
|
||||
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
|
||||
return {
|
||||
dispose: this.host.event.bindHotkey(hotkeys, {
|
||||
blockId: this.topContenteditableElement?.blockId ?? this.blockId,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
_handleEvent: DataViewProps['handleEvent'] = (name, handler) => {
|
||||
return {
|
||||
dispose: this.host.event.add(name, handler, {
|
||||
blockId: this.blockId,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
createTemplate = (
|
||||
data: {
|
||||
view: SingleView;
|
||||
@@ -218,18 +180,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
headerWidget: DataViewWidget = defineUniComponent(
|
||||
(props: DataViewWidgetProps) => {
|
||||
return html`
|
||||
<div style="margin-bottom: 16px;display:flex;flex-direction: column">
|
||||
<div
|
||||
style="display:flex;gap:12px;margin-bottom: 8px;align-items: center"
|
||||
>
|
||||
${this.renderTitle(props.dataViewInstance)}
|
||||
${this.renderDatabaseOps()}
|
||||
<div class="${databaseHeaderContainerStyles}">
|
||||
<div class="${databaseTitleRowStyles}">
|
||||
${this.renderTitle(props.dataViewLogic)} ${this.renderDatabaseOps()}
|
||||
</div>
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content: space-between;gap: 12px"
|
||||
class="database-header-bar"
|
||||
>
|
||||
<div style="flex:1">
|
||||
<div class="${databaseToolbarRowStyles} ${databaseHeaderBarStyles}">
|
||||
<div class="${databaseViewBarContainerStyles}">
|
||||
${renderUniLit(widgetPresets.viewBar, {
|
||||
...props,
|
||||
onChangeView: id => {
|
||||
@@ -284,7 +240,9 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
return () => {};
|
||||
};
|
||||
|
||||
setSelection = (selection: DataViewSelection | undefined) => {
|
||||
private readonly setSelection = (
|
||||
selection: DataViewSelection | undefined
|
||||
) => {
|
||||
if (selection) {
|
||||
getSelection()?.removeAllRanges();
|
||||
}
|
||||
@@ -301,7 +259,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
);
|
||||
};
|
||||
|
||||
toolsWidget: DataViewWidget = widgetPresets.createTools({
|
||||
private readonly toolsWidget: DataViewWidget = widgetPresets.createTools({
|
||||
table: [
|
||||
widgetPresets.tools.filter,
|
||||
widgetPresets.tools.sort,
|
||||
@@ -318,7 +276,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
],
|
||||
});
|
||||
|
||||
viewSelection$ = computed(() => {
|
||||
private readonly viewSelection$ = computed(() => {
|
||||
const databaseSelection = this.selection.value.find(
|
||||
(selection): selection is DatabaseSelection => {
|
||||
if (selection.blockId !== this.blockId) {
|
||||
@@ -330,28 +288,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
return databaseSelection?.viewSelection;
|
||||
});
|
||||
|
||||
virtualPadding$ = signal(0);
|
||||
|
||||
get dataSource(): DatabaseBlockDataSource {
|
||||
if (!this._dataSource) {
|
||||
this._dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
|
||||
dataSource.serviceSet(EditorHostKey, this.host);
|
||||
this.std.provider
|
||||
.getAll(ExternalGroupByConfigProvider)
|
||||
.forEach(config => {
|
||||
dataSource.serviceSet(
|
||||
ExternalGroupByConfigProvider(config.name),
|
||||
config
|
||||
);
|
||||
});
|
||||
});
|
||||
const id = currentViewStorage.getCurrentView(this.model.id);
|
||||
if (id && this.dataSource.viewManager.viewGet(id)) {
|
||||
this.dataSource.viewManager.setCurrentView(id);
|
||||
}
|
||||
}
|
||||
return this._dataSource;
|
||||
}
|
||||
private readonly virtualPadding$ = signal(0);
|
||||
|
||||
get optionsConfig(): DatabaseViewExtensionOptions {
|
||||
return {
|
||||
@@ -369,15 +306,15 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
return this.rootComponent;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.dataView.expose;
|
||||
}
|
||||
|
||||
private renderDatabaseOps() {
|
||||
if (this.dataSource.readonly$.value) {
|
||||
if (this.dataSource.value.readonly$.value) {
|
||||
return nothing;
|
||||
}
|
||||
return html` <div class="database-ops" @click="${this._clickDatabaseOps}">
|
||||
return html` <div
|
||||
data-testid="database-ops"
|
||||
class="${databaseOpsStyles}"
|
||||
@click="${this.clickDatabaseOps}"
|
||||
>
|
||||
${MoreHorizontalIcon()}
|
||||
</div>`;
|
||||
}
|
||||
@@ -386,6 +323,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
super.connectedCallback();
|
||||
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
this.classList.add(databaseBlockStyles);
|
||||
this.listenFullWidthChange();
|
||||
}
|
||||
|
||||
@@ -402,85 +340,97 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const peekViewService = this.std.getOptional(PeekViewProvider);
|
||||
const telemetryService = this.std.getOptional(TelemetryProvider);
|
||||
return html`
|
||||
<div
|
||||
contenteditable="false"
|
||||
style="position: relative;background-color: var(--affine-background-primary-color);border-radius: 4px"
|
||||
>
|
||||
${this.dataView.render({
|
||||
virtualPadding$: this.virtualPadding$,
|
||||
bindHotkey: this._bindHotkey,
|
||||
handleEvent: this._handleEvent,
|
||||
selection$: this.viewSelection$,
|
||||
setSelection: this.setSelection,
|
||||
dataSource: this.dataSource,
|
||||
headerWidget: this.headerWidget,
|
||||
onDrag: this.onDrag,
|
||||
clipboard: this.std.clipboard,
|
||||
notification: {
|
||||
toast: message => {
|
||||
const notification = this.std.getOptional(NotificationProvider);
|
||||
if (notification) {
|
||||
notification.toast(message);
|
||||
} else {
|
||||
toast(this.host, message);
|
||||
}
|
||||
},
|
||||
},
|
||||
eventTrace: (key, params) => {
|
||||
telemetryService?.track(key, {
|
||||
...(params as TelemetryEventMap[typeof key]),
|
||||
private readonly dataViewRootLogic = lazy(
|
||||
() =>
|
||||
new DataViewRootUILogic({
|
||||
virtualPadding$: this.virtualPadding$,
|
||||
bindHotkey: hotkeys => {
|
||||
return {
|
||||
dispose: this.host.event.bindHotkey(hotkeys, {
|
||||
blockId: this.topContenteditableElement?.blockId ?? this.blockId,
|
||||
}),
|
||||
};
|
||||
},
|
||||
handleEvent: (name, handler) => {
|
||||
return {
|
||||
dispose: this.host.event.add(name, handler, {
|
||||
blockId: this.blockId,
|
||||
});
|
||||
}),
|
||||
};
|
||||
},
|
||||
selection$: this.viewSelection$,
|
||||
setSelection: this.setSelection,
|
||||
dataSource: this.dataSource.value,
|
||||
headerWidget: this.headerWidget,
|
||||
onDrag: this.onDrag,
|
||||
clipboard: this.std.clipboard,
|
||||
notification: {
|
||||
toast: message => {
|
||||
const notification = this.std.getOptional(NotificationProvider);
|
||||
if (notification) {
|
||||
notification.toast(message);
|
||||
} else {
|
||||
toast(this.host, message);
|
||||
}
|
||||
},
|
||||
detailPanelConfig: {
|
||||
openDetailPanel: (target, data) => {
|
||||
if (peekViewService) {
|
||||
const openDoc = (docId: string) => {
|
||||
return peekViewService.peek({
|
||||
docId,
|
||||
databaseId: this.blockId,
|
||||
databaseDocId: this.model.store.id,
|
||||
databaseRowId: data.rowId,
|
||||
target: this,
|
||||
});
|
||||
};
|
||||
const doc = getSingleDocIdFromText(
|
||||
this.model.store.getBlock(data.rowId)?.model?.text
|
||||
);
|
||||
if (doc) {
|
||||
return openDoc(doc);
|
||||
}
|
||||
const abort = new AbortController();
|
||||
return new Promise<void>(focusBack => {
|
||||
peekViewService
|
||||
.peek(
|
||||
{
|
||||
target,
|
||||
template: this.createTemplate(data, docId => {
|
||||
// abort.abort();
|
||||
openDoc(docId).then(focusBack).catch(focusBack);
|
||||
}),
|
||||
},
|
||||
{ abortSignal: abort.signal }
|
||||
)
|
||||
.then(focusBack)
|
||||
.catch(focusBack);
|
||||
},
|
||||
eventTrace: (key, params) => {
|
||||
const telemetryService = this.std.getOptional(TelemetryProvider);
|
||||
telemetryService?.track(key, {
|
||||
...(params as TelemetryEventMap[typeof key]),
|
||||
blockId: this.blockId,
|
||||
});
|
||||
},
|
||||
detailPanelConfig: {
|
||||
openDetailPanel: (target, data) => {
|
||||
const peekViewService = this.std.getOptional(PeekViewProvider);
|
||||
if (peekViewService) {
|
||||
const openDoc = (docId: string) => {
|
||||
return peekViewService.peek({
|
||||
docId,
|
||||
databaseId: this.blockId,
|
||||
databaseDocId: this.model.store.id,
|
||||
databaseRowId: data.rowId,
|
||||
target: this,
|
||||
});
|
||||
} else {
|
||||
return popSideDetail(
|
||||
this.createTemplate(data, () => {
|
||||
//
|
||||
})
|
||||
);
|
||||
};
|
||||
const doc = getSingleDocIdFromText(
|
||||
this.model.store.getBlock(data.rowId)?.model?.text
|
||||
);
|
||||
if (doc) {
|
||||
return openDoc(doc);
|
||||
}
|
||||
},
|
||||
const abort = new AbortController();
|
||||
return new Promise<void>(focusBack => {
|
||||
peekViewService
|
||||
.peek(
|
||||
{
|
||||
target,
|
||||
template: this.createTemplate(data, docId => {
|
||||
// abort.abort();
|
||||
openDoc(docId).then(focusBack).catch(focusBack);
|
||||
}),
|
||||
},
|
||||
{ abortSignal: abort.signal }
|
||||
)
|
||||
.then(focusBack)
|
||||
.catch(focusBack);
|
||||
});
|
||||
} else {
|
||||
return popSideDetail(
|
||||
this.createTemplate(data, () => {
|
||||
//
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
})}
|
||||
},
|
||||
})
|
||||
);
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div contenteditable="false" class="${databaseContentStyles}">
|
||||
${this.dataViewRootLogic.value.render()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' }],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,6 +67,8 @@ export class TableSelection extends BaseSelection {
|
||||
|
||||
static override type = 'table';
|
||||
|
||||
static override recoverable = true;
|
||||
|
||||
readonly data: TableSelectionData;
|
||||
|
||||
constructor({
|
||||
|
||||
@@ -70,3 +70,19 @@ export const dividerV = css({
|
||||
backgroundColor: 'var(--affine-divider-color)',
|
||||
margin: '0 8px',
|
||||
});
|
||||
|
||||
export const dv = {
|
||||
p2,
|
||||
p4,
|
||||
p8,
|
||||
hover,
|
||||
icon16,
|
||||
icon20,
|
||||
border,
|
||||
round4,
|
||||
round8,
|
||||
color2,
|
||||
shadow2,
|
||||
dividerH,
|
||||
dividerV,
|
||||
};
|
||||
|
||||
@@ -2,42 +2,43 @@ import type {
|
||||
DatabaseAllEvents,
|
||||
EventTraceFn,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { DisposableMember } from '@blocksuite/global/disposable';
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import {
|
||||
type Clipboard,
|
||||
type EventName,
|
||||
ShadowlessElement,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/std';
|
||||
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||
import { css, unsafeCSS } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { createRef, ref } from 'lit/directives/ref.js';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { dataViewCommonStyle } from './common/css-variable.js';
|
||||
import type { DataViewSelection, DataViewSelectionState } from './types.js';
|
||||
import type { DataSource } from './data-source/index.js';
|
||||
import type { DataViewSelection } from './types.js';
|
||||
import { cacheComputed } from './utils/cache.js';
|
||||
import { renderUniLit } from './utils/uni-component/index.js';
|
||||
import type { DataViewInstance, DataViewProps } from './view/types.js';
|
||||
import type { DataViewUILogicBase } from './view/data-view-base.js';
|
||||
import type { SingleView } from './view-manager/single-view.js';
|
||||
import type { DataViewWidget } from './widget/index.js';
|
||||
|
||||
type ViewProps = {
|
||||
view: SingleView;
|
||||
selection$: ReadonlySignal<DataViewSelectionState>;
|
||||
setSelection: (selection?: DataViewSelectionState) => void;
|
||||
bindHotkey: DataViewProps['bindHotkey'];
|
||||
handleEvent: DataViewProps['handleEvent'];
|
||||
};
|
||||
|
||||
export type DataViewRendererConfig = Pick<
|
||||
DataViewProps,
|
||||
| 'bindHotkey'
|
||||
| 'handleEvent'
|
||||
| 'virtualPadding$'
|
||||
| 'clipboard'
|
||||
| 'dataSource'
|
||||
| 'headerWidget'
|
||||
| 'onDrag'
|
||||
| 'notification'
|
||||
> & {
|
||||
export type DataViewRendererConfig = {
|
||||
clipboard: Clipboard;
|
||||
onDrag?: (evt: MouseEvent, id: string) => () => void;
|
||||
notification: {
|
||||
toast: (message: string) => void;
|
||||
};
|
||||
virtualPadding$: ReadonlySignal<number>;
|
||||
headerWidget: DataViewWidget | undefined;
|
||||
handleEvent: (name: EventName, handler: UIEventHandler) => DisposableMember;
|
||||
bindHotkey: (hotkeys: Record<string, UIEventHandler>) => DisposableMember;
|
||||
dataSource: DataSource;
|
||||
selection$: ReadonlySignal<DataViewSelection | undefined>;
|
||||
setSelection: (selection: DataViewSelection | undefined) => void;
|
||||
eventTrace: EventTraceFn<DatabaseAllEvents>;
|
||||
@@ -52,7 +53,104 @@ export type DataViewRendererConfig = Pick<
|
||||
};
|
||||
};
|
||||
|
||||
export class DataViewRenderer extends SignalWatcher(
|
||||
export class DataViewRootUILogic {
|
||||
private get dataSource() {
|
||||
return this.config.dataSource;
|
||||
}
|
||||
private get viewManager() {
|
||||
return this.dataSource.viewManager;
|
||||
}
|
||||
private createDataViewUILogic(viewId: string): DataViewUILogicBase {
|
||||
const view = this.viewManager.viewGet(viewId);
|
||||
if (!view) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.DatabaseBlockError,
|
||||
`View ${viewId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
const pcLogic = view.meta.renderer.pcLogic;
|
||||
const mobileLogic = view.meta.renderer.mobileLogic;
|
||||
const logic = (IS_MOBILE ? mobileLogic : pcLogic) ?? pcLogic;
|
||||
|
||||
return new (logic(view))(this, view);
|
||||
}
|
||||
private readonly views$ = cacheComputed(this.viewManager.views$, viewId =>
|
||||
this.createDataViewUILogic(viewId)
|
||||
);
|
||||
private readonly viewsMap$ = computed(() => {
|
||||
return Object.fromEntries(
|
||||
this.views$.list.value.map(logic => [logic.view.id, logic])
|
||||
);
|
||||
});
|
||||
private readonly _uiRef = signal<DataViewRootUI>();
|
||||
|
||||
get selection$() {
|
||||
return this.config.selection$;
|
||||
}
|
||||
|
||||
setSelection(selection?: DataViewSelection) {
|
||||
this.config.setSelection(selection);
|
||||
}
|
||||
|
||||
constructor(public readonly config: DataViewRendererConfig) {}
|
||||
|
||||
get dataViewRenderer() {
|
||||
return this._uiRef.value;
|
||||
}
|
||||
|
||||
readonly currentViewId$ = computed(() => {
|
||||
return this.dataSource.viewManager.currentViewId$.value;
|
||||
});
|
||||
|
||||
readonly currentView$ = computed(() => {
|
||||
const currentViewId = this.currentViewId$.value;
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
return this.viewsMap$.value[currentViewId];
|
||||
});
|
||||
|
||||
focusFirstCell = () => {
|
||||
this.currentView$.value?.focusFirstCell();
|
||||
};
|
||||
|
||||
openDetailPanel = (ops: {
|
||||
view: SingleView;
|
||||
rowId: string;
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const openDetailPanel = this.config.detailPanelConfig.openDetailPanel;
|
||||
const target = this.dataViewRenderer;
|
||||
if (openDetailPanel && target) {
|
||||
openDetailPanel(target, {
|
||||
view: ops.view,
|
||||
rowId: ops.rowId,
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(ops.onClose);
|
||||
}
|
||||
};
|
||||
|
||||
setupViewChangeListener() {
|
||||
let preId: string | undefined = undefined;
|
||||
return this.currentViewId$.subscribe(current => {
|
||||
if (current !== preId) {
|
||||
this.config.setSelection(undefined);
|
||||
}
|
||||
preId = current;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <affine-data-view-renderer
|
||||
${ref(this._uiRef)}
|
||||
.logic="${this}"
|
||||
></affine-data-view-renderer>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataViewRootUI extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
@@ -63,63 +161,14 @@ export class DataViewRenderer extends SignalWatcher(
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _view = signal<DataViewInstance>();
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor config!: DataViewRendererConfig;
|
||||
accessor logic!: DataViewRootUILogic;
|
||||
|
||||
private readonly currentViewId$ = computed(() => {
|
||||
return this.config.dataSource.viewManager.currentViewId$.value;
|
||||
});
|
||||
|
||||
viewMap$ = computed(() => {
|
||||
const manager = this.config.dataSource.viewManager;
|
||||
return Object.fromEntries(
|
||||
manager.views$.value.map(view => [view, manager.viewGet(view)])
|
||||
);
|
||||
});
|
||||
|
||||
currentViewConfig$ = computed<ViewProps | undefined>(() => {
|
||||
const currentViewId = this.currentViewId$.value;
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
const view = this.viewMap$.value[currentViewId];
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
view: view,
|
||||
selection$: computed(() => {
|
||||
const selection$ = this.config.selection$;
|
||||
if (selection$.value?.viewId === currentViewId) {
|
||||
return selection$.value;
|
||||
}
|
||||
return;
|
||||
}),
|
||||
setSelection: selection => {
|
||||
this.config.setSelection(selection);
|
||||
},
|
||||
handleEvent: (name, handler) =>
|
||||
this.config.handleEvent(name, context => {
|
||||
return handler(context);
|
||||
}),
|
||||
bindHotkey: hotkeys =>
|
||||
this.config.bindHotkey(
|
||||
Object.fromEntries(
|
||||
Object.entries(hotkeys).map(([key, fn]) => [
|
||||
key,
|
||||
ctx => {
|
||||
return fn(ctx);
|
||||
},
|
||||
])
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
@state()
|
||||
accessor currentView: string | undefined = undefined;
|
||||
|
||||
focusFirstCell = () => {
|
||||
this.view?.focusFirstCell();
|
||||
this.logic.focusFirstCell();
|
||||
};
|
||||
|
||||
openDetailPanel = (ops: {
|
||||
@@ -127,72 +176,12 @@ export class DataViewRenderer extends SignalWatcher(
|
||||
rowId: string;
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const openDetailPanel = this.config.detailPanelConfig.openDetailPanel;
|
||||
if (openDetailPanel) {
|
||||
openDetailPanel(this, {
|
||||
view: ops.view,
|
||||
rowId: ops.rowId,
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(ops.onClose);
|
||||
}
|
||||
this.logic.openDetailPanel(ops);
|
||||
};
|
||||
|
||||
get view() {
|
||||
return this._view.value;
|
||||
}
|
||||
|
||||
private renderView(viewData?: ViewProps) {
|
||||
if (!viewData) {
|
||||
return;
|
||||
}
|
||||
const props: DataViewProps = {
|
||||
dataViewEle: this,
|
||||
headerWidget: this.config.headerWidget,
|
||||
onDrag: this.config.onDrag,
|
||||
dataSource: this.config.dataSource,
|
||||
virtualPadding$: this.config.virtualPadding$,
|
||||
clipboard: this.config.clipboard,
|
||||
notification: this.config.notification,
|
||||
view: viewData.view,
|
||||
selection$: viewData.selection$,
|
||||
setSelection: viewData.setSelection,
|
||||
bindHotkey: viewData.bindHotkey,
|
||||
handleEvent: viewData.handleEvent,
|
||||
eventTrace: (key, params) => {
|
||||
this.config.eventTrace(key, {
|
||||
...(params as DatabaseAllEvents[typeof key]),
|
||||
viewId: viewData.view.id,
|
||||
viewType: viewData.view.type,
|
||||
});
|
||||
},
|
||||
};
|
||||
const renderer = viewData.view.meta.renderer;
|
||||
const view =
|
||||
(IS_MOBILE ? renderer.mobileView : renderer.view) ?? renderer.view;
|
||||
return keyed(
|
||||
viewData.view.id,
|
||||
renderUniLit(
|
||||
view,
|
||||
{ props },
|
||||
{
|
||||
ref: this._view,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
let preId: string | undefined = undefined;
|
||||
this.disposables.add(
|
||||
this.currentViewId$.subscribe(current => {
|
||||
if (current !== preId) {
|
||||
this.config.setSelection(undefined);
|
||||
}
|
||||
preId = current;
|
||||
})
|
||||
);
|
||||
this.disposables.add(this.logic.setupViewChangeListener());
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -201,34 +190,22 @@ export class DataViewRenderer extends SignalWatcher(
|
||||
'data-view-root': true,
|
||||
'prevent-reference-popup': true,
|
||||
});
|
||||
const currentView = this.logic.currentView$.value;
|
||||
if (!currentView) {
|
||||
return;
|
||||
}
|
||||
return html`
|
||||
<div style="display: contents" class="${containerClass}">
|
||||
${this.renderView(this.currentViewConfig$.value)}
|
||||
${renderUniLit(currentView.renderer, {
|
||||
logic: currentView,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor currentView: string | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-data-view-renderer': DataViewRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataView {
|
||||
private readonly _ref = createRef<DataViewRenderer>();
|
||||
|
||||
get expose() {
|
||||
return this._ref.value?.view;
|
||||
}
|
||||
|
||||
render(props: DataViewRendererConfig) {
|
||||
return html` <affine-data-view-renderer
|
||||
${ref(this._ref)}
|
||||
.config="${props}"
|
||||
></affine-data-view-renderer>`;
|
||||
'affine-data-view-renderer': DataViewRootUI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataViewPropertiesSettingView } from './common/properties.js';
|
||||
import { Button } from './component/button/button.js';
|
||||
import { Overflow } from './component/overflow/overflow.js';
|
||||
import { MultiTagSelect, MultiTagView } from './component/tags/index.js';
|
||||
import { DataViewRenderer } from './data-view.js';
|
||||
import { DataViewRootUI } from './data-view.js';
|
||||
import { RecordDetail } from './detail/detail.js';
|
||||
import { RecordField } from './detail/field.js';
|
||||
import { VariableRefView } from './expression/ref/ref-view.js';
|
||||
@@ -15,7 +15,7 @@ import { AffineLitIcon, UniAnyRender, UniLit } from './index.js';
|
||||
import { AnyRender } from './utils/uni-component/render-template.js';
|
||||
|
||||
export function coreEffects() {
|
||||
customElements.define('affine-data-view-renderer', DataViewRenderer);
|
||||
customElements.define('affine-data-view-renderer', DataViewRootUI);
|
||||
customElements.define('any-render', AnyRender);
|
||||
customElements.define(
|
||||
'data-view-properties-setting',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './common/index.js';
|
||||
export * from './component/index.js';
|
||||
export { DataSourceBase } from './data-source/base.js';
|
||||
export { DataView } from './data-view.js';
|
||||
export { DataViewRootUILogic } from './data-view.js';
|
||||
export * from './filter/index.js';
|
||||
export * from './group-by';
|
||||
export * from './logical/index.js';
|
||||
|
||||
@@ -183,7 +183,6 @@ export class TypeSystem {
|
||||
// eslint-disable-next-line sonarjs/no-collapsible-if
|
||||
if (realArg != null) {
|
||||
if (!this._unify(newCtx, realArg, arg)) {
|
||||
console.log('arg', realArg, arg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
32
blocksuite/affine/data-view/src/core/utils/cache.ts
Normal file
32
blocksuite/affine/data-view/src/core/utils/cache.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
|
||||
export const cacheComputed = <T>(
|
||||
ids: ReadonlySignal<string[]>,
|
||||
create: (id: string) => T
|
||||
) => {
|
||||
const cache = new Map<string, T>();
|
||||
const getOrCreate = (id: string): T => {
|
||||
if (cache.has(id)) {
|
||||
return cache.get(id)!;
|
||||
}
|
||||
const value = create(id);
|
||||
if (value) {
|
||||
cache.set(id, value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return {
|
||||
getOrCreate,
|
||||
list: computed<T[]>(() => {
|
||||
const list = ids.value;
|
||||
const keys = new Set(cache.keys());
|
||||
for (const [cachedId] of cache) {
|
||||
keys.delete(cachedId);
|
||||
}
|
||||
for (const id of keys) {
|
||||
cache.delete(id);
|
||||
}
|
||||
return list.map(id => getOrCreate(id));
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './lazy.js';
|
||||
export * from './uni-component/index.js';
|
||||
export * from './uni-icon.js';
|
||||
|
||||
11
blocksuite/affine/data-view/src/core/utils/lazy.ts
Normal file
11
blocksuite/affine/data-view/src/core/utils/lazy.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const lazy = <T>(fn: () => T): { value: T } => {
|
||||
let data: { value: T } | undefined;
|
||||
return {
|
||||
get value() {
|
||||
if (!data) {
|
||||
data = { value: fn() };
|
||||
}
|
||||
return data.value;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,17 +1,106 @@
|
||||
import type {
|
||||
DatabaseAllEvents,
|
||||
DatabaseAllViewEvents,
|
||||
EventTraceFn,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { UniComponent } from '@blocksuite/affine-shared/types';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import type { DisposableMember } from '@blocksuite/global/disposable';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import {
|
||||
type EventName,
|
||||
ShadowlessElement,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/std';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { DataViewRootUILogic } from '../data-view.js';
|
||||
import type { DataViewSelection } from '../types.js';
|
||||
import type { SingleView } from '../view-manager/single-view.js';
|
||||
import type { DataViewWidget } from '../widget/index.js';
|
||||
import type { DataViewInstance, DataViewProps } from './types.js';
|
||||
|
||||
export abstract class DataViewBase<
|
||||
T extends SingleView = SingleView,
|
||||
Selection extends DataViewSelection = DataViewSelection,
|
||||
> extends SignalWatcher(WithDisposable(ShadowlessElement)) {
|
||||
abstract expose: DataViewInstance;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor props!: DataViewProps<T, Selection>;
|
||||
accessor props!: DataViewProps<Selection>;
|
||||
}
|
||||
export abstract class DataViewUIBase<
|
||||
Logic extends DataViewUILogicBase = DataViewUILogicBase,
|
||||
> extends SignalWatcher(WithDisposable(ShadowlessElement)) {
|
||||
@property({ attribute: false })
|
||||
accessor logic!: Logic;
|
||||
}
|
||||
|
||||
export abstract class DataViewUILogicBase<
|
||||
T extends SingleView = SingleView,
|
||||
Selection extends DataViewSelection = DataViewSelection,
|
||||
> {
|
||||
constructor(
|
||||
public readonly root: DataViewRootUILogic,
|
||||
public readonly view: T
|
||||
) {}
|
||||
|
||||
get headerWidget(): DataViewWidget | undefined {
|
||||
return this.root.config.headerWidget;
|
||||
}
|
||||
bindHotkey(hotkeys: Record<string, UIEventHandler>): DisposableMember {
|
||||
return this.root.config.bindHotkey(
|
||||
Object.fromEntries(
|
||||
Object.entries(hotkeys).map(([key, fn]) => [
|
||||
key,
|
||||
ctx => {
|
||||
return fn(ctx);
|
||||
},
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
handleEvent(name: EventName, handler: UIEventHandler): DisposableMember {
|
||||
return this.root.config.handleEvent(name, context => {
|
||||
return handler(context);
|
||||
});
|
||||
}
|
||||
setSelection(selection?: Selection): void {
|
||||
this.root.setSelection(selection);
|
||||
}
|
||||
|
||||
selection$ = computed<Selection | undefined>(() => {
|
||||
const selection$ = this.root.selection$;
|
||||
if (selection$.value?.viewId === this.view.id) {
|
||||
return selection$.value as Selection | undefined;
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
eventTrace: EventTraceFn<DatabaseAllViewEvents> = (key, params) => {
|
||||
this.root.config.eventTrace(key, {
|
||||
...(params as DatabaseAllEvents[typeof key]),
|
||||
viewId: this.view.id,
|
||||
viewType: this.view.type,
|
||||
});
|
||||
};
|
||||
|
||||
abstract clearSelection: () => void;
|
||||
abstract addRow: (position: InsertToPosition) => string | undefined;
|
||||
abstract focusFirstCell: () => void;
|
||||
abstract showIndicator: (evt: MouseEvent) => boolean;
|
||||
abstract hideIndicator: () => void;
|
||||
abstract moveTo: (id: string, evt: MouseEvent) => void;
|
||||
|
||||
abstract renderer: UniComponent<{
|
||||
logic: DataViewUILogicBase<T, Selection>;
|
||||
}>;
|
||||
}
|
||||
|
||||
type Constructor<T extends abstract new (...args: any) => any> = new (
|
||||
...args: ConstructorParameters<T>
|
||||
) => InstanceType<T>;
|
||||
|
||||
export type DataViewUILogicBaseConstructor = Constructor<
|
||||
typeof DataViewUILogicBase
|
||||
>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { UniComponent } from '@blocksuite/affine-shared/types';
|
||||
|
||||
import type { SingleView } from '../view-manager/single-view.js';
|
||||
import type { ViewManager } from '../view-manager/view-manager.js';
|
||||
import type { DataViewUILogicBaseConstructor } from './data-view-base.js';
|
||||
import type { DataViewInstance, DataViewProps } from './types.js';
|
||||
|
||||
export type BasicViewDataType<
|
||||
@@ -48,9 +49,10 @@ type DataViewComponent = UniComponent<
|
||||
>;
|
||||
|
||||
export interface DataViewRendererConfig {
|
||||
view: DataViewComponent;
|
||||
mobileView?: DataViewComponent;
|
||||
icon: UniComponent;
|
||||
pcLogic: (view: SingleView) => DataViewUILogicBaseConstructor;
|
||||
mobileLogic?: (view: SingleView) => DataViewUILogicBaseConstructor;
|
||||
}
|
||||
|
||||
export type ViewMeta<
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './convert.js';
|
||||
export * from './data-view.js';
|
||||
export * from './data-view-base.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -4,44 +4,21 @@ import type {
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import type { Disposable } from '@blocksuite/global/disposable';
|
||||
import type { Clipboard, EventName, UIEventHandler } from '@blocksuite/std';
|
||||
import type { EventName, UIEventHandler } from '@blocksuite/std';
|
||||
import type { ReadonlySignal } from '@preact/signals-core';
|
||||
|
||||
import type { DataSource } from '../common/index.js';
|
||||
import type { DataViewRenderer } from '../data-view.js';
|
||||
import type { DataViewSelection } from '../types.js';
|
||||
import type { SingleView } from '../view-manager/index.js';
|
||||
import type { DataViewWidget } from '../widget/index.js';
|
||||
|
||||
export interface DataViewProps<
|
||||
T extends SingleView = SingleView,
|
||||
Selection extends DataViewSelection = DataViewSelection,
|
||||
> {
|
||||
dataViewEle: DataViewRenderer;
|
||||
|
||||
headerWidget?: DataViewWidget;
|
||||
|
||||
view: T;
|
||||
dataSource: DataSource;
|
||||
|
||||
bindHotkey: (hotkeys: Record<string, UIEventHandler>) => Disposable;
|
||||
|
||||
handleEvent: (name: EventName, handler: UIEventHandler) => Disposable;
|
||||
|
||||
setSelection: (selection?: Selection) => void;
|
||||
|
||||
selection$: ReadonlySignal<Selection | undefined>;
|
||||
|
||||
virtualPadding$: ReadonlySignal<number>;
|
||||
|
||||
onDrag?: (evt: MouseEvent, id: string) => () => void;
|
||||
|
||||
clipboard: Clipboard;
|
||||
|
||||
notification: {
|
||||
toast: (message: string) => void;
|
||||
};
|
||||
|
||||
eventTrace: EventTraceFn<DatabaseAllViewEvents>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { UniComponent } from '@blocksuite/affine-shared/types';
|
||||
|
||||
import type { DataViewInstance } from '../view/types.js';
|
||||
import type { DataViewUILogicBase } from '../view/data-view-base.js';
|
||||
|
||||
export type DataViewWidgetProps = {
|
||||
dataViewInstance: DataViewInstance;
|
||||
export type DataViewWidgetProps<
|
||||
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
|
||||
> = {
|
||||
dataViewLogic: ViewLogic;
|
||||
};
|
||||
export type DataViewWidget = UniComponent<DataViewWidgetProps>;
|
||||
|
||||
@@ -2,30 +2,27 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { DataViewInstance } from '../view/types.js';
|
||||
import type { SingleView } from '../view-manager/index.js';
|
||||
import type { DataViewUILogicBase } from '../view/data-view-base.js';
|
||||
import type { DataViewWidgetProps } from './types.js';
|
||||
|
||||
export class WidgetBase<View extends SingleView = SingleView>
|
||||
export class WidgetBase<
|
||||
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
|
||||
>
|
||||
extends SignalWatcher(WithDisposable(ShadowlessElement))
|
||||
implements DataViewWidgetProps
|
||||
implements DataViewWidgetProps<ViewLogic>
|
||||
{
|
||||
get dataSource() {
|
||||
return this.view.manager.dataSource;
|
||||
return this.viewManager.dataSource;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.dataViewInstance.view;
|
||||
return this.dataViewLogic.view;
|
||||
}
|
||||
|
||||
get viewManager() {
|
||||
return this.view.manager;
|
||||
}
|
||||
|
||||
get viewMethods() {
|
||||
return this.dataViewInstance;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewInstance!: DataViewInstance<View>;
|
||||
accessor dataViewLogic!: ViewLogic;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,7 @@
|
||||
import { DataViewKanban, TableViewSelector } from './index.js';
|
||||
import { MobileKanbanCard } from './kanban/mobile/card.js';
|
||||
import { MobileKanbanCell } from './kanban/mobile/cell.js';
|
||||
import { MobileKanbanGroup } from './kanban/mobile/group.js';
|
||||
import { MobileDataViewKanban } from './kanban/mobile/kanban-view.js';
|
||||
import { KanbanCard } from './kanban/pc/card.js';
|
||||
import { KanbanCell } from './kanban/pc/cell.js';
|
||||
import { KanbanGroup } from './kanban/pc/group.js';
|
||||
import { KanbanHeader } from './kanban/pc/header.js';
|
||||
import { MobileTableCell } from './table/mobile/cell.js';
|
||||
import { MobileTableColumnHeader } from './table/mobile/column-header.js';
|
||||
import { MobileTableGroup } from './table/mobile/group.js';
|
||||
import { MobileTableHeader } from './table/mobile/header.js';
|
||||
import { MobileTableRow } from './table/mobile/row.js';
|
||||
import { MobileDataViewTable } from './table/mobile/table-view.js';
|
||||
import { pcEffects } from './table/pc/effect.js';
|
||||
import { pcVirtualEffects } from './table/pc-virtual/effect.js';
|
||||
import { DataBaseColumnStats } from './table/stats/column-stats-bar.js';
|
||||
import { DatabaseColumnStatsCell } from './table/stats/column-stats-column.js';
|
||||
import { kanbanEffects } from './kanban/effect.js';
|
||||
import { tableEffects } from './table/effect.js';
|
||||
|
||||
export function viewPresetsEffects() {
|
||||
customElements.define('affine-data-view-kanban-card', KanbanCard);
|
||||
customElements.define('mobile-kanban-card', MobileKanbanCard);
|
||||
customElements.define('affine-data-view-kanban-cell', KanbanCell);
|
||||
customElements.define('mobile-kanban-cell', MobileKanbanCell);
|
||||
customElements.define('affine-data-view-kanban-group', KanbanGroup);
|
||||
customElements.define('mobile-kanban-group', MobileKanbanGroup);
|
||||
customElements.define('affine-data-view-kanban', DataViewKanban);
|
||||
customElements.define('mobile-data-view-kanban', MobileDataViewKanban);
|
||||
customElements.define('affine-data-view-kanban-header', KanbanHeader);
|
||||
|
||||
customElements.define('mobile-table-cell', MobileTableCell);
|
||||
customElements.define('mobile-table-group', MobileTableGroup);
|
||||
customElements.define('mobile-data-view-table', MobileDataViewTable);
|
||||
customElements.define('mobile-table-header', MobileTableHeader);
|
||||
customElements.define('mobile-table-column-header', MobileTableColumnHeader);
|
||||
customElements.define('mobile-table-row', MobileTableRow);
|
||||
|
||||
customElements.define('affine-database-column-stats', DataBaseColumnStats);
|
||||
customElements.define(
|
||||
'affine-database-column-stats-cell',
|
||||
DatabaseColumnStatsCell
|
||||
);
|
||||
customElements.define('affine-database-table-selector', TableViewSelector);
|
||||
|
||||
pcEffects();
|
||||
pcVirtualEffects();
|
||||
kanbanEffects();
|
||||
tableEffects();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mobileEffects } from './mobile/effect.js';
|
||||
import { pcEffects } from './pc/effect.js';
|
||||
|
||||
export function kanbanEffects() {
|
||||
pcEffects();
|
||||
mobileEffects();
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './define.js';
|
||||
export * from './kanban-view-manager.js';
|
||||
export * from './pc/kanban-view.js';
|
||||
export * from './renderer.js';
|
||||
export * from './selection.js';
|
||||
|
||||
@@ -10,8 +10,8 @@ import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanColumn } from '../kanban-view-manager.js';
|
||||
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
import { popCardMenu } from './menu.js';
|
||||
|
||||
const styles = css`
|
||||
@@ -94,7 +94,7 @@ export class MobileKanbanCard extends SignalWatcher(
|
||||
|
||||
private readonly clickCenterPeek = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.dataViewEle.openDetailPanel({
|
||||
this.kanbanViewLogic.root.openDetailPanel({
|
||||
view: this.view,
|
||||
rowId: this.cardId,
|
||||
});
|
||||
@@ -104,10 +104,9 @@ export class MobileKanbanCard extends SignalWatcher(
|
||||
e.stopPropagation();
|
||||
popCardMenu(
|
||||
popupTargetFromElement(e.currentTarget as HTMLElement),
|
||||
this.view,
|
||||
this.groupKey,
|
||||
this.cardId,
|
||||
this.dataViewEle
|
||||
this.kanbanViewLogic
|
||||
);
|
||||
};
|
||||
|
||||
@@ -126,10 +125,10 @@ export class MobileKanbanCard extends SignalWatcher(
|
||||
return html` <mobile-kanban-cell
|
||||
.contentOnly="${false}"
|
||||
data-column-id="${column.id}"
|
||||
.view="${this.view}"
|
||||
.groupKey="${this.groupKey}"
|
||||
.column="${column}"
|
||||
.cardId="${this.cardId}"
|
||||
.kanbanViewLogic="${this.kanbanViewLogic}"
|
||||
></mobile-kanban-cell>`;
|
||||
}
|
||||
)}
|
||||
@@ -184,10 +183,10 @@ export class MobileKanbanCard extends SignalWatcher(
|
||||
<mobile-kanban-cell
|
||||
.contentOnly="${true}"
|
||||
data-column-id="${title.id}"
|
||||
.view="${this.view}"
|
||||
.groupKey="${this.groupKey}"
|
||||
.column="${title}"
|
||||
.cardId="${this.cardId}"
|
||||
.kanbanViewLogic="${this.kanbanViewLogic}"
|
||||
></mobile-kanban-cell>
|
||||
</div>`;
|
||||
}
|
||||
@@ -205,9 +204,6 @@ export class MobileKanbanCard extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor cardId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor groupKey!: string;
|
||||
|
||||
@@ -215,7 +211,11 @@ export class MobileKanbanCard extends SignalWatcher(
|
||||
accessor isFocus = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
accessor kanbanViewLogic!: MobileKanbanViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.kanbanViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
} from '../../../core/property/index.js';
|
||||
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
|
||||
import type { Property } from '../../../core/view-manager/property.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
mobile-kanban-cell {
|
||||
@@ -53,7 +53,7 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
private readonly _cell = signal<DataViewCellLifeCycle>();
|
||||
|
||||
isSelectionEditing$ = computed(() => {
|
||||
const selection = this.kanban?.props.selection$.value;
|
||||
const selection = this.kanbanViewLogic.selection$.value;
|
||||
if (selection?.selectionType !== 'cell') {
|
||||
return false;
|
||||
}
|
||||
@@ -73,8 +73,8 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
if (this.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const setSelection = this.kanban?.props.setSelection;
|
||||
const viewId = this.kanban?.props.view.id;
|
||||
const setSelection = this.kanbanViewLogic.setSelection;
|
||||
const viewId = this.kanbanViewLogic.view.id;
|
||||
if (setSelection && viewId) {
|
||||
if (editing && this.cell?.beforeEnterEditMode() === false) {
|
||||
return;
|
||||
@@ -95,14 +95,6 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
return this._cell.value;
|
||||
}
|
||||
|
||||
get kanban() {
|
||||
return this.closest('mobile-data-view-kanban');
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.closest('mobile-data-view-kanban')?.props.selection$.value;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.column.readonly$.value) return;
|
||||
@@ -172,7 +164,11 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
isEditing$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
accessor kanbanViewLogic!: MobileKanbanViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.kanbanViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MobileKanbanCard } from './card.js';
|
||||
import { MobileKanbanCell } from './cell.js';
|
||||
import { MobileKanbanGroup } from './group.js';
|
||||
import { MobileKanbanViewUI } from './kanban-view-ui-logic.js';
|
||||
|
||||
export function mobileEffects() {
|
||||
customElements.define('mobile-kanban-card', MobileKanbanCard);
|
||||
customElements.define('mobile-kanban-cell', MobileKanbanCell);
|
||||
customElements.define('mobile-kanban-group', MobileKanbanGroup);
|
||||
customElements.define('mobile-data-view-kanban-ui', MobileKanbanViewUI);
|
||||
}
|
||||
@@ -11,11 +11,10 @@ import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { GroupTitle } from '../../../core/group-by/group-title.js';
|
||||
import type { Group } from '../../../core/group-by/trait.js';
|
||||
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
mobile-kanban-group {
|
||||
@@ -112,9 +111,8 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
<mobile-kanban-card
|
||||
data-card-id="${row.rowId}"
|
||||
.groupKey="${this.group.key}"
|
||||
.dataViewEle="${this.dataViewEle}"
|
||||
.view="${this.view}"
|
||||
.cardId="${row.rowId}"
|
||||
.kanbanViewLogic="${this.kanbanViewLogic}"
|
||||
></mobile-kanban-card>
|
||||
`;
|
||||
}
|
||||
@@ -133,14 +131,15 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group!: Group;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
accessor kanbanViewLogic!: MobileKanbanViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.kanbanViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { css } from '@emotion/css';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import {
|
||||
createUniComponentFromWebComponent,
|
||||
renderUniLit,
|
||||
} from '../../../core/index.js';
|
||||
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
|
||||
import {
|
||||
DataViewUIBase,
|
||||
DataViewUILogicBase,
|
||||
} from '../../../core/view/data-view-base.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewSelectionWithType } from '../selection';
|
||||
|
||||
const mobileKanbanViewWrapper = css({
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const mobileKanbanGroups = css({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
paddingBottom: '4px',
|
||||
overflowX: 'scroll',
|
||||
overflowY: 'hidden',
|
||||
});
|
||||
|
||||
const mobileAddGroup = css({
|
||||
height: '32px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
color: `var(${unsafeCSSVarV2('icon/primary')})`,
|
||||
});
|
||||
|
||||
export class MobileKanbanViewUILogic extends DataViewUILogicBase<
|
||||
KanbanSingleView,
|
||||
KanbanViewSelectionWithType
|
||||
> {
|
||||
ui$ = signal<MobileKanbanViewUI | undefined>(undefined);
|
||||
|
||||
private get readonly() {
|
||||
return this.view.readonly$.value;
|
||||
}
|
||||
|
||||
clearSelection = () => {};
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
return this.view.rowAdd(position);
|
||||
};
|
||||
|
||||
focusFirstCell = () => {};
|
||||
|
||||
showIndicator = (_evt: MouseEvent) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
hideIndicator = () => {};
|
||||
|
||||
moveTo = () => {};
|
||||
|
||||
get groupManager() {
|
||||
return this.view.groupTrait;
|
||||
}
|
||||
|
||||
renderAddGroup = () => {
|
||||
const addGroup = this.groupManager.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = this.groupManager.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.view.manager.dataSource,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div class="${mobileAddGroup}" @click="${add}">
|
||||
${AddCursorIcon()}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
renderer = createUniComponentFromWebComponent(MobileKanbanViewUI);
|
||||
}
|
||||
|
||||
export class MobileKanbanViewUI extends DataViewUIBase<MobileKanbanViewUILogic> {
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.logic.ui$.value = this;
|
||||
this.classList.add(mobileKanbanViewWrapper);
|
||||
}
|
||||
|
||||
override render(): TemplateResult {
|
||||
const groups = this.logic.groupManager.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return html``;
|
||||
}
|
||||
const vPadding = this.logic.root.config.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.logic.headerWidget, {
|
||||
dataViewLogic: this.logic,
|
||||
})}
|
||||
<div class="${mobileKanbanGroups}" style="${wrapperStyle}">
|
||||
${repeat(
|
||||
groups,
|
||||
group => group.key,
|
||||
group => {
|
||||
return html` <mobile-kanban-group
|
||||
${sortable(group.key)}
|
||||
data-key="${group.key}"
|
||||
.kanbanViewLogic="${this.logic}"
|
||||
.group="${group}"
|
||||
></mobile-kanban-group>`;
|
||||
}
|
||||
)}
|
||||
${this.logic.renderAddGroup()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-data-view-kanban-ui': MobileKanbanViewUI;
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { css } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
|
||||
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
|
||||
import { DataViewBase } from '../../../core/view/data-view-base.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewSelectionWithType } from '../selection';
|
||||
|
||||
const styles = css`
|
||||
mobile-data-view-kanban {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mobile-kanban-groups {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding-bottom: 4px;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.mobile-add-group {
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
`;
|
||||
|
||||
export class MobileDataViewKanban extends DataViewBase<
|
||||
KanbanSingleView,
|
||||
KanbanViewSelectionWithType
|
||||
> {
|
||||
static override styles = styles;
|
||||
|
||||
renderAddGroup = () => {
|
||||
const addGroup = this.groupManager.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = this.groupManager.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(
|
||||
() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.props.view.manager.dataSource,
|
||||
}) as never
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div class="mobile-add-group" @click="${add}">
|
||||
${AddCursorIcon()}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
get expose(): DataViewInstance {
|
||||
return {
|
||||
clearSelection: () => {},
|
||||
focusFirstCell: () => {},
|
||||
getSelection: () => {
|
||||
return this.props.selection$.value;
|
||||
},
|
||||
hideIndicator: () => {},
|
||||
moveTo: () => {},
|
||||
showIndicator: () => {
|
||||
return false;
|
||||
},
|
||||
view: this.props.view,
|
||||
eventTrace: this.props.eventTrace,
|
||||
};
|
||||
}
|
||||
|
||||
get groupManager() {
|
||||
return this.props.view.groupTrait;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const groups = this.groupManager.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return html``;
|
||||
}
|
||||
const vPadding = this.props.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.props.headerWidget, {
|
||||
dataViewInstance: this.expose,
|
||||
})}
|
||||
<div class="mobile-kanban-groups" style="${wrapperStyle}">
|
||||
${repeat(
|
||||
groups,
|
||||
group => group.key,
|
||||
group => {
|
||||
return html` <mobile-kanban-group
|
||||
${sortable(group.key)}
|
||||
data-key="${group.key}"
|
||||
.dataViewEle="${this.props.dataViewEle}"
|
||||
.view="${this.props.view}"
|
||||
.group="${group}"
|
||||
></mobile-kanban-group>`;
|
||||
}
|
||||
)}
|
||||
${this.renderAddGroup()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-data-view-kanban': MobileDataViewKanban;
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,16 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { groupTraitKey } from '../../../core/group-by/trait.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
|
||||
export const popCardMenu = (
|
||||
ele: PopupTarget,
|
||||
view: KanbanSingleView,
|
||||
groupKey: string,
|
||||
cardId: string,
|
||||
dataViewEle: DataViewRenderer
|
||||
kanbanViewLogic: MobileKanbanViewUILogic
|
||||
) => {
|
||||
const groupTrait = view.traitGet(groupTraitKey);
|
||||
const groupTrait = kanbanViewLogic.view.traitGet(groupTraitKey);
|
||||
if (!groupTrait) {
|
||||
return;
|
||||
}
|
||||
@@ -34,8 +32,8 @@ export const popCardMenu = (
|
||||
name: 'Expand Card',
|
||||
prefix: ExpandFullIcon(),
|
||||
select: () => {
|
||||
dataViewEle.openDetailPanel({
|
||||
view: view,
|
||||
kanbanViewLogic.root.openDetailPanel({
|
||||
view: kanbanViewLogic.view,
|
||||
rowId: cardId,
|
||||
});
|
||||
},
|
||||
@@ -81,7 +79,10 @@ export const popCardMenu = (
|
||||
${MoveLeftIcon()}
|
||||
</div>`,
|
||||
select: () => {
|
||||
view.addCard({ before: true, id: cardId }, groupKey);
|
||||
kanbanViewLogic.view.addCard(
|
||||
{ before: true, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
@@ -92,7 +93,10 @@ export const popCardMenu = (
|
||||
${MoveRightIcon()}
|
||||
</div>`,
|
||||
select: () => {
|
||||
view.addCard({ before: false, id: cardId }, groupKey);
|
||||
kanbanViewLogic.view.addCard(
|
||||
{ before: false, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -106,7 +110,7 @@ export const popCardMenu = (
|
||||
},
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
view.rowsDelete([cardId]);
|
||||
kanbanViewLogic.view.rowsDelete([cardId]);
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -2,15 +2,16 @@ import { popupTargetFromElement } from '@blocksuite/affine-components/context-me
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { CenterPeekIcon, MoreHorizontalIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, unsafeCSS } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanColumn } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
import { openDetail, popCardMenu } from './menu.js';
|
||||
|
||||
const styles = css`
|
||||
@@ -130,7 +131,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
e.stopPropagation();
|
||||
const selection = this.getSelection();
|
||||
if (selection) {
|
||||
openDetail(this.dataViewEle, this.cardId, selection);
|
||||
openDetail(this.kanbanViewLogic, this.cardId, selection);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,7 +150,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
],
|
||||
};
|
||||
popCardMenu(
|
||||
this.dataViewEle,
|
||||
this.kanbanViewLogic,
|
||||
popupTargetFromElement(ele),
|
||||
this.cardId,
|
||||
selection
|
||||
@@ -174,7 +175,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
const target = e.target as HTMLElement;
|
||||
const ref = target.closest('affine-data-view-kanban-cell') ?? this;
|
||||
popCardMenu(
|
||||
this.dataViewEle,
|
||||
this.kanbanViewLogic,
|
||||
popupTargetFromElement(ref),
|
||||
this.cardId,
|
||||
selection
|
||||
@@ -183,7 +184,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
};
|
||||
|
||||
private getSelection() {
|
||||
return this.closest('affine-data-view-kanban')?.selectionController;
|
||||
return this.kanbanViewLogic.selectionController;
|
||||
}
|
||||
|
||||
private renderBody(columns: KanbanColumn[]) {
|
||||
@@ -201,10 +202,10 @@ export class KanbanCard extends SignalWatcher(
|
||||
return html` <affine-data-view-kanban-cell
|
||||
.contentOnly="${false}"
|
||||
data-column-id="${column.id}"
|
||||
.view="${this.view}"
|
||||
.groupKey="${this.groupKey}"
|
||||
.column="${column}"
|
||||
.cardId="${this.cardId}"
|
||||
.kanbanViewLogic="${this.kanbanViewLogic}"
|
||||
></affine-data-view-kanban-cell>`;
|
||||
}
|
||||
)}
|
||||
@@ -259,7 +260,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
<affine-data-view-kanban-cell
|
||||
.contentOnly="${true}"
|
||||
data-column-id="${title.id}"
|
||||
.view="${this.view}"
|
||||
.kanbanViewLogic="${this.kanbanViewLogic}"
|
||||
.groupKey="${this.groupKey}"
|
||||
.column="${title}"
|
||||
.cardId="${this.cardId}"
|
||||
@@ -288,7 +289,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
if (selection) {
|
||||
selection.selection = undefined;
|
||||
}
|
||||
this.dataViewEle.openDetailPanel({
|
||||
this.kanbanViewLogic.root.openDetailPanel({
|
||||
view: this.view,
|
||||
rowId: this.cardId,
|
||||
onClose: () => {
|
||||
@@ -304,7 +305,7 @@ export class KanbanCard extends SignalWatcher(
|
||||
const columns = this.view.properties$.value.filter(
|
||||
v => !this.view.isInHeader(v.id)
|
||||
);
|
||||
this.style.border = this.isFocus
|
||||
this.style.border = this.isFocus$.value
|
||||
? '1px solid var(--affine-primary-color)'
|
||||
: '';
|
||||
return html`
|
||||
@@ -316,17 +317,17 @@ export class KanbanCard extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor cardId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor groupKey!: string;
|
||||
|
||||
@state()
|
||||
accessor isFocus = false;
|
||||
isFocus$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
accessor kanbanViewLogic!: KanbanViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.kanbanViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type {
|
||||
@@ -13,8 +13,8 @@ import type {
|
||||
} from '../../../core/property/index.js';
|
||||
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
|
||||
import type { Property } from '../../../core/view-manager/property.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewSelection } from '../selection';
|
||||
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
affine-data-view-kanban-cell {
|
||||
@@ -62,10 +62,7 @@ export class KanbanCell extends SignalWatcher(
|
||||
private readonly _cell = signal<DataViewCellLifeCycle>();
|
||||
|
||||
selectCurrentCell = (editing: boolean) => {
|
||||
const selectionView = this.closest(
|
||||
'affine-data-view-kanban'
|
||||
)?.selectionController;
|
||||
if (!selectionView) return;
|
||||
const selectionView = this.kanbanViewLogic.selectionController;
|
||||
if (selectionView) {
|
||||
const selection = selectionView.selection;
|
||||
if (selection && this.isSelected(selection) && editing) {
|
||||
@@ -93,7 +90,7 @@ export class KanbanCell extends SignalWatcher(
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.closest('affine-data-view-kanban')?.selectionController;
|
||||
return this.kanbanViewLogic.selectionController;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -103,9 +100,7 @@ export class KanbanCell extends SignalWatcher(
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const selectionElement = this.closest(
|
||||
'affine-data-view-kanban'
|
||||
)?.selectionController;
|
||||
const selectionElement = this.kanbanViewLogic.selectionController;
|
||||
if (!selectionElement) return;
|
||||
if (e.shiftKey) return;
|
||||
|
||||
@@ -138,7 +133,7 @@ export class KanbanCell extends SignalWatcher(
|
||||
const { view } = renderer;
|
||||
this.view.lockRows(this.isEditing$.value);
|
||||
this.dataset['editing'] = `${this.isEditing$.value}`;
|
||||
this.style.border = this.isFocus
|
||||
this.style.border = this.isFocus$.value
|
||||
? '1px solid var(--affine-primary-color)'
|
||||
: '';
|
||||
this.style.boxShadow = this.isEditing$.value
|
||||
@@ -173,11 +168,14 @@ export class KanbanCell extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor groupKey!: string;
|
||||
|
||||
@state()
|
||||
accessor isFocus = false;
|
||||
isFocus$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
accessor kanbanViewLogic!: KanbanViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.kanbanViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { UIEventStateContext } from '@blocksuite/std';
|
||||
import type { ReactiveController } from 'lit';
|
||||
|
||||
import type { KanbanViewSelectionWithType } from '../../selection';
|
||||
import type { DataViewKanban } from '../kanban-view.js';
|
||||
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
|
||||
|
||||
export class KanbanClipboardController implements ReactiveController {
|
||||
private readonly _onCopy = (
|
||||
@@ -19,31 +19,35 @@ export class KanbanClipboardController implements ReactiveController {
|
||||
};
|
||||
|
||||
private get readonly() {
|
||||
return this.host.props.view.readonly$.value;
|
||||
return this.logic.view.readonly$.value;
|
||||
}
|
||||
|
||||
constructor(public host: DataViewKanban) {
|
||||
host.addController(this);
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
constructor(public logic: KanbanViewUILogic) {}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.handleEvent('copy', ctx => {
|
||||
const kanbanSelection = this.host.selectionController.selection;
|
||||
if (!kanbanSelection) return false;
|
||||
if (this.host) {
|
||||
this.host.disposables.add(
|
||||
this.logic.handleEvent('copy', ctx => {
|
||||
const kanbanSelection = this.logic.selectionController.selection;
|
||||
if (!kanbanSelection) return false;
|
||||
|
||||
this._onCopy(ctx, kanbanSelection);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
this._onCopy(ctx, kanbanSelection);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.add(
|
||||
this.host.props.handleEvent('paste', ctx => {
|
||||
if (this.readonly) return false;
|
||||
this.host.disposables.add(
|
||||
this.logic.handleEvent('paste', ctx => {
|
||||
if (this.readonly) return false;
|
||||
|
||||
this._onPaste(ctx);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
this._onPaste(ctx);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@ import { autoScrollOnBoundary } from '../../../../core/utils/auto-scroll.js';
|
||||
import { startDrag } from '../../../../core/utils/drag.js';
|
||||
import { KanbanCard } from '../card.js';
|
||||
import { KanbanGroup } from '../group.js';
|
||||
import type { DataViewKanban } from '../kanban-view.js';
|
||||
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
|
||||
|
||||
export class KanbanDragController implements ReactiveController {
|
||||
dragStart = (ele: KanbanCard, evt: PointerEvent) => {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
const eleRect = ele.getBoundingClientRect();
|
||||
const offsetLeft = evt.x - eleRect.left;
|
||||
const offsetTop = evt.y - eleRect.top;
|
||||
@@ -36,8 +40,8 @@ export class KanbanDragController implements ReactiveController {
|
||||
return;
|
||||
}
|
||||
preview.display(evt.x - offsetLeft, evt.y - offsetTop);
|
||||
if (!Rect.fromDOM(this.host).isPointIn(Point.from(evt))) {
|
||||
const callback = this.host.props.onDrag;
|
||||
if (!Rect.fromDOM(host).isPointIn(Point.from(evt))) {
|
||||
const callback = this.logic.root.config.onDrag;
|
||||
if (callback) {
|
||||
this.dropPreview.remove();
|
||||
return {
|
||||
@@ -47,7 +51,7 @@ export class KanbanDragController implements ReactiveController {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const result = this.shooIndicator(evt, ele);
|
||||
const result = this.showIndicator(evt, ele);
|
||||
if (result) {
|
||||
return {
|
||||
type: 'self',
|
||||
@@ -80,19 +84,26 @@ export class KanbanDragController implements ReactiveController {
|
||||
}
|
||||
},
|
||||
});
|
||||
const cancelScroll = autoScrollOnBoundary(
|
||||
this.scrollContainer,
|
||||
computed(() => {
|
||||
return {
|
||||
left: drag.mousePosition.value.x,
|
||||
right: drag.mousePosition.value.x,
|
||||
top: drag.mousePosition.value.y,
|
||||
bottom: drag.mousePosition.value.y,
|
||||
};
|
||||
})
|
||||
);
|
||||
const cancelScroll =
|
||||
this.scrollContainer != null
|
||||
? autoScrollOnBoundary(
|
||||
this.scrollContainer,
|
||||
computed(() => {
|
||||
return {
|
||||
left: drag.mousePosition.value.x,
|
||||
right: drag.mousePosition.value.x,
|
||||
top: drag.mousePosition.value.y,
|
||||
bottom: drag.mousePosition.value.y,
|
||||
};
|
||||
})
|
||||
)
|
||||
: () => {};
|
||||
};
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
dropPreview = createDropPreview();
|
||||
|
||||
getInsertPosition = (
|
||||
@@ -119,7 +130,7 @@ export class KanbanDragController implements ReactiveController {
|
||||
}
|
||||
};
|
||||
|
||||
shooIndicator = (
|
||||
showIndicator = (
|
||||
evt: MouseEvent,
|
||||
self: KanbanCard | undefined
|
||||
): { group: KanbanGroup; position: InsertToPosition } | undefined => {
|
||||
@@ -133,38 +144,36 @@ export class KanbanDragController implements ReactiveController {
|
||||
};
|
||||
|
||||
get scrollContainer() {
|
||||
const scrollContainer = this.host.querySelector(
|
||||
'.affine-data-view-kanban-groups'
|
||||
) as HTMLElement;
|
||||
const scrollContainer = this.logic.scrollContainer$.value;
|
||||
return scrollContainer;
|
||||
}
|
||||
|
||||
constructor(private readonly host: DataViewKanban) {
|
||||
this.host.addController(this);
|
||||
}
|
||||
constructor(private readonly logic: KanbanViewUILogic) {}
|
||||
|
||||
hostConnected() {
|
||||
if (this.host.props.view.readonly$.value) {
|
||||
if (this.logic.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
this.host.disposables.add(
|
||||
this.host.props.handleEvent('dragStart', context => {
|
||||
const event = context.get('pointerState').raw;
|
||||
const target = event.target;
|
||||
if (target instanceof Element) {
|
||||
const cell = target.closest('affine-data-view-kanban-cell');
|
||||
if (cell?.isEditing$.value) {
|
||||
return;
|
||||
if (this.host) {
|
||||
this.host.disposables.add(
|
||||
this.logic.handleEvent('dragStart', context => {
|
||||
const event = context.get('pointerState').raw;
|
||||
const target = event.target;
|
||||
if (target instanceof Element) {
|
||||
const cell = target.closest('affine-data-view-kanban-cell');
|
||||
if (cell?.isEditing$.value) {
|
||||
return;
|
||||
}
|
||||
cell?.selectCurrentCell(false);
|
||||
const card = target.closest('affine-data-view-kanban-card');
|
||||
if (card) {
|
||||
this.dragStart(card, event);
|
||||
}
|
||||
}
|
||||
cell?.selectCurrentCell(false);
|
||||
const card = target.closest('affine-data-view-kanban-card');
|
||||
if (card) {
|
||||
this.dragStart(card, event);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +183,8 @@ const createDragPreview = (card: KanbanCard, x: number, y: number) => {
|
||||
const div = document.createElement('div');
|
||||
const kanbanCard = new KanbanCard();
|
||||
kanbanCard.cardId = card.cardId;
|
||||
kanbanCard.view = card.view;
|
||||
kanbanCard.isFocus = true;
|
||||
kanbanCard.kanbanViewLogic = card.kanbanViewLogic;
|
||||
kanbanCard.isFocus$.value = true;
|
||||
kanbanCard.style.backgroundColor = 'var(--affine-background-primary-color)';
|
||||
div.append(kanbanCard);
|
||||
div.className = 'with-data-view-css-variable';
|
||||
|
||||
@@ -1,63 +1,67 @@
|
||||
import type { ReactiveController } from 'lit';
|
||||
|
||||
import type { DataViewKanban } from '../kanban-view.js';
|
||||
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
|
||||
|
||||
export class KanbanHotkeysController implements ReactiveController {
|
||||
private get hasSelection() {
|
||||
return !!this.host.selectionController.selection;
|
||||
return !!this.logic.selectionController.selection;
|
||||
}
|
||||
|
||||
constructor(private readonly host: DataViewKanban) {
|
||||
this.host.addController(this);
|
||||
constructor(public logic: KanbanViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.bindHotkey({
|
||||
Escape: () => {
|
||||
this.host.selectionController.focusOut();
|
||||
return true;
|
||||
},
|
||||
Enter: () => {
|
||||
this.host.selectionController.focusIn();
|
||||
},
|
||||
ArrowUp: context => {
|
||||
if (!this.hasSelection) return false;
|
||||
if (this.host) {
|
||||
this.host.disposables.add(
|
||||
this.logic.bindHotkey({
|
||||
Escape: () => {
|
||||
this.logic.selectionController.focusOut();
|
||||
return true;
|
||||
},
|
||||
Enter: () => {
|
||||
this.logic.selectionController.focusIn();
|
||||
},
|
||||
ArrowUp: context => {
|
||||
if (!this.hasSelection) return false;
|
||||
|
||||
this.host.selectionController.focusNext('up');
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowDown: context => {
|
||||
if (!this.hasSelection) return false;
|
||||
this.logic.selectionController.focusNext('up');
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowDown: context => {
|
||||
if (!this.hasSelection) return false;
|
||||
|
||||
this.host.selectionController.focusNext('down');
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Tab: context => {
|
||||
if (!this.hasSelection) return false;
|
||||
this.logic.selectionController.focusNext('down');
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Tab: context => {
|
||||
if (!this.hasSelection) return false;
|
||||
|
||||
this.host.selectionController.focusNext('down');
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowLeft: () => {
|
||||
if (!this.hasSelection) return false;
|
||||
this.logic.selectionController.focusNext('down');
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowLeft: () => {
|
||||
if (!this.hasSelection) return false;
|
||||
|
||||
this.host.selectionController.focusNext('left');
|
||||
return true;
|
||||
},
|
||||
ArrowRight: () => {
|
||||
if (!this.hasSelection) return false;
|
||||
this.logic.selectionController.focusNext('left');
|
||||
return true;
|
||||
},
|
||||
ArrowRight: () => {
|
||||
if (!this.hasSelection) return false;
|
||||
|
||||
this.host.selectionController.focusNext('right');
|
||||
return true;
|
||||
},
|
||||
Backspace: () => {
|
||||
this.host.selectionController.deleteCard();
|
||||
},
|
||||
})
|
||||
);
|
||||
this.logic.selectionController.focusNext('right');
|
||||
return true;
|
||||
},
|
||||
Backspace: () => {
|
||||
this.logic.selectionController.deleteCard();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
import { KanbanCard } from '../card.js';
|
||||
import { KanbanCell } from '../cell.js';
|
||||
import type { KanbanGroup } from '../group.js';
|
||||
import type { DataViewKanban } from '../kanban-view.js';
|
||||
import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
|
||||
|
||||
export class KanbanSelectionController implements ReactiveController {
|
||||
private _selection?: KanbanViewSelectionWithType;
|
||||
@@ -47,52 +47,62 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
set selection(data: KanbanViewSelection | undefined) {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
this.host.props.setSelection();
|
||||
this.logic.setSelection();
|
||||
return;
|
||||
}
|
||||
const selection: KanbanViewSelectionWithType = {
|
||||
...data,
|
||||
viewId: this.host.props.view.id,
|
||||
viewId: this.logic.view.id,
|
||||
type: 'kanban',
|
||||
};
|
||||
|
||||
if (selection.selectionType === 'cell' && selection.isEditing) {
|
||||
const container = getFocusCell(this.host, selection);
|
||||
const container = getFocusCell(host, selection);
|
||||
const cell = container?.cell;
|
||||
const isEditing = cell
|
||||
? cell.beforeEnterEditMode()
|
||||
? selection.isEditing
|
||||
: false
|
||||
: false;
|
||||
this.host.props.setSelection({
|
||||
this.logic.setSelection({
|
||||
...selection,
|
||||
isEditing,
|
||||
});
|
||||
} else {
|
||||
this.host.props.setSelection(selection);
|
||||
this.logic.setSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.host.props.view;
|
||||
return this.logic.view;
|
||||
}
|
||||
|
||||
constructor(private readonly host: DataViewKanban) {
|
||||
this.host.addController(this);
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
constructor(public logic: KanbanViewUILogic) {}
|
||||
|
||||
blur(selection: KanbanViewSelection) {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
if (selection.selectionType !== 'cell') {
|
||||
const selectCards = getSelectedCards(this.host, selection);
|
||||
selectCards.forEach(card => (card.isFocus = false));
|
||||
selectCards.forEach(card => (card.isFocus$.value = false));
|
||||
return;
|
||||
}
|
||||
const container = getFocusCell(this.host, selection);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.isFocus = false;
|
||||
container.isFocus$.value = false;
|
||||
const cell = container?.cell;
|
||||
|
||||
if (selection.isEditing) {
|
||||
@@ -116,19 +126,23 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
return;
|
||||
}
|
||||
if (selection.selectionType === 'card') {
|
||||
this.host.props.view.rowsDelete(selection.cards.map(v => v.cardId));
|
||||
this.view.rowsDelete(selection.cards.map(v => v.cardId));
|
||||
this.selection = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
focus(selection: KanbanViewSelection) {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
if (selection.selectionType !== 'cell') {
|
||||
const selectCards = getSelectedCards(this.host, selection);
|
||||
selectCards.forEach((card, index) => {
|
||||
if (index === 0) {
|
||||
card.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
card.isFocus = true;
|
||||
card.isFocus$.value = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -137,7 +151,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
return;
|
||||
}
|
||||
container.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
container.isFocus = true;
|
||||
container.isFocus$.value = true;
|
||||
const cell = container?.cell;
|
||||
if (selection.isEditing) {
|
||||
if (cell?.focusCell()) {
|
||||
@@ -153,10 +167,9 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
const group = this.host.groupManager?.groupsDataList$.value?.[0];
|
||||
const group = this.logic.groups$.value?.[0];
|
||||
const card = group?.rows[0];
|
||||
const columnId =
|
||||
card && this.host.props.view.getHeaderTitle(card.rowId)?.id;
|
||||
const columnId = card && this.view.getHeaderTitle(card.rowId)?.id;
|
||||
if (group && card && columnId) {
|
||||
this.selection = {
|
||||
selectionType: 'cell',
|
||||
@@ -169,6 +182,10 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
focusIn() {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
const selection = this.selection;
|
||||
if (!selection) return;
|
||||
if (selection.selectionType === 'cell' && selection.isEditing) return;
|
||||
@@ -198,6 +215,10 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
focusNext(position: 'up' | 'down' | 'left' | 'right') {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
const selection = this.selection;
|
||||
if (!selection) {
|
||||
return;
|
||||
@@ -222,7 +243,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
} else if (selection.selectionType === 'card') {
|
||||
// card focus
|
||||
const group = this.host.querySelector(
|
||||
const group = this.host?.querySelector(
|
||||
`affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]`
|
||||
);
|
||||
const cardElements = Array.from(
|
||||
@@ -292,7 +313,11 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
cards: KanbanCardSelectionCard[];
|
||||
}
|
||||
| undefined {
|
||||
const group = this.host.querySelector(
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
const group = host.querySelector(
|
||||
`affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]`
|
||||
);
|
||||
const kanbanCards = Array.from(
|
||||
@@ -332,7 +357,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
const groups = Array.from(
|
||||
this.host.querySelectorAll('affine-data-view-kanban-group')
|
||||
this.host?.querySelectorAll('affine-data-view-kanban-group') ?? []
|
||||
);
|
||||
|
||||
if (nextPosition === 'right') {
|
||||
@@ -369,6 +394,10 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
groupKey?: string;
|
||||
}
|
||||
| undefined {
|
||||
const host = this.host;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
const kanbanCells = getCardCellsBySelection(this.host, selection);
|
||||
const group = this.host.querySelector(
|
||||
`affine-data-view-kanban-group[data-key="${selection.groupKey}"]`
|
||||
@@ -426,7 +455,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
const groups = Array.from(
|
||||
this.host.querySelectorAll('affine-data-view-kanban-group')
|
||||
this.host?.querySelectorAll('affine-data-view-kanban-group') ?? []
|
||||
);
|
||||
|
||||
if (nextPosition === 'right') {
|
||||
@@ -453,8 +482,8 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.selection$.subscribe(selection => {
|
||||
this.host?.disposables.add(
|
||||
this.logic.selection$.subscribe(selection => {
|
||||
const old = this._selection;
|
||||
if (old) {
|
||||
this.blur(old);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { KanbanCard } from './card.js';
|
||||
import { KanbanCell } from './cell.js';
|
||||
import { KanbanGroup } from './group.js';
|
||||
import { KanbanHeader } from './header.js';
|
||||
|
||||
export function pcEffects() {
|
||||
customElements.define('affine-data-view-kanban-card', KanbanCard);
|
||||
customElements.define('affine-data-view-kanban-cell', KanbanCell);
|
||||
customElements.define('affine-data-view-kanban-group', KanbanGroup);
|
||||
customElements.define('affine-data-view-kanban-header', KanbanHeader);
|
||||
}
|
||||
@@ -11,11 +11,10 @@ import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { GroupTitle } from '../../../core/group-by/group-title.js';
|
||||
import type { Group } from '../../../core/group-by/trait.js';
|
||||
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
affine-data-view-kanban-group {
|
||||
@@ -99,40 +98,34 @@ export class KanbanGroup extends SignalWatcher(
|
||||
private readonly clickAddCard = () => {
|
||||
const id = this.view.addCard('end', this.group.key);
|
||||
requestAnimationFrame(() => {
|
||||
const kanban = this.closest('affine-data-view-kanban');
|
||||
if (kanban) {
|
||||
const columnId =
|
||||
this.view.mainProperties$.value.titleColumn ||
|
||||
this.view.propertyIds$.value[0];
|
||||
if (!columnId) return;
|
||||
kanban.selectionController.selection = {
|
||||
selectionType: 'cell',
|
||||
groupKey: this.group.key,
|
||||
cardId: id,
|
||||
columnId,
|
||||
isEditing: true,
|
||||
};
|
||||
}
|
||||
const columnId =
|
||||
this.view.mainProperties$.value.titleColumn ||
|
||||
this.view.propertyIds$.value[0];
|
||||
if (!columnId) return;
|
||||
this.kanbanViewLogic.selectionController.selection = {
|
||||
selectionType: 'cell',
|
||||
groupKey: this.group.key,
|
||||
cardId: id,
|
||||
columnId,
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
const id = this.view.addCard('start', this.group.key);
|
||||
requestAnimationFrame(() => {
|
||||
const kanban = this.closest('affine-data-view-kanban');
|
||||
if (kanban) {
|
||||
const columnId =
|
||||
this.view.mainProperties$.value.titleColumn ||
|
||||
this.view.propertyIds$.value[0];
|
||||
if (!columnId) return;
|
||||
kanban.selectionController.selection = {
|
||||
selectionType: 'cell',
|
||||
groupKey: this.group.key,
|
||||
cardId: id,
|
||||
columnId,
|
||||
isEditing: true,
|
||||
};
|
||||
}
|
||||
const columnId =
|
||||
this.view.mainProperties$.value.titleColumn ||
|
||||
this.view.propertyIds$.value[0];
|
||||
if (!columnId) return;
|
||||
this.kanbanViewLogic.selectionController.selection = {
|
||||
selectionType: 'cell',
|
||||
groupKey: this.group.key,
|
||||
cardId: id,
|
||||
columnId,
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -176,8 +169,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
<affine-data-view-kanban-card
|
||||
data-card-id="${row.rowId}"
|
||||
.groupKey="${this.group.key}"
|
||||
.dataViewEle="${this.dataViewEle}"
|
||||
.view="${this.view}"
|
||||
.kanbanViewLogic="${this.kanbanViewLogic}"
|
||||
.cardId="${row.rowId}"
|
||||
></affine-data-view-kanban-card>
|
||||
`;
|
||||
@@ -197,14 +189,15 @@ export class KanbanGroup extends SignalWatcher(
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group!: Group;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
accessor kanbanViewLogic!: KanbanViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.kanbanViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { css } from '@emotion/css';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { type TemplateResult } from 'lit';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import {
|
||||
type GroupTrait,
|
||||
groupTraitKey,
|
||||
} from '../../../core/group-by/trait.js';
|
||||
import {
|
||||
createUniComponentFromWebComponent,
|
||||
renderUniLit,
|
||||
} from '../../../core/index.js';
|
||||
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
|
||||
import {
|
||||
createSortContext,
|
||||
sortable,
|
||||
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
|
||||
import { horizontalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
|
||||
import {
|
||||
DataViewUIBase,
|
||||
DataViewUILogicBase,
|
||||
} from '../../../core/view/data-view-base.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewSelectionWithType } from '../selection.js';
|
||||
import { KanbanClipboardController } from './controller/clipboard.js';
|
||||
import { KanbanDragController } from './controller/drag.js';
|
||||
import { KanbanHotkeysController } from './controller/hotkeys.js';
|
||||
import { KanbanSelectionController } from './controller/selection.js';
|
||||
|
||||
export class KanbanViewUILogic extends DataViewUILogicBase<
|
||||
KanbanSingleView,
|
||||
KanbanViewSelectionWithType
|
||||
> {
|
||||
ui$ = signal<KanbanViewUI | undefined>();
|
||||
clipboardController = new KanbanClipboardController(this);
|
||||
dragController = new KanbanDragController(this);
|
||||
hotkeysController = new KanbanHotkeysController(this);
|
||||
selectionController = new KanbanSelectionController(this);
|
||||
|
||||
groupTrait$ = computed(() => {
|
||||
return this.view.traitGet(groupTraitKey);
|
||||
});
|
||||
|
||||
groups$ = computed(() => {
|
||||
const groupTrait = this.groupTrait$.value;
|
||||
return groupTrait?.groupsDataList$.value || [];
|
||||
});
|
||||
|
||||
private get readonly() {
|
||||
return this.view.readonly$.value;
|
||||
}
|
||||
|
||||
clearSelection = () => {
|
||||
this.selectionController.clear();
|
||||
};
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
const rowId = this.view.rowAdd(position);
|
||||
if (rowId) {
|
||||
this.root.openDetailPanel({
|
||||
view: this.view,
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
return rowId;
|
||||
};
|
||||
|
||||
focusFirstCell = () => {
|
||||
this.selectionController.focusFirstCell();
|
||||
};
|
||||
|
||||
showIndicator = (evt: MouseEvent) => {
|
||||
return this.dragController.showIndicator(evt, undefined) != null;
|
||||
};
|
||||
|
||||
hideIndicator = () => {
|
||||
this.dragController.dropPreview.remove();
|
||||
};
|
||||
|
||||
moveTo = (id: string, evt: MouseEvent) => {
|
||||
const position = this.dragController.getInsertPosition(evt);
|
||||
if (position) {
|
||||
position.group.group.manager.moveCardTo(
|
||||
id,
|
||||
'',
|
||||
position.group.group.key,
|
||||
position.position
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onWheel = (event: WheelEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const ele = event.currentTarget;
|
||||
if (ele instanceof HTMLElement) {
|
||||
if (ele.scrollWidth === ele.clientWidth) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
renderAddGroup = (groupHelper: GroupTrait) => {
|
||||
const addGroup = groupHelper.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = groupHelper.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.view.manager.dataSource,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div
|
||||
style="height: 32px;flex-shrink:0;display:flex;align-items:center;"
|
||||
@click="${add}"
|
||||
>
|
||||
<div class="${addGroupIconStyle}">${AddCursorIcon()}</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
scrollContainer$ = signal<HTMLElement | undefined>(undefined);
|
||||
|
||||
renderer = createUniComponentFromWebComponent(KanbanViewUI);
|
||||
}
|
||||
|
||||
export class KanbanViewUI extends DataViewUIBase<KanbanViewUILogic> {
|
||||
readonly sortContext = createSortContext({
|
||||
activators: defaultActivators,
|
||||
container: this,
|
||||
onDragEnd: evt => {
|
||||
const over = evt.over;
|
||||
const activeId = evt.active.id;
|
||||
const groupTrait = this.logic.groupTrait$.value;
|
||||
const groups = groupTrait?.groupsDataList$.value;
|
||||
if (over && over.id !== activeId && groups) {
|
||||
const activeIndex = groups.findIndex(data => data?.key === activeId);
|
||||
const overIndex = groups.findIndex(data => data?.key === over.id);
|
||||
|
||||
groupTrait?.moveGroupTo(
|
||||
activeId,
|
||||
activeIndex > overIndex
|
||||
? {
|
||||
before: true,
|
||||
id: over.id,
|
||||
}
|
||||
: {
|
||||
before: false,
|
||||
id: over.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
modifiers: [
|
||||
({ transform }) => {
|
||||
return {
|
||||
...transform,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
],
|
||||
items: computed(() => {
|
||||
return this.logic.groups$.value?.map(v => v?.key ?? 'default key') ?? [];
|
||||
}),
|
||||
strategy: horizontalListSortingStrategy,
|
||||
});
|
||||
|
||||
private renderGroups() {
|
||||
const groups = this.logic.groups$.value;
|
||||
if (!groups) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`${groups.map(group => {
|
||||
return html` <affine-data-view-kanban-group
|
||||
${sortable(group.key)}
|
||||
data-key="${group.key}"
|
||||
.kanbanViewLogic="${this.logic}"
|
||||
.group="${group}"
|
||||
></affine-data-view-kanban-group>`;
|
||||
})}`;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.logic.ui$.value = this;
|
||||
this.logic.clipboardController.hostConnected();
|
||||
this.logic.dragController.hostConnected();
|
||||
this.logic.hotkeysController.hostConnected();
|
||||
this.logic.selectionController.hostConnected();
|
||||
this.classList.add('kanban-view', kanbanViewStyle);
|
||||
this.style.userSelect = 'none';
|
||||
this.style.display = 'flex';
|
||||
this.style.flexDirection = 'column';
|
||||
}
|
||||
|
||||
override render(): TemplateResult {
|
||||
const groups = this.logic.groups$.value;
|
||||
if (!groups) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const vPadding = this.logic.root.config.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
|
||||
const groupTrait = this.logic.groupTrait$.value;
|
||||
|
||||
return html`
|
||||
${renderUniLit(this.logic.root.config.headerWidget, {
|
||||
dataViewLogic: this.logic,
|
||||
})}
|
||||
<div
|
||||
${ref(this.logic.scrollContainer$)}
|
||||
class="${kanbanGroupsStyle}"
|
||||
style="${wrapperStyle}"
|
||||
@wheel="${this.logic.onWheel}"
|
||||
>
|
||||
${this.renderGroups()}
|
||||
${groupTrait ? this.logic.renderAddGroup(groupTrait) : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const kanbanViewStyle = css({
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const kanbanGroupsStyle = css({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
paddingBottom: '4px',
|
||||
overflowX: 'scroll',
|
||||
overflowY: 'hidden',
|
||||
|
||||
'&:hover': {
|
||||
paddingBottom: '0px',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar': {
|
||||
WebkitAppearance: 'none',
|
||||
display: 'block',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar:horizontal': {
|
||||
height: '4px',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
borderRadius: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
'&:hover::-webkit-scrollbar:horizontal': {
|
||||
height: '8px',
|
||||
},
|
||||
|
||||
'&:hover::-webkit-scrollbar-thumb': {
|
||||
borderRadius: '16px',
|
||||
backgroundColor: 'var(--affine-black-30)',
|
||||
},
|
||||
|
||||
'&:hover::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
|
||||
const addGroupIconStyle = css({
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
|
||||
'& svg': {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
fill: 'var(--affine-icon-color)',
|
||||
color: 'var(--affine-icon-color)',
|
||||
},
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dv-kanban-view-ui': KanbanViewUI;
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
|
||||
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
|
||||
import {
|
||||
createSortContext,
|
||||
sortable,
|
||||
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
|
||||
import { horizontalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
|
||||
import { DataViewBase } from '../../../core/view/data-view-base.js';
|
||||
import type { KanbanSingleView } from '../kanban-view-manager.js';
|
||||
import type { KanbanViewSelectionWithType } from '../selection';
|
||||
import { KanbanClipboardController } from './controller/clipboard.js';
|
||||
import { KanbanDragController } from './controller/drag.js';
|
||||
import { KanbanHotkeysController } from './controller/hotkeys.js';
|
||||
import { KanbanSelectionController } from './controller/selection.js';
|
||||
|
||||
const styles = css`
|
||||
affine-data-view-kanban {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding-bottom: 4px;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups:hover {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups::-webkit-scrollbar:horizontal {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups:hover::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups:hover::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: var(--affine-black-30);
|
||||
}
|
||||
|
||||
.affine-data-view-kanban-groups:hover::-webkit-scrollbar-track {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.add-group-icon {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-group-icon:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.add-group-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--affine-icon-color);
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export class DataViewKanban extends DataViewBase<
|
||||
KanbanSingleView,
|
||||
KanbanViewSelectionWithType
|
||||
> {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly dragController = new KanbanDragController(this);
|
||||
|
||||
clipboardController = new KanbanClipboardController(this);
|
||||
|
||||
hotkeysController = new KanbanHotkeysController(this);
|
||||
|
||||
onWheel = (event: WheelEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const ele = event.currentTarget;
|
||||
if (ele instanceof HTMLElement) {
|
||||
if (ele.scrollWidth === ele.clientWidth) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
renderAddGroup = () => {
|
||||
const addGroup = this.groupManager.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = this.groupManager.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(
|
||||
() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.props.view.manager.dataSource,
|
||||
}) as never
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div
|
||||
style="height: 32px;flex-shrink:0;display:flex;align-items:center;"
|
||||
@click="${add}"
|
||||
>
|
||||
<div class="add-group-icon">${AddCursorIcon()}</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
selectionController = new KanbanSelectionController(this);
|
||||
|
||||
sortContext = createSortContext({
|
||||
activators: defaultActivators,
|
||||
container: this,
|
||||
onDragEnd: evt => {
|
||||
const over = evt.over;
|
||||
const activeId = evt.active.id;
|
||||
const groups = this.groupManager.groupsDataList$.value;
|
||||
if (over && over.id !== activeId && groups) {
|
||||
const activeIndex = groups.findIndex(data => data?.key === activeId);
|
||||
const overIndex = groups.findIndex(data => data?.key === over.id);
|
||||
|
||||
this.groupManager.moveGroupTo(
|
||||
activeId,
|
||||
activeIndex > overIndex
|
||||
? {
|
||||
before: true,
|
||||
id: over.id,
|
||||
}
|
||||
: {
|
||||
before: false,
|
||||
id: over.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
modifiers: [
|
||||
({ transform }) => {
|
||||
return {
|
||||
...transform,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
],
|
||||
items: computed(() => {
|
||||
return (
|
||||
this.groupManager.groupsDataList$.value?.map(
|
||||
v => v?.key ?? 'default key'
|
||||
) ?? []
|
||||
);
|
||||
}),
|
||||
strategy: horizontalListSortingStrategy,
|
||||
});
|
||||
|
||||
get expose(): DataViewInstance {
|
||||
return {
|
||||
clearSelection: () => {
|
||||
this.selectionController.clear();
|
||||
},
|
||||
addRow: position => {
|
||||
if (this.props.view.readonly$.value) return;
|
||||
const rowId = this.props.view.rowAdd(position);
|
||||
if (rowId) {
|
||||
this.props.dataViewEle.openDetailPanel({
|
||||
view: this.props.view,
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
return rowId;
|
||||
},
|
||||
focusFirstCell: () => {
|
||||
this.selectionController.focusFirstCell();
|
||||
},
|
||||
getSelection: () => {
|
||||
return this.selectionController.selection;
|
||||
},
|
||||
hideIndicator: () => {
|
||||
this.dragController.dropPreview.remove();
|
||||
},
|
||||
moveTo: (id, evt) => {
|
||||
const position = this.dragController.getInsertPosition(evt);
|
||||
if (position) {
|
||||
position.group.group.manager.moveCardTo(
|
||||
id,
|
||||
'',
|
||||
position.group.group.key,
|
||||
position.position
|
||||
);
|
||||
}
|
||||
},
|
||||
showIndicator: evt => {
|
||||
return this.dragController.shooIndicator(evt, undefined) != null;
|
||||
},
|
||||
view: this.props.view,
|
||||
eventTrace: this.props.eventTrace,
|
||||
};
|
||||
}
|
||||
|
||||
get groupManager() {
|
||||
return this.props.view.groupTrait;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const groups = this.groupManager.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return html``;
|
||||
}
|
||||
const vPadding = this.props.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.props.headerWidget, {
|
||||
dataViewInstance: this.expose,
|
||||
})}
|
||||
<div
|
||||
class="affine-data-view-kanban-groups"
|
||||
style="${wrapperStyle}"
|
||||
@wheel="${this.onWheel}"
|
||||
>
|
||||
${repeat(
|
||||
groups,
|
||||
group => group?.key ?? 'default key',
|
||||
group => {
|
||||
if (!group) return;
|
||||
return html` <affine-data-view-kanban-group
|
||||
${sortable(group.key)}
|
||||
data-key="${group.key}"
|
||||
.dataViewEle="${this.props.dataViewEle}"
|
||||
.view="${this.props.view}"
|
||||
.group="${group}"
|
||||
></affine-data-view-kanban-group>`;
|
||||
}
|
||||
)}
|
||||
${this.renderAddGroup()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.affine-data-view-kanban-groups')
|
||||
accessor groups!: HTMLElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-data-view-kanban': DataViewKanban;
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,17 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import type { KanbanSelectionController } from './controller/selection.js';
|
||||
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
|
||||
|
||||
export const openDetail = (
|
||||
dataViewEle: DataViewRenderer,
|
||||
kanbanViewLogic: KanbanViewUILogic,
|
||||
rowId: string,
|
||||
selection: KanbanSelectionController
|
||||
) => {
|
||||
const old = selection.selection;
|
||||
selection.selection = undefined;
|
||||
dataViewEle.openDetailPanel({
|
||||
kanbanViewLogic.root.openDetailPanel({
|
||||
view: selection.view,
|
||||
rowId: rowId,
|
||||
onClose: () => {
|
||||
@@ -32,7 +32,7 @@ export const openDetail = (
|
||||
};
|
||||
|
||||
export const popCardMenu = (
|
||||
dataViewEle: DataViewRenderer,
|
||||
kanbanViewLogic: KanbanViewUILogic,
|
||||
ele: PopupTarget,
|
||||
rowId: string,
|
||||
selection: KanbanSelectionController
|
||||
@@ -42,7 +42,7 @@ export const popCardMenu = (
|
||||
name: 'Expand Card',
|
||||
prefix: ExpandFullIcon(),
|
||||
select: () => {
|
||||
openDetail(dataViewEle, rowId, selection);
|
||||
openDetail(kanbanViewLogic, rowId, selection);
|
||||
},
|
||||
}),
|
||||
menu.subMenu({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { createUniComponentFromWebComponent } from '../../core/index.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import { kanbanViewModel } from './define.js';
|
||||
import { MobileDataViewKanban } from './mobile/kanban-view.js';
|
||||
import { DataViewKanban } from './pc/kanban-view.js';
|
||||
import { MobileKanbanViewUILogic } from './mobile/kanban-view-ui-logic.js';
|
||||
import { KanbanViewUILogic } from './pc/kanban-view-ui-logic.js';
|
||||
|
||||
export const kanbanViewMeta = kanbanViewModel.createMeta({
|
||||
icon: createIcon('DatabaseKanbanViewIcon'),
|
||||
view: createUniComponentFromWebComponent(DataViewKanban),
|
||||
mobileView: createUniComponentFromWebComponent(MobileDataViewKanban),
|
||||
// @ts-expect-error fixme: typesafe
|
||||
pcLogic: () => KanbanViewUILogic,
|
||||
// @ts-expect-error fixme: typesafe
|
||||
mobileLogic: () => MobileKanbanViewUILogic,
|
||||
});
|
||||
|
||||
11
blocksuite/affine/data-view/src/view-presets/table/effect.ts
Normal file
11
blocksuite/affine/data-view/src/view-presets/table/effect.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { mobileEffects } from './mobile/effect.js';
|
||||
import { pcEffects } from './pc/effect.js';
|
||||
import { pcVirtualEffects } from './pc-virtual/effect.js';
|
||||
import { statsEffects } from './stats/effect.js';
|
||||
|
||||
export function tableEffects() {
|
||||
mobileEffects();
|
||||
statsEffects();
|
||||
pcEffects();
|
||||
pcVirtualEffects();
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
export * from './define.js';
|
||||
export * from './pc/effect.js';
|
||||
export * from './pc/table-view.js';
|
||||
export * from './pc-virtual/effect.js';
|
||||
export * from './renderer.js';
|
||||
export * from './selection.js';
|
||||
export * from './table-view-manager.js';
|
||||
export * from './table-view-selector.js';
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
type CellRenderProps,
|
||||
type DataViewCellLifeCycle,
|
||||
renderUniLit,
|
||||
type SingleView,
|
||||
} from '../../../core/index.js';
|
||||
import { TableViewAreaSelection } from '../selection';
|
||||
import type { TableProperty } from '../table-view-manager.js';
|
||||
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
export class MobileTableCell extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -48,7 +48,7 @@ export class MobileTableCell extends SignalWatcher(
|
||||
});
|
||||
|
||||
isSelectionEditing$ = computed(() => {
|
||||
const selection = this.table?.props.selection$.value;
|
||||
const selection = this.tableViewLogic.selection$.value;
|
||||
if (selection?.selectionType !== 'area') {
|
||||
return false;
|
||||
}
|
||||
@@ -68,8 +68,8 @@ export class MobileTableCell extends SignalWatcher(
|
||||
if (this.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const setSelection = this.table?.props.setSelection;
|
||||
const viewId = this.table?.props.view.id;
|
||||
const setSelection = this.tableViewLogic.setSelection;
|
||||
const viewId = this.tableViewLogic.view.id;
|
||||
if (setSelection && viewId) {
|
||||
if (editing && this.cell?.beforeEnterEditMode() === false) {
|
||||
return;
|
||||
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
|
||||
return this.closest('mobile-table-group')?.group?.key;
|
||||
}
|
||||
|
||||
private get table() {
|
||||
return this.closest('mobile-data-view-table');
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.column.readonly$.value) return;
|
||||
@@ -160,7 +156,11 @@ export class MobileTableCell extends SignalWatcher(
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: SingleView;
|
||||
accessor tableViewLogic!: MobileTableViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MobileTableCell } from './cell.js';
|
||||
import { MobileTableColumnHeader } from './column-header.js';
|
||||
import { MobileTableGroup } from './group.js';
|
||||
import { MobileTableHeader } from './header.js';
|
||||
import { MobileTableRow } from './row.js';
|
||||
import { MobileTableViewUI } from './table-view-ui-logic.js';
|
||||
|
||||
export function mobileEffects() {
|
||||
customElements.define('mobile-table-cell', MobileTableCell);
|
||||
customElements.define('mobile-table-group', MobileTableGroup);
|
||||
customElements.define('mobile-data-view-table-ui', MobileTableViewUI);
|
||||
customElements.define('mobile-table-header', MobileTableHeader);
|
||||
customElements.define('mobile-table-column-header', MobileTableColumnHeader);
|
||||
customElements.define('mobile-table-row', MobileTableRow);
|
||||
}
|
||||
@@ -8,17 +8,14 @@ import { PlusIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { GroupTitle } from '../../../core/group-by/group-title.js';
|
||||
import type { Group } from '../../../core/group-by/trait.js';
|
||||
import type { Row } from '../../../core/index.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import type { DataViewTable } from '../pc/table-view.js';
|
||||
import { TableViewAreaSelection } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
.data-view-table-group-add-row {
|
||||
@@ -54,40 +51,10 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
selectionController.selection = TableViewAreaSelection.create({
|
||||
groupKey: this.group?.key,
|
||||
focus: {
|
||||
rowIndex: this.rows.length - 1,
|
||||
columnIndex: index,
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
selectionController.selection = TableViewAreaSelection.create({
|
||||
groupKey: this.group?.key,
|
||||
focus: {
|
||||
rowIndex: 0,
|
||||
columnIndex: index,
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -150,8 +117,7 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
return html` <mobile-table-row
|
||||
data-row-index="${idx}"
|
||||
data-row-id="${row.rowId}"
|
||||
.dataViewEle="${this.dataViewEle}"
|
||||
.view="${this.view}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.rowId="${row.rowId}"
|
||||
.rowIndex="${idx}"
|
||||
></mobile-table-row>`;
|
||||
@@ -172,8 +138,6 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
${PlusIcon()}<span style="font-size: 12px">New Record</span>
|
||||
</div>
|
||||
</div>`}
|
||||
<affine-database-column-stats .view="${this.view}" .group="${this.group}">
|
||||
</affine-database-column-stats>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -181,20 +145,15 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
return this.renderRows(this.rows);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group: Group | undefined = undefined;
|
||||
|
||||
@query('.affine-database-block-rows')
|
||||
accessor rowsContainer: HTMLElement | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
accessor tableViewLogic!: MobileTableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor viewEle!: DataViewTable;
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { cellDivider } from '../styles.js';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
|
||||
export class MobileTableHeader extends SignalWatcher(
|
||||
@@ -60,7 +61,7 @@ export class MobileTableHeader extends SignalWatcher(
|
||||
.column="${column}"
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
></mobile-table-column-header>
|
||||
<div class="cell-divider" style="height: auto;"></div>
|
||||
<div class="${cellDivider}" style="height: auto;"></div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { DeleteIcon, ExpandFullIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import type { SingleView } from '../../../core/index.js';
|
||||
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
export const popMobileRowMenu = (
|
||||
target: PopupTarget,
|
||||
rowId: string,
|
||||
dataViewEle: DataViewRenderer,
|
||||
tableViewLogic: MobileTableViewUILogic,
|
||||
view: SingleView
|
||||
) => {
|
||||
popFilterableSimpleMenu(target, [
|
||||
@@ -21,7 +21,7 @@ export const popMobileRowMenu = (
|
||||
name: 'Expand Row',
|
||||
prefix: ExpandFullIcon(),
|
||||
select: () => {
|
||||
dataViewEle.openDetailPanel({
|
||||
tableViewLogic.root.openDetailPanel({
|
||||
view: view,
|
||||
rowId: rowId,
|
||||
});
|
||||
|
||||
@@ -10,9 +10,9 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { cellDivider } from '../styles.js';
|
||||
import { popMobileRowMenu } from './menu.js';
|
||||
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
export class MobileTableRow extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -73,7 +73,7 @@ export class MobileTableRow extends SignalWatcher(
|
||||
v => v.id,
|
||||
(column, i) => {
|
||||
const clickDetail = () => {
|
||||
this.dataViewEle.openDetailPanel({
|
||||
this.tableViewLogic.root.openDetailPanel({
|
||||
view: this.view,
|
||||
rowId: this.rowId,
|
||||
});
|
||||
@@ -83,7 +83,7 @@ export class MobileTableRow extends SignalWatcher(
|
||||
popMobileRowMenu(
|
||||
popupTargetFromElement(ele),
|
||||
this.rowId,
|
||||
this.dataViewEle,
|
||||
this.tableViewLogic,
|
||||
this.view
|
||||
);
|
||||
};
|
||||
@@ -95,7 +95,7 @@ export class MobileTableRow extends SignalWatcher(
|
||||
width: `${column.width$.value}px`,
|
||||
border: i === 0 ? 'none' : undefined,
|
||||
})}
|
||||
.view="${view}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.column="${column}"
|
||||
.rowId="${this.rowId}"
|
||||
data-row-id="${this.rowId}"
|
||||
@@ -107,7 +107,7 @@ export class MobileTableRow extends SignalWatcher(
|
||||
data-column-index="${i}"
|
||||
>
|
||||
</mobile-table-cell>
|
||||
<div class="cell-divider"></div>
|
||||
<div class="${cellDivider}"></div>
|
||||
</div>
|
||||
${!column.readonly$.value &&
|
||||
column.view.mainProperties$.value.titleColumn === column.id
|
||||
@@ -130,7 +130,7 @@ export class MobileTableRow extends SignalWatcher(
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
accessor tableViewLogic!: MobileTableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowId!: string;
|
||||
@@ -138,8 +138,9 @@ export class MobileTableRow extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
|
||||
export const mobileTableViewWrapper = css({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
paddingBottom: '4px',
|
||||
/**
|
||||
* Disable horizontal scrolling to prevent crashes on iOS Safari
|
||||
* See https://github.com/toeverything/AFFiNE/pull/12203
|
||||
* and https://github.com/toeverything/blocksuite/pull/8784
|
||||
*/
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
});
|
||||
|
||||
export const mobileTableViewContainer = css({
|
||||
position: 'relative',
|
||||
width: 'fit-content',
|
||||
minWidth: '100%',
|
||||
});
|
||||
|
||||
export const mobileCellDivider = css({
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
backgroundColor: cssVarV2.layer.insideBorder.border,
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { GroupTrait } from '../../../core/group-by/trait.js';
|
||||
import {
|
||||
createUniComponentFromWebComponent,
|
||||
renderUniLit,
|
||||
} from '../../../core/index.js';
|
||||
import {
|
||||
DataViewUIBase,
|
||||
DataViewUILogicBase,
|
||||
} from '../../../core/view/data-view-base.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import type { TableViewSelectionWithType } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import {
|
||||
mobileTableViewContainer,
|
||||
mobileTableViewWrapper,
|
||||
} from './table-view-style.js';
|
||||
|
||||
export class MobileTableViewUILogic extends DataViewUILogicBase<
|
||||
TableSingleView,
|
||||
TableViewSelectionWithType
|
||||
> {
|
||||
ui$ = signal<MobileTableViewUI | undefined>(undefined);
|
||||
|
||||
private get readonly() {
|
||||
return this.view.readonly$.value;
|
||||
}
|
||||
|
||||
clearSelection = () => {};
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
return this.view.rowAdd(position);
|
||||
};
|
||||
|
||||
focusFirstCell = () => {};
|
||||
|
||||
showIndicator = (_evt: MouseEvent) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
hideIndicator = () => {};
|
||||
|
||||
moveTo = () => {};
|
||||
|
||||
renderAddGroup = (groupHelper: GroupTrait) => {
|
||||
const addGroup = groupHelper.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = groupHelper.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.view.manager.dataSource,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div style="display:flex;">
|
||||
<div
|
||||
class="dv-hover dv-round-8"
|
||||
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
|
||||
@click="${add}"
|
||||
>
|
||||
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
|
||||
<div>New Group</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
renderer = createUniComponentFromWebComponent(MobileTableViewUI);
|
||||
}
|
||||
|
||||
export class MobileTableViewUI extends DataViewUIBase<MobileTableViewUILogic> {
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.logic.ui$.value = this;
|
||||
this.classList.add(mobileTableViewWrapper);
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
const groups = this.logic.view.groupTrait.groupsDataList$.value;
|
||||
if (groups) {
|
||||
return html`
|
||||
<div style="display:flex;flex-direction: column;gap: 16px;">
|
||||
${repeat(
|
||||
groups,
|
||||
v => v.key,
|
||||
group => {
|
||||
return html` <mobile-table-group
|
||||
data-group-key="${group.key}"
|
||||
.tableViewLogic="${this.logic}"
|
||||
.group="${group}"
|
||||
></mobile-table-group>`;
|
||||
}
|
||||
)}
|
||||
${this.logic.renderAddGroup(this.logic.view.groupTrait)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html` <mobile-table-group
|
||||
.tableViewLogic="${this.logic}"
|
||||
></mobile-table-group>`;
|
||||
}
|
||||
|
||||
override render(): TemplateResult {
|
||||
const vPadding = this.logic.root.config.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
});
|
||||
const containerStyle = styleMap({
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.logic.root.config.headerWidget, {
|
||||
dataViewLogic: this.logic,
|
||||
})}
|
||||
<div class="${mobileTableViewWrapper}" style="${wrapperStyle}">
|
||||
<div class="${mobileTableViewContainer}" style="${containerStyle}">
|
||||
${this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-data-view-table-ui': MobileTableViewUI;
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, unsafeCSS } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { GroupTrait } from '../../../core/group-by/trait.js';
|
||||
import type { DataViewInstance } from '../../../core/index.js';
|
||||
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
|
||||
import { DataViewBase } from '../../../core/view/data-view-base.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import type { TableViewSelectionWithType } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
|
||||
export class MobileDataViewTable extends DataViewBase<
|
||||
TableSingleView,
|
||||
TableViewSelectionWithType
|
||||
> {
|
||||
static override styles = css`
|
||||
.mobile-affine-database-table-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
/**
|
||||
* Disable horizontal scrolling to prevent crashes on iOS Safari
|
||||
* See https://github.com/toeverything/AFFiNE/pull/12203
|
||||
* and https://github.com/toeverything/blocksuite/pull/8784
|
||||
*/
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.mobile-affine-database-table-container {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.cell-divider {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: ${unsafeCSS(cssVarV2.layer.insideBorder.border)};
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _addRow = (
|
||||
tableViewManager: TableSingleView,
|
||||
position: InsertToPosition | number
|
||||
) => {
|
||||
if (this.readonly) return;
|
||||
tableViewManager.rowAdd(position);
|
||||
};
|
||||
|
||||
onWheel = (event: WheelEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const ele = event.currentTarget;
|
||||
if (ele instanceof HTMLElement) {
|
||||
if (ele.scrollWidth === ele.clientWidth) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
renderAddGroup = (groupHelper: GroupTrait) => {
|
||||
const addGroup = groupHelper.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = groupHelper.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(
|
||||
() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.props.view.manager.dataSource,
|
||||
}) as never
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div style="display:flex;">
|
||||
<div
|
||||
class="dv-hover dv-round-8"
|
||||
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
|
||||
@click="${add}"
|
||||
>
|
||||
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
|
||||
<div>New Group</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
get expose(): DataViewInstance {
|
||||
return {
|
||||
clearSelection: () => {},
|
||||
addRow: position => {
|
||||
this._addRow(this.props.view, position);
|
||||
},
|
||||
focusFirstCell: () => {},
|
||||
showIndicator: _evt => {
|
||||
return false;
|
||||
},
|
||||
hideIndicator: () => {
|
||||
// this.dragController.dropPreview.remove();
|
||||
},
|
||||
moveTo: (_id, _evt) => {
|
||||
// const result = this.dragController.getInsertPosition(evt);
|
||||
// if (result) {
|
||||
// this.props.view.rowMove(
|
||||
// id,
|
||||
// result.position,
|
||||
// undefined,
|
||||
// result.groupKey,
|
||||
// );
|
||||
// }
|
||||
},
|
||||
getSelection: () => {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.DatabaseBlockError,
|
||||
'Not implemented'
|
||||
);
|
||||
},
|
||||
view: this.props.view,
|
||||
eventTrace: this.props.eventTrace,
|
||||
};
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.props.view.readonly$.value;
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
const groups = this.props.view.groupTrait.groupsDataList$.value;
|
||||
if (groups) {
|
||||
return html`
|
||||
<div style="display:flex;flex-direction: column;gap: 16px;">
|
||||
${repeat(
|
||||
groups,
|
||||
v => v.key,
|
||||
group => {
|
||||
return html` <mobile-table-group
|
||||
data-group-key="${group.key}"
|
||||
.dataViewEle="${this.props.dataViewEle}"
|
||||
.view="${this.props.view}"
|
||||
.viewEle="${this}"
|
||||
.group="${group}"
|
||||
></mobile-table-group>`;
|
||||
}
|
||||
)}
|
||||
${this.renderAddGroup(this.props.view.groupTrait)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html` <mobile-table-group
|
||||
.dataViewEle="${this.props.dataViewEle}"
|
||||
.view="${this.props.view}"
|
||||
.viewEle="${this}"
|
||||
></mobile-table-group>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const vPadding = this.props.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
});
|
||||
const containerStyle = styleMap({
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.props.headerWidget, {
|
||||
dataViewInstance: this.expose,
|
||||
})}
|
||||
<div class="mobile-affine-database-table-wrapper" style="${wrapperStyle}">
|
||||
<div
|
||||
class="mobile-affine-database-table-container"
|
||||
style="${containerStyle}"
|
||||
@wheel="${this.onWheel}"
|
||||
>
|
||||
${this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-data-view-table': MobileDataViewTable;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { UIEventStateContext } from '@blocksuite/std';
|
||||
import type { ReactiveController } from 'lit';
|
||||
|
||||
@@ -9,20 +10,18 @@ import {
|
||||
type TableViewSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../selection';
|
||||
import type { VirtualTableView } from '../table-view.js';
|
||||
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
|
||||
const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table';
|
||||
type JsonAreaData = string[][];
|
||||
const TEXT = 'text/plain';
|
||||
|
||||
export class TableClipboardController implements ReactiveController {
|
||||
disposables = new DisposableGroup();
|
||||
private readonly _onCopy = (
|
||||
tableSelection: TableViewSelectionWithType,
|
||||
isCut = false
|
||||
) => {
|
||||
const table = this.host;
|
||||
|
||||
const area = getSelectedArea(tableSelection, table);
|
||||
const area = getSelectedArea(tableSelection, this.logic);
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
@@ -44,7 +43,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.props.view.rowsDelete(deleteRows);
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -79,12 +78,11 @@ export class TableClipboardController implements ReactiveController {
|
||||
private readonly _onPaste = async (_context: UIEventStateContext) => {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
const view = this.host;
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
const tableSelection = this.selection;
|
||||
if (TableViewRowSelection.is(tableSelection)) {
|
||||
return;
|
||||
}
|
||||
@@ -97,7 +95,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
if (dataString) {
|
||||
// If internal format data exists, use it
|
||||
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
|
||||
pasteToCells(view, jsonAreaData, tableSelection);
|
||||
pasteToCells(this.logic, jsonAreaData, tableSelection);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
@@ -115,7 +113,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
|
||||
|
||||
if (rows.length > 0) {
|
||||
pasteToCells(view, rows, tableSelection);
|
||||
pasteToCells(this.logic, rows, tableSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,27 +122,28 @@ export class TableClipboardController implements ReactiveController {
|
||||
};
|
||||
|
||||
private get clipboard() {
|
||||
return this.props.clipboard;
|
||||
return this.logic.root.config.clipboard;
|
||||
}
|
||||
|
||||
private get notification() {
|
||||
return this.props.notification;
|
||||
}
|
||||
|
||||
get props() {
|
||||
return this.host.props;
|
||||
return this.logic.root.config.notification;
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.props.view.readonly$.value;
|
||||
return this.logic.view.readonly$.value;
|
||||
}
|
||||
|
||||
constructor(public host: VirtualTableView) {
|
||||
host.addController(this);
|
||||
constructor(public logic: VirtualTableViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
get selection() {
|
||||
return this.logic.selectionController.selection;
|
||||
}
|
||||
|
||||
copy() {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
const tableSelection = this.selection;
|
||||
if (!tableSelection) {
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
|
||||
cut() {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
const tableSelection = this.selection;
|
||||
if (!tableSelection) {
|
||||
return;
|
||||
}
|
||||
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.props.handleEvent('copy', _ctx => {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
this.disposables.add(
|
||||
this.logic.handleEvent('copy', _ctx => {
|
||||
const tableSelection = this.selection;
|
||||
if (!tableSelection) return false;
|
||||
|
||||
this._onCopy(tableSelection);
|
||||
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.add(
|
||||
this.props.handleEvent('cut', _ctx => {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
this.disposables.add(
|
||||
this.logic.handleEvent('cut', _ctx => {
|
||||
const tableSelection = this.selection;
|
||||
if (!tableSelection) return false;
|
||||
|
||||
this._onCut(tableSelection);
|
||||
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.add(
|
||||
this.props.handleEvent('paste', ctx => {
|
||||
this.disposables.add(
|
||||
this.logic.handleEvent('paste', ctx => {
|
||||
if (this.readonly) return false;
|
||||
|
||||
this._onPaste(ctx).catch(console.error);
|
||||
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
|
||||
|
||||
function getSelectedArea(
|
||||
selection: TableViewSelection,
|
||||
table: VirtualTableView
|
||||
table: VirtualTableViewUILogic
|
||||
): SelectedArea | undefined {
|
||||
const view = table.props.view;
|
||||
const view = table.view;
|
||||
if (TableViewRowSelection.is(selection)) {
|
||||
const rows = TableViewRowSelection.rows(selection)
|
||||
.map(row => {
|
||||
@@ -282,7 +281,7 @@ function getTargetRangeFromSelection(
|
||||
}
|
||||
|
||||
function pasteToCells(
|
||||
table: VirtualTableView,
|
||||
table: VirtualTableViewUILogic,
|
||||
rows: JsonAreaData,
|
||||
selection: TableViewAreaSelection
|
||||
) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as Y from 'yjs';
|
||||
|
||||
import { t } from '../../../../core/index.js';
|
||||
import type { TableViewAreaSelection } from '../../selection';
|
||||
import type { VirtualTableView } from '../table-view';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
|
||||
|
||||
export class DragToFillElement extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
@@ -55,12 +55,12 @@ declare global {
|
||||
}
|
||||
|
||||
export function fillSelectionWithFocusCellData(
|
||||
host: VirtualTableView,
|
||||
logic: VirtualTableViewUILogic,
|
||||
selection: TableViewAreaSelection
|
||||
) {
|
||||
const { groupKey, rowsSelection, columnsSelection, focus } = selection;
|
||||
|
||||
const focusCell = host.selectionController.getCellContainer(
|
||||
const focusCell = logic.selectionController.getCellContainer(
|
||||
groupKey,
|
||||
focus.rowIndex,
|
||||
focus.columnIndex
|
||||
@@ -85,7 +85,7 @@ export function fillSelectionWithFocusCellData(
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i === focus.rowIndex) continue;
|
||||
|
||||
const cellContainer = host.selectionController.getCellContainer(
|
||||
const cellContainer = logic.selectionController.getCellContainer(
|
||||
groupKey,
|
||||
i,
|
||||
draggingColIdx
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import type { ReactiveController } from 'lit';
|
||||
|
||||
// import { startDrag } from '../../../../core/utils/drag.js';
|
||||
import type { VirtualTableView } from '../table-view';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
|
||||
|
||||
export class TableDragController implements ReactiveController {
|
||||
// dragStart = (row: TableRow, evt: PointerEvent) => {
|
||||
@@ -91,9 +90,9 @@ export class TableDragController implements ReactiveController {
|
||||
| undefined => {
|
||||
const y = evt.y;
|
||||
const tableRect = this.host
|
||||
.querySelector('affine-data-view-table-group')
|
||||
?.querySelector('affine-data-view-table-group')
|
||||
?.getBoundingClientRect();
|
||||
const rows = this.host.querySelectorAll('data-view-table-row');
|
||||
const rows = this.host?.querySelectorAll('data-view-table-row');
|
||||
if (!rows || !tableRect || y < tableRect.top) {
|
||||
return;
|
||||
}
|
||||
@@ -127,12 +126,14 @@ export class TableDragController implements ReactiveController {
|
||||
return position;
|
||||
};
|
||||
|
||||
constructor(private readonly host: VirtualTableView) {
|
||||
this.host.addController(this);
|
||||
constructor(private readonly logic: VirtualTableViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
if (this.host.props.view.readonly$.value) {
|
||||
if (this.logic.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
// this.host.disposables.add(
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { ReactiveController } from 'lit';
|
||||
|
||||
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
|
||||
import { popRowMenu } from '../row/menu';
|
||||
import type { VirtualTableView } from '../table-view.js';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
|
||||
|
||||
export class TableHotkeysController implements ReactiveController {
|
||||
disposables = new DisposableGroup();
|
||||
get selectionController() {
|
||||
return this.host.selectionController;
|
||||
return this.logic.selectionController;
|
||||
}
|
||||
|
||||
constructor(private readonly host: VirtualTableView) {
|
||||
this.host.addController(this);
|
||||
constructor(private readonly logic: VirtualTableViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.bindHotkey({
|
||||
this.disposables.add(
|
||||
this.logic.bindHotkey({
|
||||
Backspace: () => {
|
||||
const selection = this.selectionController.selection;
|
||||
if (!selection) {
|
||||
@@ -25,7 +29,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
if (TableViewRowSelection.is(selection)) {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.host.props.view.rowsDelete(rows);
|
||||
this.logic.view.rowsDelete(rows);
|
||||
return;
|
||||
}
|
||||
const {
|
||||
@@ -334,14 +338,14 @@ export class TableHotkeysController implements ReactiveController {
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
this.selectionController.selection = TableViewRowSelection.create({
|
||||
rows:
|
||||
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap(
|
||||
this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
|
||||
group =>
|
||||
group?.rows.map(row => ({
|
||||
groupKey: group.key,
|
||||
id: row.rowId,
|
||||
})) ?? []
|
||||
) ??
|
||||
this.host.props.view.rows$.value.map(row => ({
|
||||
this.logic.view.rows$.value.map(row => ({
|
||||
groupKey: undefined,
|
||||
id: row.rowId,
|
||||
})),
|
||||
@@ -377,7 +381,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
rows: [row],
|
||||
});
|
||||
popRowMenu(
|
||||
this.host.props.dataViewEle,
|
||||
this.logic,
|
||||
popupTargetFromElement(cell),
|
||||
this.selectionController
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getRangeByPositions } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../selection';
|
||||
import type { DatabaseCellContainer } from '../row/cell';
|
||||
import type { VirtualTableView } from '../table-view.js';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
|
||||
import type { TableGridCell } from '../types.js';
|
||||
import {
|
||||
DragToFillElement,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from './drag-to-fill.js';
|
||||
|
||||
export class TableSelectionController implements ReactiveController {
|
||||
disposables = new DisposableGroup();
|
||||
private _tableViewSelection?: TableViewSelectionWithType;
|
||||
|
||||
private readonly getFocusCellContainer = () => {
|
||||
@@ -85,12 +87,12 @@ export class TableSelectionController implements ReactiveController {
|
||||
);
|
||||
const cell = container?.cell;
|
||||
const isEditing = cell ? cell.beforeEnterEditMode() : true;
|
||||
this.host.props.setSelection({
|
||||
this.logic.setSelection({
|
||||
...selection,
|
||||
isEditing,
|
||||
});
|
||||
} else {
|
||||
this.host.props.setSelection(selection);
|
||||
this.logic.setSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,27 +101,26 @@ export class TableSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.host.props.view;
|
||||
return this.logic.view;
|
||||
}
|
||||
|
||||
get viewData() {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
constructor(public host: VirtualTableView) {
|
||||
host.addController(this);
|
||||
constructor(public logic: VirtualTableViewUILogic) {
|
||||
this.__selectionElement = new SelectionElement();
|
||||
this.__selectionElement.controller = this;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
private clearSelection() {
|
||||
this.host.props.setSelection();
|
||||
this.logic.setSelection();
|
||||
}
|
||||
|
||||
private handleDragEvent() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.handleEvent('dragStart', context => {
|
||||
if (this.host.props.view.readonly$.value) {
|
||||
this.disposables.add(
|
||||
this.logic.handleEvent('dragStart', context => {
|
||||
if (this.logic.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const event = context.get('pointerState').raw;
|
||||
@@ -150,8 +151,8 @@ export class TableSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
private handleSelectionChange() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.selection$.subscribe(tableSelection => {
|
||||
this.disposables.add(
|
||||
this.logic.selection$.subscribe(tableSelection => {
|
||||
if (!this.isValidSelection(tableSelection)) {
|
||||
this.selection = undefined;
|
||||
return;
|
||||
@@ -236,7 +237,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
|
||||
: this.view.rows$.value;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.host.props.view.properties$.value.findIndex(
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
this.selection = TableViewAreaSelection.create({
|
||||
@@ -625,7 +626,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
get virtualScroll() {
|
||||
return this.host.virtualScroll$.value;
|
||||
return this.logic.virtualScroll$.value;
|
||||
}
|
||||
|
||||
getGroup(groupKey: string | undefined) {
|
||||
@@ -913,7 +914,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
if (fillValues && this.selection) {
|
||||
this.__dragToFillElement.dragging = false;
|
||||
fillSelectionWithFocusCellData(
|
||||
this.host,
|
||||
this.logic,
|
||||
TableViewAreaSelection.create({
|
||||
groupKey: groupKey,
|
||||
rowsSelection: selection.row,
|
||||
@@ -1024,7 +1025,7 @@ export class SelectionElement extends SignalWatcher(
|
||||
if (left == null || top == null || width == null || height == null) {
|
||||
return;
|
||||
}
|
||||
const paddingLeft = this.controller.host.props.virtualPadding$.value;
|
||||
const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
|
||||
return {
|
||||
left: left + paddingLeft,
|
||||
top,
|
||||
@@ -1054,7 +1055,7 @@ export class SelectionElement extends SignalWatcher(
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
const paddingLeft = this.controller.host.props.virtualPadding$.value;
|
||||
const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
|
||||
return {
|
||||
left: rect.left + paddingLeft,
|
||||
top: rect.top,
|
||||
@@ -1063,15 +1064,8 @@ export class SelectionElement extends SignalWatcher(
|
||||
};
|
||||
});
|
||||
|
||||
rowsPosition$ = computed(() => {
|
||||
const selection = this.selection$.value;
|
||||
if (selection?.selectionType !== 'area') {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
get selection$() {
|
||||
return this.controller.host.props.selection$;
|
||||
return this.controller.logic.selection$;
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { KanbanViewUI } from '../../kanban/pc/kanban-view-ui-logic';
|
||||
import { DragToFillElement } from './controller/drag-to-fill';
|
||||
import { SelectionElement } from './controller/selection';
|
||||
import { TableGroupFooter } from './group/bottom/group-footer';
|
||||
@@ -12,11 +13,12 @@ import { TableVerticalIndicator } from './group/top/header/vertical-indicator';
|
||||
import { DatabaseCellContainer } from './row/cell';
|
||||
import { TableRowHeader } from './row/row-header';
|
||||
import { TableRowLast } from './row/row-last';
|
||||
import { VirtualTableView } from './table-view';
|
||||
import { TableViewUI } from './table-view-ui-logic';
|
||||
import { VirtualElementWrapper } from './virtual/virtual-cell';
|
||||
|
||||
export function pcVirtualEffects() {
|
||||
customElements.define('affine-virtual-table', VirtualTableView);
|
||||
customElements.define('dv-table-view-ui-virtual', TableViewUI);
|
||||
customElements.define('dv-kanban-view-ui', KanbanViewUI);
|
||||
customElements.define(
|
||||
'affine-database-virtual-cell-container',
|
||||
DatabaseCellContainer
|
||||
|
||||
@@ -6,29 +6,29 @@ import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { TableViewAreaSelection } from '../../../selection';
|
||||
import type { VirtualTableView } from '../../table-view';
|
||||
import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
|
||||
import type { TableGridGroup } from '../../types';
|
||||
import * as styles from './group-footer-css';
|
||||
|
||||
export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor tableView!: VirtualTableView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor gridGroup!: TableGridGroup;
|
||||
|
||||
group$ = computed(() => {
|
||||
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find(
|
||||
return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
|
||||
g => g.key === this.gridGroup.groupId
|
||||
);
|
||||
});
|
||||
|
||||
get selectionController() {
|
||||
return this.tableView.selectionController;
|
||||
return this.tableViewLogic.selectionController;
|
||||
}
|
||||
|
||||
get tableViewManager() {
|
||||
return this.tableView.props.view;
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { TableViewAreaSelection } from '../../../selection';
|
||||
import type { VirtualTableView } from '../../table-view';
|
||||
import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
|
||||
import type { TableGridGroup } from '../../types';
|
||||
import * as styles from './group-header-css';
|
||||
import { GroupTitle } from './group-title';
|
||||
@@ -18,7 +18,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
@property({ attribute: false })
|
||||
accessor tableView!: VirtualTableView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor gridGroup!: TableGridGroup;
|
||||
@@ -35,7 +35,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
}
|
||||
|
||||
group$ = computed(() => {
|
||||
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find(
|
||||
return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
|
||||
g => g.key === this.gridGroup.groupId
|
||||
);
|
||||
});
|
||||
@@ -45,11 +45,11 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
});
|
||||
|
||||
get tableViewManager() {
|
||||
return this.tableView.props.view;
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
|
||||
get selectionController() {
|
||||
return this.tableView.selectionController;
|
||||
return this.tableViewLogic.selectionController;
|
||||
}
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
@@ -123,7 +123,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
return html`
|
||||
${this.renderGroupHeader()}
|
||||
<virtual-table-header
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
></virtual-table-header>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { PlusIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { css } from '@emotion/css';
|
||||
import { nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
@@ -13,9 +14,13 @@ import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { renderUniLit } from '../../../../../../core';
|
||||
import type { TableSingleView } from '../../../../table-view-manager';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../../../../consts';
|
||||
import { cellDivider } from '../../../../styles';
|
||||
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
|
||||
import * as styles from './column-header-css';
|
||||
|
||||
const leftBarStyle = css({
|
||||
width: LEFT_TOOL_BAR_WIDTH,
|
||||
});
|
||||
export class VirtualTableHeader extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
@@ -79,9 +84,7 @@ export class VirtualTableHeader extends SignalWatcher(
|
||||
override render() {
|
||||
return html`
|
||||
<div class="${styles.columnHeader} database-row">
|
||||
${this.readonly
|
||||
? nothing
|
||||
: html` <div class="data-view-table-left-bar"></div>`}
|
||||
${this.readonly ? nothing : html` <div class="${leftBarStyle}"></div>`}
|
||||
${repeat(
|
||||
this.tableViewManager.properties$.value,
|
||||
column => column.id,
|
||||
@@ -97,9 +100,9 @@ export class VirtualTableHeader extends SignalWatcher(
|
||||
data-column-index="${index}"
|
||||
class="${styles.column} ${styles.cell}"
|
||||
.column="${column}"
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
></virtual-database-header-column>
|
||||
<div class="cell-divider" style="height: auto;"></div>
|
||||
<div class="${cellDivider}" style="height: auto;"></div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
@@ -118,7 +121,11 @@ export class VirtualTableHeader extends SignalWatcher(
|
||||
accessor scaleDiv!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewManager!: TableSingleView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
|
||||
get tableViewManager() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
TableProperty,
|
||||
TableSingleView,
|
||||
} from '../../../../table-view-manager';
|
||||
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
|
||||
|
||||
export class DataViewColumnPreview extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
`;
|
||||
|
||||
get tableViewManager(): TableSingleView {
|
||||
return this.column.view as TableSingleView;
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
|
||||
private renderGroup(rows: Row[]) {
|
||||
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
)};box-shadow: var(--affine-shadow-2);"
|
||||
>
|
||||
<affine-database-header-column
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.column="${this.column}"
|
||||
></affine-database-header-column>
|
||||
${repeat(rows, (id, index) => {
|
||||
const height = this.container.querySelector(
|
||||
`affine-database-cell-container[data-row-id="${id}"]`
|
||||
`dv-table-view-cell-container[data-row-id="${id}"]`
|
||||
)?.clientHeight;
|
||||
const style = styleMap({
|
||||
height: height + 'px',
|
||||
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
)}"
|
||||
>
|
||||
<div style="${style}">
|
||||
<affine-database-cell-container
|
||||
<dv-table-view-cell-container
|
||||
.column="${this.column}"
|
||||
.view="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.rowId="${id}"
|
||||
.columnId="${this.column.id}"
|
||||
.rowIndex="${index}"
|
||||
.columnIndex="${columnIndex}"
|
||||
></affine-database-cell-container>
|
||||
></dv-table-view-cell-container>
|
||||
</div>
|
||||
</div>`;
|
||||
})}
|
||||
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group: Group | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -45,10 +45,8 @@ import {
|
||||
ShowQuickSettingBarKey,
|
||||
} from '../../../../../../widget-presets/quick-setting-bar/context';
|
||||
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts';
|
||||
import type {
|
||||
TableProperty,
|
||||
TableSingleView,
|
||||
} from '../../../../table-view-manager';
|
||||
import type { TableProperty } from '../../../../table-view-manager';
|
||||
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
|
||||
import {
|
||||
getTableGroupRect,
|
||||
getVerticalIndicator,
|
||||
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
|
||||
|
||||
const sortUtils = createSortUtils(
|
||||
sortTrait,
|
||||
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {})
|
||||
this.tableViewLogic.eventTrace ?? (() => {})
|
||||
);
|
||||
const sortList = sortUtils.sortList$.value;
|
||||
const existingIndex = sortList.findIndex(
|
||||
@@ -398,28 +396,25 @@ export class DatabaseHeaderColumn extends SignalWatcher(
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const table = this.closest('affine-database-table');
|
||||
if (table) {
|
||||
this.disposables.add(
|
||||
table.props.handleEvent('dragStart', context => {
|
||||
if (this.tableViewManager.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const event = context.get('pointerState').raw;
|
||||
const target = event.target;
|
||||
if (
|
||||
target instanceof Element &&
|
||||
this.widthDragBar.value?.contains(target)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.widthDragStart(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
this.disposables.add(
|
||||
this.tableViewLogic.handleEvent('dragStart', context => {
|
||||
if (this.tableViewManager.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const event = context.get('pointerState').raw;
|
||||
const target = event.target;
|
||||
if (
|
||||
target instanceof Element &&
|
||||
this.widthDragBar.value?.contains(target)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.widthDragStart(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -481,7 +476,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
|
||||
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewManager!: TableSingleView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
|
||||
get tableViewManager() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
function numberFormatConfig(column: Property): MenuConfig {
|
||||
|
||||
@@ -10,13 +10,12 @@ import type {
|
||||
CellRenderProps,
|
||||
DataViewCellLifeCycle,
|
||||
} from '../../../../core/property';
|
||||
import type { SingleView } from '../../../../core/view-manager/single-view';
|
||||
import {
|
||||
TableViewAreaSelection,
|
||||
TableViewRowSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../selection';
|
||||
import type { VirtualTableView } from '../table-view';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
|
||||
import type { TableGridCell } from '../types';
|
||||
import { popRowMenu } from './menu';
|
||||
import { rowSelectedBg } from './row-header-css';
|
||||
@@ -82,7 +81,7 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
}
|
||||
|
||||
private get selectionView() {
|
||||
return this.tableView?.selectionController;
|
||||
return this.tableViewLogic.selectionController;
|
||||
}
|
||||
|
||||
get rowSelected$() {
|
||||
@@ -104,11 +103,7 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
rows: [row],
|
||||
});
|
||||
}
|
||||
popRowMenu(
|
||||
this.tableView.props.dataViewEle,
|
||||
popupTargetFromElement(this),
|
||||
selection
|
||||
);
|
||||
popRowMenu(this.tableViewLogic, popupTargetFromElement(this), selection);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -216,10 +211,11 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
accessor gridCell!: TableGridCell;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: SingleView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableView!: VirtualTableView;
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -12,19 +12,19 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import type { DataViewRenderer } from '../../../../core/data-view';
|
||||
import { TableViewRowSelection } from '../../selection';
|
||||
import type { TableSelectionController } from '../controller/selection';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
|
||||
|
||||
export const openDetail = (
|
||||
dataViewEle: DataViewRenderer,
|
||||
tableViewLogic: VirtualTableViewUILogic,
|
||||
rowId: string,
|
||||
selection: TableSelectionController
|
||||
) => {
|
||||
const old = selection.selection;
|
||||
selection.selection = undefined;
|
||||
dataViewEle.openDetailPanel({
|
||||
view: selection.host.props.view,
|
||||
tableViewLogic.root.openDetailPanel({
|
||||
view: tableViewLogic.view,
|
||||
rowId: rowId,
|
||||
onClose: () => {
|
||||
selection.selection = old;
|
||||
@@ -33,7 +33,7 @@ export const openDetail = (
|
||||
};
|
||||
|
||||
export const popRowMenu = (
|
||||
dataViewEle: DataViewRenderer,
|
||||
tableViewLogic: VirtualTableViewUILogic,
|
||||
ele: PopupTarget,
|
||||
selectionController: TableSelectionController
|
||||
) => {
|
||||
@@ -55,7 +55,7 @@ export const popRowMenu = (
|
||||
${CopyIcon()}
|
||||
</div>`,
|
||||
select: () => {
|
||||
selectionController.host.clipboardController.copy();
|
||||
tableViewLogic.clipboardController.copy();
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -85,7 +85,7 @@ export const popRowMenu = (
|
||||
name: 'Expand Row',
|
||||
prefix: ExpandFullIcon(),
|
||||
select: () => {
|
||||
openDetail(dataViewEle, row.id, selectionController);
|
||||
openDetail(tableViewLogic, row.id, selectionController);
|
||||
},
|
||||
}),
|
||||
menu.group({
|
||||
|
||||
@@ -7,14 +7,17 @@ import { nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { TableSingleView } from '../../table-view-manager.js';
|
||||
import type { VirtualTableView } from '../table-view.js';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
|
||||
import type { TableGridCell } from '../types.js';
|
||||
import * as styles from './row-header-css.js';
|
||||
|
||||
export class TableRowHeader extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.disposables.add(
|
||||
@@ -42,7 +45,7 @@ export class TableRowHeader extends SignalWatcher(
|
||||
};
|
||||
|
||||
get selectionController() {
|
||||
return this.tableView.selectionController;
|
||||
return this.tableViewLogic.selectionController;
|
||||
}
|
||||
|
||||
get rowSelected$() {
|
||||
@@ -115,10 +118,7 @@ export class TableRowHeader extends SignalWatcher(
|
||||
accessor gridCell!: TableGridCell;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableView!: VirtualTableView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,8 +5,7 @@ import { effect } from '@preact/signals-core';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { TableSingleView } from '../../table-view-manager.js';
|
||||
import type { VirtualTableView } from '../table-view.js';
|
||||
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
|
||||
import type { TableGridCell } from '../types.js';
|
||||
import * as styles from './row-header-css.js';
|
||||
|
||||
@@ -56,10 +55,7 @@ export class TableRowLast extends SignalWatcher(
|
||||
accessor gridCell!: TableGridCell;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableView!: VirtualTableView;
|
||||
accessor tableViewLogic!: VirtualTableViewUILogic;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -3,19 +3,27 @@ import {
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import * as dv from '../../../core/common/dv-css.js';
|
||||
import { dv } from '../../../core/common/dv-css.js';
|
||||
import {
|
||||
type GroupTrait,
|
||||
groupTraitKey,
|
||||
} from '../../../core/group-by/trait.js';
|
||||
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
|
||||
import { DataViewBase } from '../../../core/view/data-view-base.js';
|
||||
import {
|
||||
createUniComponentFromWebComponent,
|
||||
renderUniLit,
|
||||
} from '../../../core/index.js';
|
||||
import {
|
||||
DataViewUIBase,
|
||||
DataViewUILogicBase,
|
||||
} from '../../../core/view/data-view-base.js';
|
||||
import {
|
||||
type TableSingleView,
|
||||
TableViewRowSelection,
|
||||
@@ -26,9 +34,9 @@ import { TableClipboardController } from './controller/clipboard.js';
|
||||
import { TableDragController } from './controller/drag.js';
|
||||
import { TableHotkeysController } from './controller/hotkeys.js';
|
||||
import { TableSelectionController } from './controller/selection.js';
|
||||
import { TableGroupFooter } from './group/bottom/group-footer';
|
||||
import { TableGroupHeader } from './group/top/group-header';
|
||||
import { DatabaseCellContainer } from './row/cell';
|
||||
import { TableGroupFooter } from './group/bottom/group-footer.js';
|
||||
import { TableGroupHeader } from './group/top/group-header.js';
|
||||
import { DatabaseCellContainer } from './row/cell.js';
|
||||
import { TableRowHeader } from './row/row-header.js';
|
||||
import { TableRowLast } from './row/row-last.js';
|
||||
import * as styles from './table-view-css.js';
|
||||
@@ -43,15 +51,83 @@ import {
|
||||
GridVirtualScroll,
|
||||
} from './virtual/virtual-scroll.js';
|
||||
|
||||
export class VirtualTableView extends DataViewBase<
|
||||
export class VirtualTableViewUILogic extends DataViewUILogicBase<
|
||||
TableSingleView,
|
||||
TableViewSelectionWithType
|
||||
> {
|
||||
ui$ = signal<TableViewUI | undefined>();
|
||||
clipboardController = new TableClipboardController(this);
|
||||
|
||||
dragController = new TableDragController(this);
|
||||
|
||||
hotkeysController = new TableHotkeysController(this);
|
||||
selectionController = new TableSelectionController(this);
|
||||
|
||||
virtualScroll$ = signal<TableGrid>();
|
||||
yScrollContainer: HTMLElement | undefined;
|
||||
|
||||
columns$ = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 'row-header',
|
||||
width: LEFT_TOOL_BAR_WIDTH,
|
||||
},
|
||||
...this.view.properties$.value.map(property => ({
|
||||
id: property.id,
|
||||
width: property.width$.value + 1,
|
||||
})),
|
||||
{
|
||||
id: 'row-last',
|
||||
width: 40,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
groupTrait$ = computed(() => {
|
||||
return this.view.traitGet(groupTraitKey);
|
||||
});
|
||||
|
||||
groups$ = computed(() => {
|
||||
const groupTrait = this.groupTrait$.value;
|
||||
if (!groupTrait?.groupsDataList$.value) {
|
||||
return [
|
||||
{
|
||||
id: '',
|
||||
rows: this.view.rowIds$.value,
|
||||
},
|
||||
];
|
||||
}
|
||||
return groupTrait.groupsDataList$.value.map(group => ({
|
||||
id: group.key,
|
||||
rows: group.rows.map(v => v.rowId),
|
||||
}));
|
||||
});
|
||||
|
||||
clearSelection = () => {
|
||||
this.selectionController.clear();
|
||||
};
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
return this.view.rowAdd(position);
|
||||
};
|
||||
|
||||
focusFirstCell = () => {
|
||||
this.selectionController.focusFirstCell();
|
||||
};
|
||||
|
||||
showIndicator = (evt: MouseEvent) => {
|
||||
return this.dragController.showIndicator(evt) != null;
|
||||
};
|
||||
|
||||
hideIndicator = () => {
|
||||
this.dragController.dropPreview.remove();
|
||||
};
|
||||
|
||||
moveTo = (id: string, evt: MouseEvent) => {
|
||||
const result = this.dragController.getInsertPosition(evt);
|
||||
if (result) {
|
||||
const row = this.view.rowGetOrCreate(id);
|
||||
row.move(result.position, undefined, result.groupKey);
|
||||
}
|
||||
};
|
||||
|
||||
onWheel = (event: WheelEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
@@ -80,13 +156,12 @@ export class VirtualTableView extends DataViewBase<
|
||||
onComplete: text => {
|
||||
const column = groupHelper.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(
|
||||
() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.props.view.manager.dataSource,
|
||||
}) as never
|
||||
column.dataUpdate(() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.view.manager.dataSource,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -103,92 +178,8 @@ export class VirtualTableView extends DataViewBase<
|
||||
</div>`;
|
||||
};
|
||||
|
||||
selectionController = new TableSelectionController(this);
|
||||
yScrollContainer: HTMLElement | undefined;
|
||||
|
||||
get expose(): DataViewInstance {
|
||||
return {
|
||||
clearSelection: () => {
|
||||
this.selectionController.clear();
|
||||
},
|
||||
addRow: position => {
|
||||
if (this.readonly) return;
|
||||
const rowId = this.props.view.rowAdd(position);
|
||||
if (rowId) {
|
||||
this.props.dataViewEle.openDetailPanel({
|
||||
view: this.props.view,
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
return rowId;
|
||||
},
|
||||
focusFirstCell: () => {
|
||||
this.selectionController.focusFirstCell();
|
||||
},
|
||||
showIndicator: evt => {
|
||||
return this.dragController.showIndicator(evt) != null;
|
||||
},
|
||||
hideIndicator: () => {
|
||||
this.dragController.dropPreview.remove();
|
||||
},
|
||||
moveTo: (id, evt) => {
|
||||
const result = this.dragController.getInsertPosition(evt);
|
||||
if (result) {
|
||||
const row = this.props.view.rowGetOrCreate(id);
|
||||
row.move(result.position, undefined, result.groupKey);
|
||||
}
|
||||
},
|
||||
getSelection: () => {
|
||||
return this.selectionController.selection;
|
||||
},
|
||||
view: this.props.view,
|
||||
eventTrace: this.props.eventTrace,
|
||||
};
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.props.view.readonly$.value;
|
||||
}
|
||||
|
||||
columns$ = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 'row-header',
|
||||
width: LEFT_TOOL_BAR_WIDTH,
|
||||
},
|
||||
...this.props.view.properties$.value.map(property => ({
|
||||
id: property.id,
|
||||
width: property.width$.value + 1,
|
||||
})),
|
||||
{
|
||||
id: 'row-last',
|
||||
width: 40,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
groupTrait$ = computed(() => {
|
||||
return this.props.view.traitGet(groupTraitKey);
|
||||
});
|
||||
|
||||
groups$ = computed(() => {
|
||||
const groupTrait = this.groupTrait$.value;
|
||||
if (!groupTrait?.groupsDataList$.value) {
|
||||
return [
|
||||
{
|
||||
id: '',
|
||||
rows: this.props.view.rowIds$.value,
|
||||
},
|
||||
];
|
||||
}
|
||||
return groupTrait.groupsDataList$.value.map(group => ({
|
||||
id: group.key,
|
||||
rows: group.rows.map(v => v.rowId),
|
||||
}));
|
||||
});
|
||||
virtualScroll$ = signal<TableGrid>();
|
||||
private initVirtualScroll(yScrollContainer: HTMLElement) {
|
||||
this.virtualScroll$.value = new GridVirtualScroll<
|
||||
initVirtualScroll(yScrollContainer: HTMLElement, ui: TableViewUI) {
|
||||
const virtualScroll = new GridVirtualScroll<
|
||||
TableGroupData,
|
||||
TableRowData,
|
||||
TableCellData
|
||||
@@ -213,7 +204,7 @@ export class VirtualTableView extends DataViewBase<
|
||||
return row.cells$.value.some(cell => cell.data.hover$.value);
|
||||
}),
|
||||
selected$: computed(() => {
|
||||
const selection = this.props.selection$.value;
|
||||
const selection = this.selection$.value;
|
||||
if (!selection || selection.selectionType !== 'row') {
|
||||
return false;
|
||||
}
|
||||
@@ -234,34 +225,31 @@ export class VirtualTableView extends DataViewBase<
|
||||
if (cell.columnId === 'row-header') {
|
||||
wrapper.style.borderBottom = `1px solid ${cssVarV2.database.border}`;
|
||||
const rowHeader = new TableRowHeader();
|
||||
rowHeader.view = this.props.view;
|
||||
rowHeader.gridCell = cell;
|
||||
rowHeader.tableView = this;
|
||||
rowHeader.tableViewLogic = this;
|
||||
return rowHeader;
|
||||
}
|
||||
if (cell.columnId === 'row-last') {
|
||||
const rowLast = new TableRowLast();
|
||||
rowLast.view = this.props.view;
|
||||
rowLast.gridCell = cell;
|
||||
rowLast.tableView = this;
|
||||
rowLast.tableViewLogic = this;
|
||||
return rowLast;
|
||||
}
|
||||
const cellContainer = new DatabaseCellContainer();
|
||||
cellContainer.view = this.props.view;
|
||||
cellContainer.gridCell = cell;
|
||||
cellContainer.tableView = this;
|
||||
cellContainer.tableViewLogic = this;
|
||||
return cellContainer;
|
||||
},
|
||||
createGroup: {
|
||||
top: gridGroup => {
|
||||
const groupHeader = new TableGroupHeader();
|
||||
groupHeader.tableView = this;
|
||||
groupHeader.tableViewLogic = this;
|
||||
groupHeader.gridGroup = gridGroup;
|
||||
return groupHeader;
|
||||
},
|
||||
bottom: gridGroup => {
|
||||
const groupFooter = new TableGroupFooter();
|
||||
groupFooter.tableView = this;
|
||||
groupFooter.tableViewLogic = this;
|
||||
groupFooter.gridGroup = gridGroup;
|
||||
return groupFooter;
|
||||
},
|
||||
@@ -269,26 +257,40 @@ export class VirtualTableView extends DataViewBase<
|
||||
fixedRowHeight$: signal(undefined),
|
||||
yScrollContainer,
|
||||
});
|
||||
|
||||
this.yScrollContainer = yScrollContainer;
|
||||
|
||||
this.virtualScroll$.value = virtualScroll;
|
||||
requestAnimationFrame(() => {
|
||||
const virtualScroll = this.virtualScroll$.value;
|
||||
if (virtualScroll) {
|
||||
virtualScroll.init();
|
||||
this.disposables.add(() => virtualScroll.dispose());
|
||||
ui.disposables.add(() => virtualScroll.dispose());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderer = createUniComponentFromWebComponent(TableViewUI);
|
||||
}
|
||||
|
||||
export class TableViewUI extends DataViewUIBase<VirtualTableViewUILogic> {
|
||||
private renderTable() {
|
||||
return this.virtualScroll$.value?.content;
|
||||
return this.logic.virtualScroll$.value?.content;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.initVirtualScroll(getScrollContainer(this, 'y') ?? document.body);
|
||||
this.logic.ui$.value = this;
|
||||
this.logic.clipboardController.hostConnected();
|
||||
this.logic.dragController.hostConnected();
|
||||
this.logic.hotkeysController.hostConnected();
|
||||
this.logic.selectionController.hostConnected();
|
||||
const scrollContainer = getScrollContainer(this, 'y') ?? document.body;
|
||||
this.logic.initVirtualScroll(scrollContainer, this);
|
||||
this.classList.add(styles.tableView);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const vPadding = this.props.virtualPadding$.value;
|
||||
override render(): TemplateResult {
|
||||
const vPadding = this.logic.root.config.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
@@ -298,11 +300,11 @@ export class VirtualTableView extends DataViewBase<
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.props.headerWidget, {
|
||||
dataViewInstance: this.expose,
|
||||
${renderUniLit(this.logic.root.config.headerWidget, {
|
||||
dataViewLogic: this.logic,
|
||||
})}
|
||||
<div class="${styles.tableContainer}" style="${wrapperStyle}">
|
||||
<div class="${styles.tableBlockTable}" @wheel="${this.onWheel}">
|
||||
<div class="${styles.tableBlockTable}" @wheel="${this.logic.onWheel}">
|
||||
<div class="${styles.tableContainer2}" style="${containerStyle}">
|
||||
${this.renderTable()}
|
||||
</div>
|
||||
@@ -314,6 +316,6 @@ export class VirtualTableView extends DataViewBase<
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-virtual-table': VirtualTableView;
|
||||
'dv-table-view-ui-virtual': TableViewUI;
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,8 @@ export class BatchTaskManager {
|
||||
|
||||
private run(): void {
|
||||
let totalBatchCount = this.totalBatchSize;
|
||||
// let skipCount = 0;
|
||||
// let tasksExecuted = false;
|
||||
let skipCount = 0;
|
||||
let tasksExecuted = false;
|
||||
const runTaskArr = this.queues.map(() => 0);
|
||||
for (let i = this.queues.length - 1; i >= 0; i--) {
|
||||
const queue = this.queues[i];
|
||||
@@ -82,22 +82,22 @@ export class BatchTaskManager {
|
||||
if (result !== false) {
|
||||
totalBatchCount--;
|
||||
priorityBatchCount--;
|
||||
// tasksExecuted = true;
|
||||
tasksExecuted = true;
|
||||
runTaskArr[i] = (runTaskArr[i] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if (tasksExecuted) {
|
||||
// console.log(
|
||||
// 'run task count',
|
||||
// ...runTaskArr,
|
||||
// 'skip count',
|
||||
// skipCount,
|
||||
// 'total task count',
|
||||
// ...this.queues.map(arr => arr.size)
|
||||
// );
|
||||
// }
|
||||
if (tasksExecuted) {
|
||||
console.log(
|
||||
'run task count',
|
||||
...runTaskArr,
|
||||
'skip count',
|
||||
skipCount,
|
||||
'total task count',
|
||||
...this.queues.map(arr => arr.size)
|
||||
);
|
||||
}
|
||||
|
||||
const hasRemainingTasks = this.queues.some(queue => !queue.isEmpty());
|
||||
|
||||
|
||||
@@ -9,19 +9,18 @@ import type {
|
||||
CellRenderProps,
|
||||
DataViewCellLifeCycle,
|
||||
} from '../../../core/property/index.js';
|
||||
import type { SingleView } from '../../../core/view-manager/single-view.js';
|
||||
import {
|
||||
TableViewAreaSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../selection';
|
||||
import type { TableProperty } from '../table-view-manager.js';
|
||||
import type { TableGroup } from './group.js';
|
||||
|
||||
export class DatabaseCellContainer extends SignalWatcher(
|
||||
import type { TableViewUILogic } from './table-view-ui-logic.js';
|
||||
export class TableViewCellContainer extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
affine-database-cell-container {
|
||||
dv-table-view-cell-container {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
@@ -30,16 +29,16 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
outline: none;
|
||||
}
|
||||
|
||||
affine-database-cell-container * {
|
||||
dv-table-view-cell-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
affine-database-cell-container uni-lit > *:first-child {
|
||||
dv-table-view-cell-container uni-lit > *:first-child {
|
||||
padding: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _cell = signal<DataViewCellLifeCycle>();
|
||||
private readonly _cell$ = signal<DataViewCellLifeCycle>();
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor column!: TableProperty;
|
||||
@@ -55,7 +54,7 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
if (this.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const selectionView = this.selectionView;
|
||||
const selectionView = this.selectionController;
|
||||
if (selectionView) {
|
||||
const selection = selectionView.selection;
|
||||
if (selection && this.isSelected(selection) && editing) {
|
||||
@@ -81,20 +80,15 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
};
|
||||
|
||||
get cell(): DataViewCellLifeCycle | undefined {
|
||||
return this._cell.value;
|
||||
return this._cell$.value;
|
||||
}
|
||||
|
||||
private get groupKey() {
|
||||
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
|
||||
}
|
||||
|
||||
private get selectionView() {
|
||||
return this.closest('affine-database-table')?.selectionController;
|
||||
}
|
||||
|
||||
get table() {
|
||||
const table = this.closest('affine-database-table');
|
||||
return table;
|
||||
private get selectionController() {
|
||||
return this.tableViewLogic.selectionController;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -134,7 +128,7 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
};
|
||||
|
||||
return renderUniLit(view, props, {
|
||||
ref: this._cell,
|
||||
ref: this._cell$,
|
||||
style: {
|
||||
display: 'contents',
|
||||
},
|
||||
@@ -152,12 +146,16 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: SingleView;
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-database-cell-container': DatabaseCellContainer;
|
||||
'dv-table-view-cell-container': TableViewCellContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type TableViewSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../selection';
|
||||
import type { DataViewTable } from '../table-view.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
|
||||
const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table';
|
||||
type JsonAreaData = string[][];
|
||||
@@ -20,9 +20,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
tableSelection: TableViewSelectionWithType,
|
||||
isCut = false
|
||||
) => {
|
||||
const table = this.host;
|
||||
|
||||
const area = getSelectedArea(tableSelection, table);
|
||||
const area = getSelectedArea(tableSelection, this.logic);
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
@@ -44,7 +42,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.props.view.rowsDelete(deleteRows);
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -79,12 +77,11 @@ export class TableClipboardController implements ReactiveController {
|
||||
private readonly _onPaste = async (_context: UIEventStateContext) => {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
const view = this.host;
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
const tableSelection = this.selectionController.selection;
|
||||
if (TableViewRowSelection.is(tableSelection)) {
|
||||
return;
|
||||
}
|
||||
@@ -97,7 +94,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
if (dataString) {
|
||||
// If internal format data exists, use it
|
||||
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
|
||||
pasteToCells(view, jsonAreaData, tableSelection);
|
||||
pasteToCells(this.logic, jsonAreaData, tableSelection);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
@@ -115,7 +112,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
|
||||
|
||||
if (rows.length > 0) {
|
||||
pasteToCells(view, rows, tableSelection);
|
||||
pasteToCells(this.logic, rows, tableSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,27 +121,29 @@ export class TableClipboardController implements ReactiveController {
|
||||
};
|
||||
|
||||
private get clipboard() {
|
||||
return this.props.clipboard;
|
||||
return this.logic.root.config.clipboard;
|
||||
}
|
||||
|
||||
private get notification() {
|
||||
return this.props.notification;
|
||||
}
|
||||
|
||||
get props() {
|
||||
return this.host.props;
|
||||
return this.logic.root.config.notification;
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.props.view.readonly$.value;
|
||||
return this.logic.view.readonly$.value;
|
||||
}
|
||||
|
||||
constructor(public host: DataViewTable) {
|
||||
host.addController(this);
|
||||
constructor(public logic: TableViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
get selectionController() {
|
||||
return this.logic.selectionController;
|
||||
}
|
||||
|
||||
copy() {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
const tableSelection = this.selectionController.selection;
|
||||
if (!tableSelection) {
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
|
||||
cut() {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
const tableSelection = this.selectionController.selection;
|
||||
if (!tableSelection) {
|
||||
return;
|
||||
}
|
||||
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.props.handleEvent('copy', _ctx => {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
this.host?.disposables.add(
|
||||
this.logic.handleEvent('copy', _ctx => {
|
||||
const tableSelection = this.selectionController.selection;
|
||||
if (!tableSelection) return false;
|
||||
|
||||
this._onCopy(tableSelection);
|
||||
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.add(
|
||||
this.props.handleEvent('cut', _ctx => {
|
||||
const tableSelection = this.host.selectionController.selection;
|
||||
this.host?.disposables.add(
|
||||
this.logic.handleEvent('cut', _ctx => {
|
||||
const tableSelection = this.selectionController.selection;
|
||||
if (!tableSelection) return false;
|
||||
|
||||
this._onCut(tableSelection);
|
||||
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.add(
|
||||
this.props.handleEvent('paste', ctx => {
|
||||
this.host?.disposables.add(
|
||||
this.logic.handleEvent('paste', ctx => {
|
||||
if (this.readonly) return false;
|
||||
|
||||
this._onPaste(ctx).catch(console.error);
|
||||
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
|
||||
|
||||
function getSelectedArea(
|
||||
selection: TableViewSelection,
|
||||
table: DataViewTable
|
||||
table: TableViewUILogic
|
||||
): SelectedArea | undefined {
|
||||
const view = table.props.view;
|
||||
const view = table.view;
|
||||
if (TableViewRowSelection.is(selection)) {
|
||||
const rows = TableViewRowSelection.rows(selection)
|
||||
.map(row => {
|
||||
@@ -283,7 +282,7 @@ function getTargetRangeFromSelection(
|
||||
}
|
||||
|
||||
function pasteToCells(
|
||||
table: DataViewTable,
|
||||
table: TableViewUILogic,
|
||||
rows: JsonAreaData,
|
||||
selection: TableViewAreaSelection
|
||||
) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as Y from 'yjs';
|
||||
|
||||
import { t } from '../../../../core/index.js';
|
||||
import type { TableViewAreaSelection } from '../../selection';
|
||||
import type { DataViewTable } from '../table-view.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
|
||||
export class DragToFillElement extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
@@ -49,12 +49,12 @@ export class DragToFillElement extends ShadowlessElement {
|
||||
}
|
||||
|
||||
export function fillSelectionWithFocusCellData(
|
||||
host: DataViewTable,
|
||||
logic: TableViewUILogic,
|
||||
selection: TableViewAreaSelection
|
||||
) {
|
||||
const { groupKey, rowsSelection, columnsSelection, focus } = selection;
|
||||
|
||||
const focusCell = host.selectionController.getCellContainer(
|
||||
const focusCell = logic.selectionController.getCellContainer(
|
||||
groupKey,
|
||||
focus.rowIndex,
|
||||
focus.columnIndex
|
||||
@@ -78,7 +78,7 @@ export function fillSelectionWithFocusCellData(
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i === focus.rowIndex) continue;
|
||||
|
||||
const cellContainer = host.selectionController.getCellContainer(
|
||||
const cellContainer = logic.selectionController.getCellContainer(
|
||||
groupKey,
|
||||
i,
|
||||
draggingColIdx
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ReactiveController } from 'lit';
|
||||
|
||||
import { startDrag } from '../../../../core/utils/drag.js';
|
||||
import { TableRowView } from '../row/row.js';
|
||||
import type { DataViewTable } from '../table-view.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
|
||||
export class TableDragController implements ReactiveController {
|
||||
dragStart = (rowView: TableRowView, evt: PointerEvent) => {
|
||||
@@ -32,8 +32,8 @@ export class TableDragController implements ReactiveController {
|
||||
onDrag: () => undefined,
|
||||
onMove: evt => {
|
||||
preview.display(evt.x - offsetLeft, evt.y - offsetTop);
|
||||
if (!this.host.contains(evt.target as Node)) {
|
||||
const callback = this.host.props.onDrag;
|
||||
if (!this.host?.contains(evt.target as Node)) {
|
||||
const callback = this.logic.root.config.onDrag;
|
||||
if (callback) {
|
||||
this.dropPreview.remove();
|
||||
return {
|
||||
@@ -66,7 +66,7 @@ export class TableDragController implements ReactiveController {
|
||||
return;
|
||||
}
|
||||
if (result.type === 'self') {
|
||||
const row = this.host.props.view.rowGetOrCreate(rowView.rowId);
|
||||
const row = this.logic.view.rowGetOrCreate(rowView.rowId);
|
||||
row.move(result.position, fromGroup, result.groupKey);
|
||||
}
|
||||
},
|
||||
@@ -88,9 +88,9 @@ export class TableDragController implements ReactiveController {
|
||||
| undefined => {
|
||||
const y = evt.y;
|
||||
const tableRect = this.host
|
||||
.querySelector('affine-data-view-table-group')
|
||||
?.querySelector('affine-data-view-table-group')
|
||||
?.getBoundingClientRect();
|
||||
const rows = this.host.querySelectorAll('data-view-table-row');
|
||||
const rows = this.host?.querySelectorAll('data-view-table-row');
|
||||
if (!rows || !tableRect || y < tableRect.top) {
|
||||
return;
|
||||
}
|
||||
@@ -124,21 +124,23 @@ export class TableDragController implements ReactiveController {
|
||||
return position;
|
||||
};
|
||||
|
||||
constructor(private readonly host: DataViewTable) {
|
||||
this.host.addController(this);
|
||||
constructor(private readonly logic: TableViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
if (this.host.props.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
this.host.disposables.add(
|
||||
this.host.props.handleEvent('dragStart', context => {
|
||||
this.host?.disposables.add(
|
||||
this.logic.handleEvent('dragStart', context => {
|
||||
if (this.logic.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const event = context.get('pointerState').raw;
|
||||
const target = event.target;
|
||||
if (
|
||||
target instanceof Element &&
|
||||
this.host.contains(target) &&
|
||||
this.host?.contains(target) &&
|
||||
target.closest('.data-view-table-view-drag-handler')
|
||||
) {
|
||||
event.preventDefault();
|
||||
@@ -158,10 +160,9 @@ export class TableDragController implements ReactiveController {
|
||||
const createDragPreview = (row: TableRowView, x: number, y: number) => {
|
||||
const div = document.createElement('div');
|
||||
const cloneRow = new TableRowView();
|
||||
cloneRow.view = row.view;
|
||||
cloneRow.rowIndex = row.rowIndex;
|
||||
cloneRow.rowId = row.rowId;
|
||||
cloneRow.dataViewEle = row.dataViewEle;
|
||||
cloneRow.tableViewLogic = row.tableViewLogic;
|
||||
div.append(cloneRow);
|
||||
div.className = 'with-data-view-css-variable';
|
||||
div.style.width = `${row.getBoundingClientRect().width}px`;
|
||||
|
||||
@@ -3,20 +3,22 @@ import type { ReactiveController } from 'lit';
|
||||
|
||||
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
|
||||
import { popRowMenu } from '../menu.js';
|
||||
import type { DataViewTable } from '../table-view.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic';
|
||||
|
||||
export class TableHotkeysController implements ReactiveController {
|
||||
get selectionController() {
|
||||
return this.host.selectionController;
|
||||
return this.logic.selectionController;
|
||||
}
|
||||
|
||||
constructor(private readonly host: DataViewTable) {
|
||||
this.host.addController(this);
|
||||
constructor(private readonly logic: TableViewUILogic) {}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.bindHotkey({
|
||||
this.host?.disposables.add(
|
||||
this.logic.bindHotkey({
|
||||
Backspace: () => {
|
||||
const selection = this.selectionController.selection;
|
||||
if (!selection) {
|
||||
@@ -25,7 +27,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
if (TableViewRowSelection.is(selection)) {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.host.props.view.rowsDelete(rows);
|
||||
this.logic.view.rowsDelete(rows);
|
||||
return;
|
||||
}
|
||||
const {
|
||||
@@ -334,14 +336,14 @@ export class TableHotkeysController implements ReactiveController {
|
||||
context.get('keyboardState').raw.preventDefault();
|
||||
this.selectionController.selection = TableViewRowSelection.create({
|
||||
rows:
|
||||
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap(
|
||||
this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
|
||||
group =>
|
||||
group?.rows.map(row => ({
|
||||
groupKey: group.key,
|
||||
id: row.rowId,
|
||||
})) ?? []
|
||||
) ??
|
||||
this.host.props.view.rows$.value.map(row => ({
|
||||
this.logic.view.rows$.value.map(row => ({
|
||||
groupKey: undefined,
|
||||
id: row.rowId,
|
||||
})),
|
||||
@@ -377,7 +379,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
rows: [row],
|
||||
});
|
||||
popRowMenu(
|
||||
this.host.props.dataViewEle,
|
||||
this.logic,
|
||||
popupTargetFromElement(cell),
|
||||
this.selectionController
|
||||
);
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
type TableViewSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../selection';
|
||||
import type { DatabaseCellContainer } from '../cell.js';
|
||||
import type { TableViewCellContainer } from '../cell.js';
|
||||
import type { TableGroup } from '../group.js';
|
||||
import type { TableRowView } from '../row/row.js';
|
||||
import type { DataViewTable } from '../table-view.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
import {
|
||||
DragToFillElement,
|
||||
fillSelectionWithFocusCellData,
|
||||
@@ -87,41 +87,44 @@ export class TableSelectionController implements ReactiveController {
|
||||
);
|
||||
const cell = container?.cell;
|
||||
const isEditing = cell ? cell.beforeEnterEditMode() : true;
|
||||
this.host.props.setSelection({
|
||||
this.logic.setSelection({
|
||||
...selection,
|
||||
isEditing,
|
||||
});
|
||||
} else {
|
||||
this.host.props.setSelection(selection);
|
||||
this.logic.setSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
get tableContainer() {
|
||||
return this.host.querySelector('.affine-database-table-container');
|
||||
return this.logic.tableContainer$.value;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.host.props.view;
|
||||
return this.logic.view;
|
||||
}
|
||||
|
||||
get viewData() {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
constructor(public host: DataViewTable) {
|
||||
host.addController(this);
|
||||
constructor(public logic: TableViewUILogic) {
|
||||
this.__selectionElement = new SelectionElement();
|
||||
this.__selectionElement.controller = this;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.logic.ui$.value;
|
||||
}
|
||||
|
||||
private clearSelection() {
|
||||
this.host.props.setSelection();
|
||||
this.logic.setSelection();
|
||||
}
|
||||
|
||||
private handleDragEvent() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.handleEvent('dragStart', context => {
|
||||
if (this.host.props.view.readonly$.value) {
|
||||
this.host?.disposables.add(
|
||||
this.logic.handleEvent('dragStart', context => {
|
||||
if (this.logic.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const event = context.get('pointerState').raw;
|
||||
@@ -152,8 +155,8 @@ export class TableSelectionController implements ReactiveController {
|
||||
}
|
||||
|
||||
private handleSelectionChange() {
|
||||
this.host.disposables.add(
|
||||
this.host.props.selection$.subscribe(tableSelection => {
|
||||
this.host?.disposables.add(
|
||||
this.logic.selection$.subscribe(tableSelection => {
|
||||
if (!this.isValidSelection(tableSelection)) {
|
||||
this.selection = undefined;
|
||||
return;
|
||||
@@ -238,7 +241,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
|
||||
: this.view.rows$.value;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.host.props.view.properties$.value.findIndex(
|
||||
const index = this.logic.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
this.selection = TableViewAreaSelection.create({
|
||||
@@ -254,14 +257,14 @@ export class TableSelectionController implements ReactiveController {
|
||||
|
||||
private resolveDragStartTarget(
|
||||
target: HTMLElement
|
||||
): [cell: DatabaseCellContainer | null, fillValues: boolean] {
|
||||
let cell: DatabaseCellContainer | null;
|
||||
): [cell: TableViewCellContainer | null, fillValues: boolean] {
|
||||
let cell: TableViewCellContainer | null;
|
||||
const fillValues = !!target.dataset.dragToFill;
|
||||
if (fillValues) {
|
||||
const focusCellContainer = this.getFocusCellContainer();
|
||||
cell = focusCellContainer ?? null;
|
||||
} else {
|
||||
cell = target.closest('affine-database-cell-container');
|
||||
cell = target.closest('dv-table-view-cell-container');
|
||||
}
|
||||
return [cell, fillValues];
|
||||
}
|
||||
@@ -295,7 +298,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
const rows = this.rows(groupKey);
|
||||
const cells = rows
|
||||
?.item(0)
|
||||
.querySelectorAll('affine-database-cell-container');
|
||||
.querySelectorAll('dv-table-view-cell-container');
|
||||
|
||||
return (x1: number, x2: number, y1: number, y2: number) => {
|
||||
const rowOffsets: number[] = Array.from(rows ?? []).map(
|
||||
@@ -394,7 +397,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
?.querySelectorAll('data-view-table-row') ?? []
|
||||
);
|
||||
const cells = Array.from(
|
||||
row?.querySelectorAll('affine-database-cell-container') ?? []
|
||||
row?.querySelectorAll('dv-table-view-cell-container') ?? []
|
||||
);
|
||||
if (!row || !rows || !cells) {
|
||||
return;
|
||||
@@ -432,7 +435,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
}
|
||||
}
|
||||
rows[rowIndex]
|
||||
?.querySelectorAll('affine-database-cell-container')
|
||||
?.querySelectorAll('dv-table-view-cell-container')
|
||||
?.item(columnIndex)
|
||||
?.selectCurrentCell(false);
|
||||
}
|
||||
@@ -441,10 +444,10 @@ export class TableSelectionController implements ReactiveController {
|
||||
groupKey: string | undefined,
|
||||
rowIndex: number,
|
||||
columnIndex: number
|
||||
): DatabaseCellContainer | undefined {
|
||||
): TableViewCellContainer | undefined {
|
||||
const row = this.rows(groupKey)?.item(rowIndex);
|
||||
return row
|
||||
?.querySelectorAll('affine-database-cell-container')
|
||||
?.querySelectorAll('dv-table-view-cell-container')
|
||||
.item(columnIndex);
|
||||
}
|
||||
|
||||
@@ -479,7 +482,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
if (!topRow || !bottomRow) {
|
||||
return;
|
||||
}
|
||||
const topCells = topRow.querySelectorAll('affine-database-cell-container');
|
||||
const topCells = topRow.querySelectorAll('dv-table-view-cell-container');
|
||||
const leftCell = topCells.item(left);
|
||||
const rightCell = topCells.item(right);
|
||||
if (!leftCell || !rightCell) {
|
||||
@@ -752,7 +755,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
const max =
|
||||
(this.rows(newSelection.groupKey)
|
||||
?.item(0)
|
||||
.querySelectorAll('affine-database-cell-container').length ?? 0) - 1;
|
||||
.querySelectorAll('dv-table-view-cell-container').length ?? 0) - 1;
|
||||
newSelection.columnsSelection.end = Math.min(
|
||||
max,
|
||||
newSelection.columnsSelection.end + 1
|
||||
@@ -810,7 +813,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
|
||||
startDrag(
|
||||
evt: PointerEvent,
|
||||
cell: DatabaseCellContainer,
|
||||
cell: TableViewCellContainer,
|
||||
fillValues?: boolean
|
||||
) {
|
||||
const groupKey = cell.closest<TableGroup>('affine-data-view-table-group')
|
||||
@@ -882,7 +885,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
if (fillValues && this.selection) {
|
||||
this.__dragToFillElement.dragging = false;
|
||||
fillSelectionWithFocusCellData(
|
||||
this.host,
|
||||
this.logic,
|
||||
TableViewAreaSelection.create({
|
||||
groupKey: groupKey,
|
||||
rowsSelection: selection.row,
|
||||
@@ -997,7 +1000,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
|
||||
selectionRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
|
||||
|
||||
get selection$() {
|
||||
return this.controller.host.props.selection$;
|
||||
return this.controller.logic.selection$;
|
||||
}
|
||||
|
||||
clearAreaStyle() {
|
||||
@@ -1049,7 +1052,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
|
||||
this.cancelSelectionUpdate();
|
||||
if (
|
||||
selection?.selectionType === 'area' &&
|
||||
!this.controller.host.props.view.readonly$.value
|
||||
!this.controller.logic.view.readonly$.value
|
||||
) {
|
||||
this.updateAreaSelectionStyle(
|
||||
selection.groupKey,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseCellContainer } from './cell.js';
|
||||
import { TableViewCellContainer } from './cell.js';
|
||||
import { DragToFillElement } from './controller/drag-to-fill.js';
|
||||
import { SelectionElement } from './controller/selection.js';
|
||||
import { TableGroup } from './group.js';
|
||||
@@ -9,15 +9,12 @@ import { DatabaseNumberFormatBar } from './header/number-format-bar.js';
|
||||
import { TableVerticalIndicator } from './header/vertical-indicator.js';
|
||||
import { TableRowView } from './row/row.js';
|
||||
import { RowSelectCheckbox } from './row/row-select-checkbox.js';
|
||||
import { DataViewTable } from './table-view.js';
|
||||
import { TableViewUI } from './table-view-ui-logic.js';
|
||||
|
||||
export function pcEffects() {
|
||||
customElements.define('affine-database-table', DataViewTable);
|
||||
customElements.define('dv-table-view-ui', TableViewUI);
|
||||
customElements.define('affine-data-view-table-group', TableGroup);
|
||||
customElements.define(
|
||||
'affine-database-cell-container',
|
||||
DatabaseCellContainer
|
||||
);
|
||||
customElements.define('dv-table-view-cell-container', TableViewCellContainer);
|
||||
customElements.define('affine-database-column-header', DatabaseColumnHeader);
|
||||
customElements.define(
|
||||
'affine-data-view-column-preview',
|
||||
|
||||
@@ -12,7 +12,6 @@ import { css, html, unsafeCSS } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { GroupTitle } from '../../../core/group-by/group-title.js';
|
||||
import type { Group } from '../../../core/group-by/trait.js';
|
||||
import type { Row } from '../../../core/index.js';
|
||||
@@ -21,10 +20,9 @@ import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
|
||||
import { linearMove } from '../../../core/utils/wc-dnd/utils/linear-move.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import { TableViewAreaSelection } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { DataViewColumnPreview } from './header/column-renderer.js';
|
||||
import { getVerticalIndicator } from './header/vertical-indicator.js';
|
||||
import type { DataViewTable } from './table-view.js';
|
||||
import type { TableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
const styles = css`
|
||||
affine-data-view-table-group:hover .group-header-op {
|
||||
@@ -71,7 +69,7 @@ export class TableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
const selectionController = this.tableViewLogic.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
@@ -90,7 +88,7 @@ export class TableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
const selectionController = this.tableViewLogic.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
@@ -152,9 +150,6 @@ export class TableGroup extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor group: Group | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
|
||||
dndContext = createDndContext({
|
||||
activators: defaultActivators,
|
||||
container: this,
|
||||
@@ -187,6 +182,7 @@ export class TableGroup extends SignalWatcher(
|
||||
preview.column = column;
|
||||
preview.group = this.group;
|
||||
preview.container = this;
|
||||
preview.tableViewLogic = this.tableViewLogic;
|
||||
preview.style.position = 'absolute';
|
||||
preview.style.zIndex = '999';
|
||||
const scale = this.dndContext.scale$.value;
|
||||
@@ -246,7 +242,7 @@ export class TableGroup extends SignalWatcher(
|
||||
return html`
|
||||
<affine-database-column-header
|
||||
.renderGroupHeader="${this.renderGroupHeader}"
|
||||
.tableViewManager="${this.view}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
></affine-database-column-header>
|
||||
<div class="affine-database-block-rows">
|
||||
${repeat(
|
||||
@@ -256,8 +252,7 @@ export class TableGroup extends SignalWatcher(
|
||||
return html` <data-view-table-row
|
||||
data-row-index="${idx}"
|
||||
data-row-id="${row.rowId}"
|
||||
.dataViewEle="${this.dataViewEle}"
|
||||
.view="${this.view}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.rowId="${row.rowId}"
|
||||
.rowIndex="${idx}"
|
||||
></data-view-table-row>`;
|
||||
@@ -278,7 +273,10 @@ export class TableGroup extends SignalWatcher(
|
||||
${PlusIcon()}<span style="font-size: 12px">New Record</span>
|
||||
</div>
|
||||
</div>`}
|
||||
<affine-database-column-stats .view="${this.view}" .group="${this.group}">
|
||||
<affine-database-column-stats
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.group="${this.group}"
|
||||
>
|
||||
</affine-database-column-stats>
|
||||
`;
|
||||
}
|
||||
@@ -292,14 +290,15 @@ export class TableGroup extends SignalWatcher(
|
||||
return this.renderRows(this.rows);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@query('.affine-database-block-rows')
|
||||
accessor rowsContainer: HTMLElement | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor viewEle!: DataViewTable;
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -9,8 +9,10 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { TableSingleView } from '../../table-view-manager.js';
|
||||
import { cellDivider } from '../../styles.js';
|
||||
import type { TableGroup } from '../group.js';
|
||||
import { tableStyle } from '../table-view-style';
|
||||
import { type TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
import { styles } from './styles.js';
|
||||
|
||||
export class DatabaseColumnHeader extends SignalWatcher(
|
||||
@@ -31,7 +33,6 @@ export class DatabaseColumnHeader extends SignalWatcher(
|
||||
editLastColumnTitle = () => {
|
||||
const columns = this.querySelectorAll('affine-database-header-column');
|
||||
const column = columns.item(columns.length - 1);
|
||||
column.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
column.editTitle();
|
||||
};
|
||||
|
||||
@@ -88,7 +89,7 @@ export class DatabaseColumnHeader extends SignalWatcher(
|
||||
<div class="affine-database-column-header database-row">
|
||||
${this.readonly
|
||||
? nothing
|
||||
: html`<div class="data-view-table-left-bar"></div>`}
|
||||
: html`<div class="${tableStyle.leftToolBarStyle}"></div>`}
|
||||
${repeat(
|
||||
this.tableViewManager.properties$.value,
|
||||
column => column.id,
|
||||
@@ -104,9 +105,9 @@ export class DatabaseColumnHeader extends SignalWatcher(
|
||||
data-column-index="${index}"
|
||||
class="affine-database-column database-cell"
|
||||
.column="${column}"
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
></affine-database-header-column>
|
||||
<div class="cell-divider" style="height: auto;"></div>
|
||||
<div class="${cellDivider}" style="height: auto;"></div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
@@ -128,7 +129,11 @@ export class DatabaseColumnHeader extends SignalWatcher(
|
||||
accessor scaleDiv!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewManager!: TableSingleView;
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
|
||||
get tableViewManager() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
TableProperty,
|
||||
TableSingleView,
|
||||
} from '../../table-view-manager.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
|
||||
export class DataViewColumnPreview extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
`;
|
||||
|
||||
get tableViewManager(): TableSingleView {
|
||||
return this.column.view as TableSingleView;
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
|
||||
private renderGroup(rows: Row[]) {
|
||||
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
)};box-shadow: var(--affine-shadow-2);"
|
||||
>
|
||||
<affine-database-header-column
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.column="${this.column}"
|
||||
></affine-database-header-column>
|
||||
${repeat(rows, (id, index) => {
|
||||
const height = this.container.querySelector(
|
||||
`affine-database-cell-container[data-row-id="${id}"]`
|
||||
`dv-table-view-cell-container[data-row-id="${id}"]`
|
||||
)?.clientHeight;
|
||||
const style = styleMap({
|
||||
height: height + 'px',
|
||||
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
)}"
|
||||
>
|
||||
<div style="${style}">
|
||||
<affine-database-cell-container
|
||||
<dv-table-view-cell-container
|
||||
.column="${this.column}"
|
||||
.view="${this.tableViewManager}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.rowId="${id}"
|
||||
.columnId="${this.column.id}"
|
||||
.rowIndex="${index}"
|
||||
.columnIndex="${columnIndex}"
|
||||
></affine-database-cell-container>
|
||||
></dv-table-view-cell-container>
|
||||
</div>
|
||||
</div>`;
|
||||
})}
|
||||
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group: Group | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -45,10 +45,8 @@ import {
|
||||
ShowQuickSettingBarKey,
|
||||
} from '../../../../widget-presets/quick-setting-bar/context.js';
|
||||
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js';
|
||||
import type {
|
||||
TableProperty,
|
||||
TableSingleView,
|
||||
} from '../../table-view-manager.js';
|
||||
import type { TableProperty } from '../../table-view-manager.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
import {
|
||||
getTableGroupRect,
|
||||
getVerticalIndicator,
|
||||
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
|
||||
|
||||
const sortUtils = createSortUtils(
|
||||
sortTrait,
|
||||
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {})
|
||||
this.tableViewLogic.eventTrace
|
||||
);
|
||||
const sortList = sortUtils.sortList$.value;
|
||||
const existingIndex = sortList.findIndex(
|
||||
@@ -398,10 +396,10 @@ export class DatabaseHeaderColumn extends SignalWatcher(
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const table = this.closest('affine-database-table');
|
||||
const table = this.closest('dv-table-view-ui');
|
||||
if (table) {
|
||||
this.disposables.add(
|
||||
table.props.handleEvent('dragStart', context => {
|
||||
table.logic.handleEvent('dragStart', context => {
|
||||
if (this.tableViewManager.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
@@ -481,7 +479,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
|
||||
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewManager!: TableSingleView;
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
|
||||
get tableViewManager() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
function numberFormatConfig(column: Property): MenuConfig {
|
||||
|
||||
@@ -12,19 +12,19 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import { TableViewRowSelection } from '../selection';
|
||||
import type { TableSelectionController } from './controller/selection.js';
|
||||
import type { TableViewUILogic } from './table-view-ui-logic.js';
|
||||
|
||||
export const openDetail = (
|
||||
dataViewEle: DataViewRenderer,
|
||||
tableViewLogic: TableViewUILogic,
|
||||
rowId: string,
|
||||
selection: TableSelectionController
|
||||
) => {
|
||||
const old = selection.selection;
|
||||
selection.selection = undefined;
|
||||
dataViewEle.openDetailPanel({
|
||||
view: selection.host.props.view,
|
||||
tableViewLogic.root.openDetailPanel({
|
||||
view: selection.logic.view,
|
||||
rowId: rowId,
|
||||
onClose: () => {
|
||||
selection.selection = old;
|
||||
@@ -33,7 +33,7 @@ export const openDetail = (
|
||||
};
|
||||
|
||||
export const popRowMenu = (
|
||||
dataViewEle: DataViewRenderer,
|
||||
tableViewLogic: TableViewUILogic,
|
||||
ele: PopupTarget,
|
||||
selectionController: TableSelectionController
|
||||
) => {
|
||||
@@ -55,7 +55,7 @@ export const popRowMenu = (
|
||||
${CopyIcon()}
|
||||
</div>`,
|
||||
select: () => {
|
||||
selectionController.host.clipboardController.copy();
|
||||
selectionController.logic.clipboardController.copy();
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -85,7 +85,7 @@ export const popRowMenu = (
|
||||
name: 'Expand Row',
|
||||
prefix: ExpandFullIcon(),
|
||||
select: () => {
|
||||
openDetail(dataViewEle, row.id, selectionController);
|
||||
openDetail(tableViewLogic, row.id, selectionController);
|
||||
},
|
||||
}),
|
||||
menu.group({
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { CheckBoxCheckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import {
|
||||
TableViewRowSelection,
|
||||
type TableViewSelectionWithType,
|
||||
} from '../../selection';
|
||||
import { TableViewRowSelection } from '../../selection';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic';
|
||||
|
||||
export class RowSelectCheckbox extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -42,10 +40,10 @@ export class RowSelectCheckbox extends SignalWatcher(
|
||||
accessor rowId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor selection!: ReadonlySignal<TableViewSelectionWithType | undefined>;
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
|
||||
isSelected$ = computed(() => {
|
||||
const selection = this.selection.value;
|
||||
const selection = this.tableViewLogic.selection$.value;
|
||||
if (!selection || selection.selectionType !== 'row') {
|
||||
return false;
|
||||
}
|
||||
@@ -58,7 +56,7 @@ export class RowSelectCheckbox extends SignalWatcher(
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.disposables.addFromEvent(this, 'click', () => {
|
||||
this.closest('affine-database-table')?.selectionController.toggleRow(
|
||||
this.tableViewLogic.selectionController.toggleRow(
|
||||
this.rowId,
|
||||
this.groupKey
|
||||
);
|
||||
|
||||
@@ -9,14 +9,14 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../../core/data-view.js';
|
||||
import {
|
||||
TableViewRowSelection,
|
||||
type TableViewSelection,
|
||||
} from '../../selection';
|
||||
import type { TableSingleView } from '../../table-view-manager.js';
|
||||
import { cellDivider } from '../../styles';
|
||||
import type { TableGroup } from '../group.js';
|
||||
import { openDetail, popRowMenu } from '../menu.js';
|
||||
import type { TableViewUILogic } from '../table-view-ui-logic.js';
|
||||
|
||||
export class TableRowView extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -138,7 +138,7 @@ export class TableRowView extends SignalWatcher(
|
||||
}
|
||||
e.preventDefault();
|
||||
const ele = e.target as HTMLElement;
|
||||
const cell = ele.closest('affine-database-cell-container');
|
||||
const cell = ele.closest('dv-table-view-cell-container');
|
||||
const row = { id: this.rowId, groupKey: this.groupKey };
|
||||
if (!TableViewRowSelection.includes(selection.selection, row)) {
|
||||
selection.selection = TableViewRowSelection.create({
|
||||
@@ -150,7 +150,7 @@ export class TableRowView extends SignalWatcher(
|
||||
(e.target as HTMLElement).closest('.database-cell') ?? // for last add btn cell
|
||||
(e.target as HTMLElement);
|
||||
|
||||
popRowMenu(this.dataViewEle, popupTargetFromElement(target), selection);
|
||||
popRowMenu(this.tableViewLogic, popupTargetFromElement(target), selection);
|
||||
};
|
||||
|
||||
setSelection = (selection?: TableViewSelection) => {
|
||||
@@ -164,7 +164,7 @@ export class TableRowView extends SignalWatcher(
|
||||
}
|
||||
|
||||
get selectionController() {
|
||||
return this.closest('affine-database-table')?.selectionController;
|
||||
return this.tableViewLogic.selectionController;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -193,7 +193,7 @@ export class TableRowView extends SignalWatcher(
|
||||
></div>
|
||||
</div>
|
||||
<row-select-checkbox
|
||||
.selection="${this.dataViewEle.config.selection$}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.rowId="${this.rowId}"
|
||||
.groupKey="${this.groupKey}"
|
||||
></row-select-checkbox>
|
||||
@@ -212,7 +212,11 @@ export class TableRowView extends SignalWatcher(
|
||||
rows: [{ id: this.rowId, groupKey: this.groupKey }],
|
||||
})
|
||||
);
|
||||
openDetail(this.dataViewEle, this.rowId, this.selectionController);
|
||||
openDetail(
|
||||
this.tableViewLogic,
|
||||
this.rowId,
|
||||
this.selectionController
|
||||
);
|
||||
};
|
||||
const openMenu = (e: MouseEvent) => {
|
||||
if (!this.selectionController) {
|
||||
@@ -234,20 +238,20 @@ export class TableRowView extends SignalWatcher(
|
||||
);
|
||||
}
|
||||
popRowMenu(
|
||||
this.dataViewEle,
|
||||
this.tableViewLogic,
|
||||
popupTargetFromElement(ele),
|
||||
this.selectionController
|
||||
);
|
||||
};
|
||||
return html`
|
||||
<div style="display: flex;">
|
||||
<affine-database-cell-container
|
||||
<dv-table-view-cell-container
|
||||
class="database-cell"
|
||||
style=${styleMap({
|
||||
width: `${column.width$.value}px`,
|
||||
border: i === 0 ? 'none' : undefined,
|
||||
})}
|
||||
.view="${view}"
|
||||
.tableViewLogic="${this.tableViewLogic}"
|
||||
.column="${column}"
|
||||
.rowId="${this.rowId}"
|
||||
data-row-id="${this.rowId}"
|
||||
@@ -258,8 +262,8 @@ export class TableRowView extends SignalWatcher(
|
||||
.columnIndex="${i}"
|
||||
data-column-index="${i}"
|
||||
>
|
||||
</affine-database-cell-container>
|
||||
<div class="cell-divider"></div>
|
||||
</dv-table-view-cell-container>
|
||||
<div class="${cellDivider}"></div>
|
||||
</div>
|
||||
${!column.readonly$.value &&
|
||||
column.view.mainProperties$.value.titleColumn === column.id
|
||||
@@ -282,7 +286,7 @@ export class TableRowView extends SignalWatcher(
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
accessor tableViewLogic!: TableViewUILogic;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowId!: string;
|
||||
@@ -290,8 +294,9 @@ export class TableRowView extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
get view() {
|
||||
return this.tableViewLogic.view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts';
|
||||
|
||||
export const tableViewStyle = css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'& *': {
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
});
|
||||
export const tableWrapperStyle = css({
|
||||
overflowY: 'auto',
|
||||
});
|
||||
export const tableScrollContainerStyle = css({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
paddingBottom: '4px',
|
||||
zIndex: 1,
|
||||
overflowX: 'scroll',
|
||||
overflowY: 'hidden',
|
||||
|
||||
'&:hover': {
|
||||
paddingBottom: '0px',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar': {
|
||||
WebkitAppearance: 'none',
|
||||
display: 'block',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar:horizontal': {
|
||||
height: '4px',
|
||||
},
|
||||
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
borderRadius: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
'&:hover::-webkit-scrollbar:horizontal': {
|
||||
height: '8px',
|
||||
},
|
||||
|
||||
'&:hover::-webkit-scrollbar-thumb': {
|
||||
borderRadius: '16px',
|
||||
backgroundColor: 'var(--affine-black-30)',
|
||||
},
|
||||
|
||||
'&:hover::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
|
||||
'.affine-database-table-container': {
|
||||
position: 'relative',
|
||||
width: 'fit-content',
|
||||
minWidth: '100%',
|
||||
},
|
||||
});
|
||||
export const tableGroupsContainerStyle = css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
});
|
||||
export const addGroupStyle = css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '6px 12px 6px 8px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
position: 'sticky',
|
||||
left: `${LEFT_TOOL_BAR_WIDTH}px`,
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
export const addGroupIconStyle = css({
|
||||
display: 'flex',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
|
||||
'& svg': {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
fill: 'var(--affine-icon-color)',
|
||||
},
|
||||
});
|
||||
const cellDividerStyle = css({
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
backgroundColor: 'var(--affine-border-color)',
|
||||
});
|
||||
const leftToolBarStyle = css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'sticky',
|
||||
zIndex: 1,
|
||||
left: 0,
|
||||
width: `${LEFT_TOOL_BAR_WIDTH}px`,
|
||||
flexShrink: 0,
|
||||
});
|
||||
export const tableStyle = {
|
||||
leftToolBarStyle,
|
||||
cellDividerStyle,
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { GroupTrait } from '../../../core/group-by/trait.js';
|
||||
import {
|
||||
createUniComponentFromWebComponent,
|
||||
renderUniLit,
|
||||
} from '../../../core/index.js';
|
||||
import {
|
||||
DataViewUIBase,
|
||||
DataViewUILogicBase,
|
||||
} from '../../../core/view/data-view-base.js';
|
||||
import type { TableViewSelectionWithType } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { TableClipboardController } from './controller/clipboard.js';
|
||||
import { TableDragController } from './controller/drag.js';
|
||||
import { TableHotkeysController } from './controller/hotkeys.js';
|
||||
import { TableSelectionController } from './controller/selection.js';
|
||||
import {
|
||||
addGroupIconStyle,
|
||||
addGroupStyle,
|
||||
tableGroupsContainerStyle,
|
||||
tableScrollContainerStyle,
|
||||
tableViewStyle,
|
||||
tableWrapperStyle,
|
||||
} from './table-view-style';
|
||||
|
||||
export class TableViewUILogic extends DataViewUILogicBase<
|
||||
TableSingleView,
|
||||
TableViewSelectionWithType
|
||||
> {
|
||||
ui$ = signal<TableViewUI>();
|
||||
scrollContainer$ = signal<HTMLDivElement>();
|
||||
tableContainer$ = signal<HTMLDivElement>();
|
||||
|
||||
clipboardController = new TableClipboardController(this);
|
||||
dragController = new TableDragController(this);
|
||||
hotkeysController = new TableHotkeysController(this);
|
||||
selectionController = new TableSelectionController(this);
|
||||
|
||||
private get readonly() {
|
||||
return this.view.readonly$.value;
|
||||
}
|
||||
|
||||
clearSelection = () => {
|
||||
this.selectionController.clear();
|
||||
};
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
const rowId = this.view.rowAdd(position);
|
||||
if (rowId) {
|
||||
this.root.openDetailPanel({
|
||||
view: this.view,
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
return rowId;
|
||||
};
|
||||
|
||||
focusFirstCell = () => {
|
||||
this.selectionController.focusFirstCell();
|
||||
};
|
||||
|
||||
showIndicator = (evt: MouseEvent) => {
|
||||
return this.dragController.showIndicator(evt) != null;
|
||||
};
|
||||
|
||||
hideIndicator = () => {
|
||||
this.dragController.dropPreview.remove();
|
||||
};
|
||||
|
||||
moveTo = (id: string, evt: MouseEvent) => {
|
||||
const result = this.dragController.getInsertPosition(evt);
|
||||
if (result) {
|
||||
const row = this.view.rowGetOrCreate(id);
|
||||
row.move(result.position, undefined, result.groupKey);
|
||||
}
|
||||
};
|
||||
|
||||
onWheel = (event: WheelEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const ele = event.currentTarget;
|
||||
if (ele instanceof HTMLElement) {
|
||||
if (ele.scrollWidth === ele.clientWidth) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
renderAddGroup = (groupHelper: GroupTrait) => {
|
||||
const addGroup = groupHelper.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = groupHelper.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.view.manager.dataSource,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div style="display:flex;">
|
||||
<div class="${addGroupStyle}" @click="${add}">
|
||||
<div class="${addGroupIconStyle}">${AddCursorIcon()}</div>
|
||||
<div>New Group</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
renderer = createUniComponentFromWebComponent(TableViewUI);
|
||||
}
|
||||
|
||||
export class TableViewUI extends DataViewUIBase<TableViewUILogic> {
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.logic.ui$.value = this;
|
||||
this.logic.clipboardController.hostConnected();
|
||||
this.logic.dragController.hostConnected();
|
||||
this.logic.hotkeysController.hostConnected();
|
||||
this.logic.selectionController.hostConnected();
|
||||
this.classList.add('affine-database-table', tableViewStyle);
|
||||
this.dataset['testid'] = 'dv-table-view';
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
const groups = this.logic.view.groupTrait.groupsDataList$.value;
|
||||
if (groups) {
|
||||
return html`
|
||||
<div class="${tableGroupsContainerStyle}">
|
||||
${repeat(
|
||||
groups,
|
||||
v => v.key,
|
||||
group => {
|
||||
return html` <affine-data-view-table-group
|
||||
data-group-key="${group.key}"
|
||||
.tableViewLogic="${this.logic}"
|
||||
.group="${group}"
|
||||
></affine-data-view-table-group>`;
|
||||
}
|
||||
)}
|
||||
${this.logic.renderAddGroup(this.logic.view.groupTrait)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html` <affine-data-view-table-group
|
||||
.tableViewLogic="${this.logic}"
|
||||
></affine-data-view-table-group>`;
|
||||
}
|
||||
|
||||
override render(): TemplateResult {
|
||||
const vPadding = this.logic.root.config.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
});
|
||||
const containerStyle = styleMap({
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${this.logic.headerWidget
|
||||
? renderUniLit(this.logic.headerWidget, {
|
||||
dataViewLogic: this.logic,
|
||||
})
|
||||
: ''}
|
||||
<div class="${tableWrapperStyle}" style="${wrapperStyle}">
|
||||
<div
|
||||
${ref(this.logic.scrollContainer$)}
|
||||
class="${tableScrollContainerStyle}"
|
||||
@wheel="${this.logic.onWheel}"
|
||||
>
|
||||
<div
|
||||
${ref(this.logic.tableContainer$)}
|
||||
class="affine-database-table-container"
|
||||
style="${containerStyle}"
|
||||
>
|
||||
${this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dv-table-view-ui': TableViewUI;
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { AddCursorIcon } from '@blocksuite/icons/lit';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, unsafeCSS } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { GroupTrait } from '../../../core/group-by/trait.js';
|
||||
import type { DataViewInstance } from '../../../core/index.js';
|
||||
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
|
||||
import { DataViewBase } from '../../../core/view/data-view-base.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import type { TableViewSelectionWithType } from '../selection';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { TableClipboardController } from './controller/clipboard.js';
|
||||
import { TableDragController } from './controller/drag.js';
|
||||
import { TableHotkeysController } from './controller/hotkeys.js';
|
||||
import { TableSelectionController } from './controller/selection.js';
|
||||
|
||||
const styles = css`
|
||||
affine-database-table {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
affine-database-table * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.affine-database-table {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.affine-database-block-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin: 2px 0 2px;
|
||||
}
|
||||
|
||||
.affine-database-block-table {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
z-index: 1;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.affine-database-block-table:hover {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.affine-database-block-table::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.affine-database-block-table::-webkit-scrollbar:horizontal {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.affine-database-block-table::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.affine-database-block-table:hover::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.affine-database-block-table:hover::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: var(--affine-black-30);
|
||||
}
|
||||
|
||||
.affine-database-block-table:hover::-webkit-scrollbar-track {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.affine-database-table-container {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.affine-database-block-tag-circle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.affine-database-block-tag {
|
||||
display: inline-flex;
|
||||
border-radius: 11px;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cell-divider {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: ${unsafeCSS(cssVarV2.layer.insideBorder.border)};
|
||||
}
|
||||
|
||||
.data-view-table-left-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
width: ${LEFT_TOOL_BAR_WIDTH}px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.affine-database-block-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
export class DataViewTable extends DataViewBase<
|
||||
TableSingleView,
|
||||
TableViewSelectionWithType
|
||||
> {
|
||||
static override styles = styles;
|
||||
|
||||
clipboardController = new TableClipboardController(this);
|
||||
|
||||
dragController = new TableDragController(this);
|
||||
|
||||
hotkeysController = new TableHotkeysController(this);
|
||||
|
||||
onWheel = (event: WheelEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const ele = event.currentTarget;
|
||||
if (ele instanceof HTMLElement) {
|
||||
if (ele.scrollWidth === ele.clientWidth) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
renderAddGroup = (groupHelper: GroupTrait) => {
|
||||
const addGroup = groupHelper.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
}
|
||||
const add = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
onComplete: text => {
|
||||
const column = groupHelper.property$.value;
|
||||
if (column) {
|
||||
column.dataUpdate(
|
||||
() =>
|
||||
addGroup({
|
||||
text,
|
||||
oldData: column.data$.value,
|
||||
dataSource: this.props.view.manager.dataSource,
|
||||
}) as never
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
return html` <div style="display:flex;">
|
||||
<div
|
||||
class="dv-hover dv-round-8"
|
||||
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
|
||||
@click="${add}"
|
||||
>
|
||||
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
|
||||
<div>New Group</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
selectionController = new TableSelectionController(this);
|
||||
|
||||
get expose(): DataViewInstance {
|
||||
return {
|
||||
clearSelection: () => {
|
||||
this.selectionController.clear();
|
||||
},
|
||||
addRow: position => {
|
||||
if (this.readonly) return;
|
||||
const rowId = this.props.view.rowAdd(position);
|
||||
if (rowId) {
|
||||
this.props.dataViewEle.openDetailPanel({
|
||||
view: this.props.view,
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
return rowId;
|
||||
},
|
||||
focusFirstCell: () => {
|
||||
this.selectionController.focusFirstCell();
|
||||
},
|
||||
showIndicator: evt => {
|
||||
return this.dragController.showIndicator(evt) != null;
|
||||
},
|
||||
hideIndicator: () => {
|
||||
this.dragController.dropPreview.remove();
|
||||
},
|
||||
moveTo: (id, evt) => {
|
||||
const result = this.dragController.getInsertPosition(evt);
|
||||
if (result) {
|
||||
const row = this.props.view.rowGetOrCreate(id);
|
||||
row.move(result.position, undefined, result.groupKey);
|
||||
}
|
||||
},
|
||||
getSelection: () => {
|
||||
return this.selectionController.selection;
|
||||
},
|
||||
view: this.props.view,
|
||||
eventTrace: this.props.eventTrace,
|
||||
};
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.props.view.readonly$.value;
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
const groups = this.props.view.groupTrait.groupsDataList$.value;
|
||||
if (groups) {
|
||||
return html`
|
||||
<div style="display:flex;flex-direction: column;gap: 16px;">
|
||||
${repeat(
|
||||
groups,
|
||||
v => v.key,
|
||||
group => {
|
||||
return html` <affine-data-view-table-group
|
||||
data-group-key="${group.key}"
|
||||
.dataViewEle="${this.props.dataViewEle}"
|
||||
.view="${this.props.view}"
|
||||
.viewEle="${this}"
|
||||
.group="${group}"
|
||||
></affine-data-view-table-group>`;
|
||||
}
|
||||
)}
|
||||
${this.renderAddGroup(this.props.view.groupTrait)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html` <affine-data-view-table-group
|
||||
.dataViewEle="${this.props.dataViewEle}"
|
||||
.view="${this.props.view}"
|
||||
.viewEle="${this}"
|
||||
></affine-data-view-table-group>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const vPadding = this.props.virtualPadding$.value;
|
||||
const wrapperStyle = styleMap({
|
||||
marginLeft: `-${vPadding}px`,
|
||||
marginRight: `-${vPadding}px`,
|
||||
});
|
||||
const containerStyle = styleMap({
|
||||
paddingLeft: `${vPadding}px`,
|
||||
paddingRight: `${vPadding}px`,
|
||||
});
|
||||
return html`
|
||||
${renderUniLit(this.props.headerWidget, {
|
||||
dataViewInstance: this.expose,
|
||||
})}
|
||||
<div class="affine-database-table" style="${wrapperStyle}">
|
||||
<div class="affine-database-block-table" @wheel="${this.onWheel}">
|
||||
<div
|
||||
class="affine-database-table-container"
|
||||
style="${containerStyle}"
|
||||
>
|
||||
${this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-database-table': DataViewTable;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import './pc/effect.js';
|
||||
import './pc-virtual/effect.js';
|
||||
import './pc/effect.js';
|
||||
|
||||
import { createUniComponentFromWebComponent } from '../../core/utils/uni-component/uni-component.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import { tableViewModel } from './define.js';
|
||||
import { MobileDataViewTable } from './mobile/table-view.js';
|
||||
import { TableViewSelector } from './table-view-selector.js';
|
||||
import { MobileTableViewUILogic } from './mobile/table-view-ui-logic.js';
|
||||
import { TableViewUILogic } from './pc/table-view-ui-logic.js';
|
||||
import { VirtualTableViewUILogic } from './pc-virtual/table-view-ui-logic';
|
||||
|
||||
export const tableViewMeta = tableViewModel.createMeta({
|
||||
view: createUniComponentFromWebComponent(TableViewSelector),
|
||||
mobileView: createUniComponentFromWebComponent(MobileDataViewTable),
|
||||
icon: createIcon('DatabaseTableViewIcon'),
|
||||
pcLogic: view =>
|
||||
// @ts-expect-error fixme: typesafe
|
||||
view.manager.dataSource.featureFlags$.value.enable_table_virtual_scroll
|
||||
? VirtualTableViewUILogic
|
||||
: TableViewUILogic,
|
||||
// @ts-expect-error fixme: typesafe
|
||||
mobileLogic: () => MobileTableViewUILogic,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user