mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(editor): extract data view block (#9452)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
|
||||
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
|
||||
import { CodeBlockSpec } from '@blocksuite/affine-block-code';
|
||||
import { DataViewBlockSpec } from '@blocksuite/affine-block-data-view';
|
||||
import { DatabaseBlockSpec } from '@blocksuite/affine-block-database';
|
||||
import { DividerBlockSpec } from '@blocksuite/affine-block-divider';
|
||||
import { EdgelessTextBlockSpec } from '@blocksuite/affine-block-edgeless-text';
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import { AdapterFactoryExtensions } from '../_common/adapters/extension.js';
|
||||
import { DataViewBlockSpec } from '../data-view-block/data-view-spec.js';
|
||||
|
||||
export const CommonBlockSpecs: ExtensionType[] = [
|
||||
DocDisplayMetaService,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { PropertyMetaConfig } from '@blocksuite/data-view';
|
||||
import type { Disposable } from '@blocksuite/global/utils';
|
||||
import type { Block, BlockModel } from '@blocksuite/store';
|
||||
|
||||
type PropertyMeta<
|
||||
T extends BlockModel = BlockModel,
|
||||
Value = unknown,
|
||||
ColumnData extends NonNullable<unknown> = NonNullable<unknown>,
|
||||
> = {
|
||||
name: string;
|
||||
key: string;
|
||||
metaConfig: PropertyMetaConfig<string, ColumnData, Value>;
|
||||
getColumnData?: (block: T) => ColumnData;
|
||||
setColumnData?: (block: T, data: ColumnData) => void;
|
||||
get: (block: T) => Value;
|
||||
set?: (block: T, value: Value) => void;
|
||||
updated: (block: T, callback: () => void) => Disposable;
|
||||
};
|
||||
export type BlockMeta<T extends BlockModel = BlockModel> = {
|
||||
selector: (block: Block) => boolean;
|
||||
properties: PropertyMeta<T>[];
|
||||
};
|
||||
export const createBlockMeta = <T extends BlockModel>(
|
||||
options: Omit<BlockMeta<T>, 'properties'>
|
||||
) => {
|
||||
const meta: BlockMeta = {
|
||||
...options,
|
||||
properties: [],
|
||||
};
|
||||
return {
|
||||
...meta,
|
||||
addProperty: <Value>(property: PropertyMeta<T, Value>) => {
|
||||
meta.properties.push(property as PropertyMeta);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { BlockMeta } from './base.js';
|
||||
import { todoMeta } from './todo.js';
|
||||
|
||||
export const blockMetaMap = {
|
||||
todo: todoMeta,
|
||||
} satisfies Record<string, BlockMeta>;
|
||||
@@ -1,60 +0,0 @@
|
||||
import { richTextColumnConfig } from '@blocksuite/affine-block-database';
|
||||
import { type ListBlockModel, ListBlockSchema } from '@blocksuite/affine-model';
|
||||
import { propertyPresets } from '@blocksuite/data-view/property-presets';
|
||||
|
||||
import { createBlockMeta } from './base.js';
|
||||
|
||||
export const todoMeta = createBlockMeta<ListBlockModel>({
|
||||
selector: block => {
|
||||
if (block.flavour !== ListBlockSchema.model.flavour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (block.model as ListBlockModel).type === 'todo';
|
||||
},
|
||||
});
|
||||
todoMeta.addProperty({
|
||||
name: 'Content',
|
||||
key: 'todo-title',
|
||||
metaConfig: richTextColumnConfig,
|
||||
get: block => block.text.yText,
|
||||
set: (_block, _value) => {
|
||||
//
|
||||
},
|
||||
updated: (block, callback) => {
|
||||
block.text?.yText.observe(callback);
|
||||
return {
|
||||
dispose: () => {
|
||||
block.text?.yText.unobserve(callback);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
todoMeta.addProperty({
|
||||
name: 'Checked',
|
||||
key: 'todo-checked',
|
||||
metaConfig: propertyPresets.checkboxPropertyConfig,
|
||||
get: block => block.checked,
|
||||
set: (block, value) => {
|
||||
block.checked = value;
|
||||
},
|
||||
updated: (block, callback) => {
|
||||
return block.propsUpdated.on(({ key }) => {
|
||||
if (key === 'checked') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
todoMeta.addProperty({
|
||||
name: 'Source',
|
||||
key: 'todo-source',
|
||||
metaConfig: propertyPresets.textPropertyConfig,
|
||||
get: block => block.doc.meta?.title ?? '',
|
||||
updated: (block, callback) => {
|
||||
return block.doc.collection.meta.docMetaUpdated.on(() => {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { richTextColumnConfig } from '@blocksuite/affine-block-database';
|
||||
import type { PropertyMetaConfig } from '@blocksuite/data-view';
|
||||
import { propertyPresets } from '@blocksuite/data-view/property-presets';
|
||||
|
||||
export const queryBlockColumns = [
|
||||
propertyPresets.datePropertyConfig,
|
||||
propertyPresets.numberPropertyConfig,
|
||||
propertyPresets.progressPropertyConfig,
|
||||
propertyPresets.selectPropertyConfig,
|
||||
propertyPresets.multiSelectPropertyConfig,
|
||||
propertyPresets.checkboxPropertyConfig,
|
||||
];
|
||||
export const queryBlockHiddenColumns = [richTextColumnConfig];
|
||||
const queryBlockAllColumns = [...queryBlockColumns, ...queryBlockHiddenColumns];
|
||||
export const queryBlockAllColumnMap = Object.fromEntries(
|
||||
queryBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig])
|
||||
);
|
||||
@@ -1,307 +0,0 @@
|
||||
import {
|
||||
databaseBlockAllPropertyMap,
|
||||
databasePropertyConverts,
|
||||
} from '@blocksuite/affine-block-database';
|
||||
import type { Column } from '@blocksuite/affine-model';
|
||||
import {
|
||||
insertPositionToIndex,
|
||||
type InsertToPosition,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { DataSourceBase, type PropertyMetaConfig } from '@blocksuite/data-view';
|
||||
import { propertyPresets } from '@blocksuite/data-view/property-presets';
|
||||
import { assertExists, Slot } from '@blocksuite/global/utils';
|
||||
import type { Block, Doc } from '@blocksuite/store';
|
||||
|
||||
import type { BlockMeta } from './block-meta/base.js';
|
||||
import { blockMetaMap } from './block-meta/index.js';
|
||||
import { queryBlockAllColumnMap, queryBlockColumns } from './columns/index.js';
|
||||
import type { DataViewBlockModel } from './data-view-model.js';
|
||||
|
||||
export type BlockQueryDataSourceConfig = {
|
||||
type: keyof typeof blockMetaMap;
|
||||
};
|
||||
|
||||
// @ts-expect-error FIXME: ts error
|
||||
export class BlockQueryDataSource extends DataSourceBase {
|
||||
private readonly columnMetaMap = new Map<
|
||||
string,
|
||||
PropertyMetaConfig<any, any, any>
|
||||
>();
|
||||
|
||||
private readonly meta: BlockMeta;
|
||||
|
||||
blockMap = new Map<string, Block>();
|
||||
|
||||
docDisposeMap = new Map<string, () => void>();
|
||||
|
||||
slots = {
|
||||
update: new Slot(),
|
||||
};
|
||||
|
||||
private get blocks() {
|
||||
return [...this.blockMap.values()];
|
||||
}
|
||||
|
||||
get properties(): string[] {
|
||||
return [
|
||||
...this.meta.properties.map(v => v.key),
|
||||
...this.block.columns.map(v => v.id),
|
||||
];
|
||||
}
|
||||
|
||||
get propertyMetas(): PropertyMetaConfig[] {
|
||||
return queryBlockColumns as PropertyMetaConfig[];
|
||||
}
|
||||
|
||||
get rows(): string[] {
|
||||
return this.blocks.map(v => v.id);
|
||||
}
|
||||
|
||||
get workspace() {
|
||||
return this.host.doc.collection;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly host: EditorHost,
|
||||
private readonly block: DataViewBlockModel,
|
||||
config: BlockQueryDataSourceConfig
|
||||
) {
|
||||
super();
|
||||
this.meta = blockMetaMap[config.type];
|
||||
for (const property of this.meta.properties) {
|
||||
this.columnMetaMap.set(property.metaConfig.type, property.metaConfig);
|
||||
}
|
||||
for (const collection of this.workspace.docs.values()) {
|
||||
for (const block of Object.values(collection.getDoc().blocks.peek())) {
|
||||
if (this.meta.selector(block)) {
|
||||
this.blockMap.set(block.id, block);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.workspace.docs.forEach(doc => {
|
||||
this.listenToDoc(doc.getDoc());
|
||||
});
|
||||
this.workspace.slots.docAdded.on(id => {
|
||||
const doc = this.workspace.getDoc(id);
|
||||
if (doc) {
|
||||
this.listenToDoc(doc);
|
||||
}
|
||||
});
|
||||
this.workspace.slots.docRemoved.on(id => {
|
||||
this.docDisposeMap.get(id)?.();
|
||||
});
|
||||
}
|
||||
|
||||
private getProperty(propertyId: string) {
|
||||
const property = this.meta.properties.find(v => v.key === propertyId);
|
||||
assertExists(property, `property ${propertyId} not found`);
|
||||
return property;
|
||||
}
|
||||
|
||||
private newColumnName() {
|
||||
let i = 1;
|
||||
while (this.block.columns.some(column => column.name === `Column ${i}`)) {
|
||||
i++;
|
||||
}
|
||||
return `Column ${i}`;
|
||||
}
|
||||
|
||||
cellValueChange(rowId: string, propertyId: string, value: unknown): void {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
this.block.cells[rowId] = {
|
||||
...this.block.cells[rowId],
|
||||
[propertyId]: value,
|
||||
};
|
||||
return;
|
||||
}
|
||||
const block = this.blockMap.get(rowId);
|
||||
if (block) {
|
||||
this.meta.properties
|
||||
.find(v => v.key === propertyId)
|
||||
?.set?.(block.model, value);
|
||||
}
|
||||
}
|
||||
|
||||
cellValueGet(rowId: string, propertyId: string): unknown {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
return this.block.cells[rowId]?.[propertyId];
|
||||
}
|
||||
const block = this.blockMap.get(rowId);
|
||||
if (block) {
|
||||
return this.getProperty(propertyId)?.get(block.model);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
getViewColumn(id: string) {
|
||||
return this.block.columns.find(v => v.id === id);
|
||||
}
|
||||
|
||||
listenToDoc(doc: Doc) {
|
||||
this.docDisposeMap.set(
|
||||
doc.id,
|
||||
doc.slots.blockUpdated.on(v => {
|
||||
if (v.type === 'add') {
|
||||
const blockById = doc.getBlock(v.id);
|
||||
if (blockById && this.meta.selector(blockById)) {
|
||||
this.blockMap.set(v.id, blockById);
|
||||
}
|
||||
} else if (v.type === 'delete') {
|
||||
this.blockMap.delete(v.id);
|
||||
}
|
||||
this.slots.update.emit();
|
||||
}).dispose
|
||||
);
|
||||
}
|
||||
|
||||
propertyAdd(
|
||||
insertToPosition: InsertToPosition,
|
||||
type: string | undefined
|
||||
): string {
|
||||
const doc = this.block.doc;
|
||||
doc.captureSync();
|
||||
const column = databaseBlockAllPropertyMap[
|
||||
type ?? propertyPresets.multiSelectPropertyConfig.type
|
||||
].create(this.newColumnName());
|
||||
|
||||
const id = doc.generateBlockId();
|
||||
if (this.block.columns.some(v => v.id === id)) {
|
||||
return id;
|
||||
}
|
||||
doc.transact(() => {
|
||||
const col: Column = {
|
||||
...column,
|
||||
id,
|
||||
};
|
||||
this.block.columns.splice(
|
||||
insertPositionToIndex(insertToPosition, this.block.columns),
|
||||
0,
|
||||
col
|
||||
);
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
propertyDataGet(propertyId: string): Record<string, unknown> {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
return viewColumn.data;
|
||||
}
|
||||
const property = this.getProperty(propertyId);
|
||||
return (
|
||||
property.getColumnData?.(this.blocks[0].model) ??
|
||||
property.metaConfig.config.defaultData()
|
||||
);
|
||||
}
|
||||
|
||||
propertyDataSet(propertyId: string, data: Record<string, unknown>): void {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
viewColumn.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
propertyDelete(_id: string): void {
|
||||
const index = this.block.columns.findIndex(v => v.id === _id);
|
||||
if (index >= 0) {
|
||||
this.block.columns.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
propertyDuplicate(_columnId: string): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
propertyMetaGet(type: string): PropertyMetaConfig {
|
||||
const meta = this.columnMetaMap.get(type);
|
||||
if (meta) {
|
||||
return meta;
|
||||
}
|
||||
return queryBlockAllColumnMap[type];
|
||||
}
|
||||
|
||||
propertyNameGet(propertyId: string): string {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
return viewColumn.name;
|
||||
}
|
||||
if (propertyId === 'type') {
|
||||
return 'Block Type';
|
||||
}
|
||||
return this.getProperty(propertyId)?.name ?? '';
|
||||
}
|
||||
|
||||
propertyNameSet(propertyId: string, name: string): void {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
viewColumn.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
override propertyReadonlyGet(propertyId: string): boolean {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
return false;
|
||||
}
|
||||
if (propertyId === 'type') return true;
|
||||
return this.getProperty(propertyId)?.set == null;
|
||||
}
|
||||
|
||||
propertyTypeGet(propertyId: string): string {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
return viewColumn.type;
|
||||
}
|
||||
if (propertyId === 'type') {
|
||||
return 'image';
|
||||
}
|
||||
return this.getProperty(propertyId).metaConfig.type;
|
||||
}
|
||||
|
||||
propertyTypeSet(propertyId: string, toType: string): void {
|
||||
const viewColumn = this.getViewColumn(propertyId);
|
||||
if (viewColumn) {
|
||||
const currentType = viewColumn.type;
|
||||
const currentData = viewColumn.data;
|
||||
const rows = this.rows$.value;
|
||||
const currentCells = rows.map(rowId =>
|
||||
this.cellValueGet(rowId, propertyId)
|
||||
);
|
||||
const convertFunction = databasePropertyConverts.find(
|
||||
v => v.from === currentType && v.to === toType
|
||||
)?.convert;
|
||||
const result = convertFunction?.(
|
||||
currentData as any,
|
||||
|
||||
currentCells as any
|
||||
) ?? {
|
||||
property: databaseBlockAllPropertyMap[toType].config.defaultData(),
|
||||
cells: currentCells.map(() => undefined),
|
||||
};
|
||||
this.block.doc.captureSync();
|
||||
viewColumn.type = toType;
|
||||
viewColumn.data = result.property;
|
||||
currentCells.forEach((value, i) => {
|
||||
if (value != null || result.cells[i] != null) {
|
||||
this.block.cells[rows[i]] = {
|
||||
...this.block.cells[rows[i]],
|
||||
[propertyId]: result.cells[i],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rowAdd(_insertPosition: InsertToPosition | number): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
rowDelete(_ids: string[]): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
rowMove(_rowId: string, _position: InsertToPosition): void {}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
import { BlockRenderer, NoteRenderer } from '@blocksuite/affine-block-database';
|
||||
import type { NoteBlockComponent } from '@blocksuite/affine-block-note';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import {
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
NotificationProvider,
|
||||
type TelemetryEventMap,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/block-std';
|
||||
import {
|
||||
createRecordDetail,
|
||||
createUniComponentFromWebComponent,
|
||||
DatabaseSelection,
|
||||
type DataSource,
|
||||
DataView,
|
||||
dataViewCommonStyle,
|
||||
type DataViewProps,
|
||||
type DataViewSelection,
|
||||
type DataViewWidget,
|
||||
type DataViewWidgetProps,
|
||||
defineUniComponent,
|
||||
renderUniLit,
|
||||
uniMap,
|
||||
} from '@blocksuite/data-view';
|
||||
import { widgetPresets } from '@blocksuite/data-view/widget-presets';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { css, nothing, unsafeCSS } from 'lit';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import {
|
||||
EdgelessRootBlockComponent,
|
||||
type RootService,
|
||||
} from '../root-block/index.js';
|
||||
import { BlockQueryDataSource } from './data-source.js';
|
||||
import type { DataViewBlockModel } from './data-view-model.js';
|
||||
|
||||
export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBlockModel> {
|
||||
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;
|
||||
}
|
||||
|
||||
.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) => {
|
||||
popMenu(popupTargetFromElement(e.currentTarget as HTMLElement), {
|
||||
options: {
|
||||
items: [
|
||||
menu.input({
|
||||
initialValue: this.model.title,
|
||||
placeholder: 'Untitled',
|
||||
onChange: text => {
|
||||
this.model.title = text;
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
prefix: CopyIcon,
|
||||
name: 'Copy',
|
||||
select: () => {
|
||||
const slice = Slice.fromModels(this.doc, [this.model]);
|
||||
this.std.clipboard.copySlice(slice).catch(console.error);
|
||||
},
|
||||
}),
|
||||
menu.group({
|
||||
name: '',
|
||||
items: [
|
||||
menu.action({
|
||||
prefix: DeleteIcon,
|
||||
class: {
|
||||
'delete-item': true,
|
||||
},
|
||||
name: 'Delete Database',
|
||||
select: () => {
|
||||
this.model.children.slice().forEach(block => {
|
||||
this.doc.deleteBlock(block);
|
||||
});
|
||||
this.doc.deleteBlock(this.model);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _dataSource?: DataSource;
|
||||
|
||||
private readonly dataView = new DataView();
|
||||
|
||||
_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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
getRootService = () => {
|
||||
return this.std.getService<RootService>('affine:page');
|
||||
};
|
||||
|
||||
headerWidget: DataViewWidget = defineUniComponent(
|
||||
(props: DataViewWidgetProps) => {
|
||||
return html`
|
||||
<div style="margin-bottom: 16px;display:flex;flex-direction: column">
|
||||
<div style="display:flex;gap:8px;padding: 0 6px;margin-bottom: 8px;">
|
||||
<div>${this.model.title}</div>
|
||||
${this.renderDatabaseOps()}
|
||||
</div>
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content: space-between;gap: 12px"
|
||||
class="database-header-bar"
|
||||
>
|
||||
<div style="flex:1">
|
||||
${renderUniLit(widgetPresets.viewBar, props)}
|
||||
</div>
|
||||
${renderUniLit(this.toolsWidget, props)}
|
||||
</div>
|
||||
${renderUniLit(widgetPresets.quickSettingBar, props)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
selection$ = computed(() => {
|
||||
const databaseSelection = this.selection.value.find(
|
||||
(selection): selection is DatabaseSelection => {
|
||||
if (selection.blockId !== this.blockId) {
|
||||
return false;
|
||||
}
|
||||
return selection instanceof DatabaseSelection;
|
||||
}
|
||||
);
|
||||
return databaseSelection?.viewSelection;
|
||||
});
|
||||
|
||||
setSelection = (selection: DataViewSelection | undefined) => {
|
||||
this.selection.setGroup(
|
||||
'note',
|
||||
selection
|
||||
? [
|
||||
new DatabaseSelection({
|
||||
blockId: this.blockId,
|
||||
viewSelection: selection,
|
||||
}),
|
||||
]
|
||||
: []
|
||||
);
|
||||
};
|
||||
|
||||
toolsWidget: DataViewWidget = widgetPresets.createTools({
|
||||
table: [
|
||||
widgetPresets.tools.filter,
|
||||
widgetPresets.tools.search,
|
||||
widgetPresets.tools.viewOptions,
|
||||
widgetPresets.tools.tableAddRow,
|
||||
],
|
||||
kanban: [
|
||||
widgetPresets.tools.filter,
|
||||
widgetPresets.tools.search,
|
||||
widgetPresets.tools.viewOptions,
|
||||
],
|
||||
});
|
||||
|
||||
get dataSource(): DataSource {
|
||||
if (!this._dataSource) {
|
||||
this._dataSource = new BlockQueryDataSource(this.host, this.model, {
|
||||
type: 'todo',
|
||||
});
|
||||
}
|
||||
return this._dataSource;
|
||||
}
|
||||
|
||||
override get topContenteditableElement() {
|
||||
if (this.rootComponent instanceof EdgelessRootBlockComponent) {
|
||||
const note = this.closest<NoteBlockComponent>('affine-note');
|
||||
return note;
|
||||
}
|
||||
return this.rootComponent;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.dataView.expose;
|
||||
}
|
||||
|
||||
private renderDatabaseOps() {
|
||||
if (this.doc.readonly) {
|
||||
return nothing;
|
||||
}
|
||||
return html` <div class="database-ops" @click="${this._clickDatabaseOps}">
|
||||
${MoreHorizontalIcon}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-data-view': DataViewBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { Column } from '@blocksuite/affine-model';
|
||||
import {
|
||||
arrayMove,
|
||||
insertPositionToIndex,
|
||||
type InsertToPosition,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { DataViewDataType } from '@blocksuite/data-view';
|
||||
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
views: DataViewDataType[];
|
||||
columns: Column[];
|
||||
cells: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
|
||||
export class DataViewBlockModel extends BlockModel<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
applyViewsUpdate() {
|
||||
this.doc.updateBlock(this, {
|
||||
views: this.views,
|
||||
});
|
||||
}
|
||||
|
||||
deleteView(id: string) {
|
||||
this.doc.captureSync();
|
||||
this.doc.transact(() => {
|
||||
this.views = this.views.filter(v => v.id !== id);
|
||||
});
|
||||
}
|
||||
|
||||
duplicateView(id: string): string {
|
||||
const newId = this.doc.generateBlockId();
|
||||
this.doc.transact(() => {
|
||||
const index = this.views.findIndex(v => v.id === id);
|
||||
const view = this.views[index];
|
||||
if (view) {
|
||||
this.views.splice(
|
||||
index + 1,
|
||||
0,
|
||||
JSON.parse(JSON.stringify({ ...view, id: newId }))
|
||||
);
|
||||
}
|
||||
});
|
||||
return newId;
|
||||
}
|
||||
|
||||
moveViewTo(id: string, position: InsertToPosition) {
|
||||
this.doc.transact(() => {
|
||||
this.views = arrayMove(
|
||||
this.views,
|
||||
v => v.id === id,
|
||||
arr => insertPositionToIndex(position, arr)
|
||||
);
|
||||
});
|
||||
this.applyViewsUpdate();
|
||||
}
|
||||
|
||||
updateView(
|
||||
id: string,
|
||||
update: (data: DataViewDataType) => Partial<DataViewDataType>
|
||||
) {
|
||||
this.doc.transact(() => {
|
||||
this.views = this.views.map(v => {
|
||||
if (v.id !== id) {
|
||||
return v;
|
||||
}
|
||||
return { ...v, ...(update(v) as DataViewDataType) };
|
||||
});
|
||||
});
|
||||
this.applyViewsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export const DataViewBlockSchema = defineBlockSchema({
|
||||
flavour: 'affine:data-view',
|
||||
props: (): Props => ({
|
||||
views: [],
|
||||
title: '',
|
||||
columns: [],
|
||||
cells: {},
|
||||
}),
|
||||
metadata: {
|
||||
role: 'hub',
|
||||
version: 1,
|
||||
parent: ['affine:note'],
|
||||
children: ['affine:paragraph', 'affine:list'],
|
||||
},
|
||||
toModel: () => {
|
||||
return new DataViewBlockModel();
|
||||
},
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import {
|
||||
BlockViewExtension,
|
||||
type ExtensionType,
|
||||
FlavourExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
export const DataViewBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:data-view'),
|
||||
BlockViewExtension('affine:data-view', literal`affine-data-view`),
|
||||
];
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { DataViewBlockModel } from './data-view-model.js';
|
||||
|
||||
export * from './data-view-block.js';
|
||||
export * from './data-view-model.js';
|
||||
export * from './data-view-spec.js';
|
||||
|
||||
declare global {
|
||||
namespace BlockSuite {
|
||||
interface BlockModels {
|
||||
'affine:data-view': DataViewBlockModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ViewMeta } from '@blocksuite/data-view';
|
||||
import { viewPresets } from '@blocksuite/data-view/view-presets';
|
||||
|
||||
export const blockQueryViews: ViewMeta[] = [
|
||||
viewPresets.tableViewMeta,
|
||||
viewPresets.kanbanViewMeta,
|
||||
];
|
||||
|
||||
export const blockQueryViewMap = Object.fromEntries(
|
||||
blockQueryViews.map(view => [view.type, view])
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { effects as blockAttachmentEffects } from '@blocksuite/affine-block-attachment/effects';
|
||||
import { effects as blockBookmarkEffects } from '@blocksuite/affine-block-bookmark/effects';
|
||||
import { effects as blockCodeEffects } from '@blocksuite/affine-block-code/effects';
|
||||
import { effects as blockDataViewEffects } from '@blocksuite/affine-block-data-view/effects';
|
||||
import { effects as blockDatabaseEffects } from '@blocksuite/affine-block-database/effects';
|
||||
import { effects as blockDividerEffects } from '@blocksuite/affine-block-divider/effects';
|
||||
import { effects as blockEdgelessTextEffects } from '@blocksuite/affine-block-edgeless-text/effects';
|
||||
@@ -38,7 +39,6 @@ import { effects as inlineEffects } from '@blocksuite/inline/effects';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { registerSpecs } from './_specs/register-specs.js';
|
||||
import { DataViewBlockComponent } from './data-view-block/index.js';
|
||||
import { EdgelessAutoCompletePanel } from './root-block/edgeless/components/auto-complete/auto-complete-panel.js';
|
||||
import { EdgelessAutoComplete } from './root-block/edgeless/components/auto-complete/edgeless-auto-complete.js';
|
||||
import { EdgelessToolIconButton } from './root-block/edgeless/components/buttons/tool-icon-button.js';
|
||||
@@ -209,6 +209,7 @@ export function effects() {
|
||||
blockLatexEffects();
|
||||
blockEdgelessTextEffects();
|
||||
blockDividerEffects();
|
||||
blockDataViewEffects();
|
||||
blockCodeEffects();
|
||||
|
||||
componentCaptionEffects();
|
||||
@@ -234,7 +235,6 @@ export function effects() {
|
||||
customElements.define('affine-preview-root', PreviewRootBlockComponent);
|
||||
customElements.define('mini-mindmap-preview', MiniMindmapPreview);
|
||||
customElements.define('mini-mindmap-surface-block', MindmapSurfaceBlock);
|
||||
customElements.define('affine-data-view', DataViewBlockComponent);
|
||||
customElements.define('affine-edgeless-root', EdgelessRootBlockComponent);
|
||||
customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel);
|
||||
customElements.define(
|
||||
|
||||
@@ -17,7 +17,6 @@ export * from './_common/test-utils/test-utils.js';
|
||||
export * from './_common/transformers/index.js';
|
||||
export { type AbstractEditor } from './_common/types.js';
|
||||
export * from './_specs/index.js';
|
||||
export * from './data-view-block';
|
||||
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
|
||||
export type {
|
||||
Template,
|
||||
@@ -42,6 +41,7 @@ export {
|
||||
export * from '@blocksuite/affine-block-attachment';
|
||||
export * from '@blocksuite/affine-block-bookmark';
|
||||
export * from '@blocksuite/affine-block-code';
|
||||
export * from '@blocksuite/affine-block-data-view';
|
||||
export * from '@blocksuite/affine-block-database';
|
||||
export * from '@blocksuite/affine-block-divider';
|
||||
export * from '@blocksuite/affine-block-edgeless-text';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
|
||||
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-block-bookmark';
|
||||
import type { DataViewBlockComponent } from '@blocksuite/affine-block-data-view';
|
||||
import {
|
||||
FigmaIcon,
|
||||
GithubIcon,
|
||||
@@ -51,7 +52,6 @@ import type { BlockModel } from '@blocksuite/store';
|
||||
import { Slice, Text } from '@blocksuite/store';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import type { DataViewBlockComponent } from '../../../data-view-block/index.js';
|
||||
import type { RootBlockComponent } from '../../types.js';
|
||||
import { formatDate, formatTime } from '../../utils/misc.js';
|
||||
import type { AffineLinkedDocWidget } from '../linked-doc/index.js';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Import models only, the bundled file should not include anything else.
|
||||
import { DataViewBlockSchema } from '@blocksuite/affine-block-data-view';
|
||||
import { SurfaceBlockSchema } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
AttachmentBlockSchema,
|
||||
@@ -26,8 +27,6 @@ import {
|
||||
import type { BlockSchema } from '@blocksuite/store';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { DataViewBlockSchema } from './data-view-block/data-view-model.js';
|
||||
|
||||
/** Built-in first party block models built for affine */
|
||||
export const AffineSchemas: z.infer<typeof BlockSchema>[] = [
|
||||
CodeBlockSchema,
|
||||
|
||||
Reference in New Issue
Block a user