refactor(editor): unify directories naming (#11516)

**Directory Structure Changes**

- Renamed multiple block-related directories by removing the "block-" prefix:
  - `block-attachment` → `attachment`
  - `block-bookmark` → `bookmark`
  - `block-callout` → `callout`
  - `block-code` → `code`
  - `block-data-view` → `data-view`
  - `block-database` → `database`
  - `block-divider` → `divider`
  - `block-edgeless-text` → `edgeless-text`
  - `block-embed` → `embed`
This commit is contained in:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions

View File

@@ -0,0 +1,37 @@
import type { PropertyMetaConfig } from '@blocksuite/data-view';
import type { DisposableMember } from '@blocksuite/global/disposable';
import type { Block, BlockModel } from '@blocksuite/store';
type PropertyMeta<
T extends BlockModel = BlockModel,
RawValue = unknown,
JsonValue = unknown,
ColumnData extends NonNullable<unknown> = NonNullable<unknown>,
> = {
name: string;
key: string;
metaConfig: PropertyMetaConfig<string, ColumnData, RawValue, JsonValue>;
getColumnData?: (block: T) => ColumnData;
setColumnData?: (block: T, data: ColumnData) => void;
get: (block: T) => RawValue;
set?: (block: T, value: RawValue) => void;
updated: (block: T, callback: () => void) => DisposableMember;
};
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);
},
};
};

View File

@@ -0,0 +1,6 @@
import type { BlockMeta } from './base.js';
import { todoMeta } from './todo.js';
export const blockMetaMap = {
todo: todoMeta,
} satisfies Record<string, BlockMeta>;

View File

@@ -0,0 +1,13 @@
import { type ListBlockModel, ListBlockSchema } from '@blocksuite/affine-model';
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).props.type === 'todo';
},
});

View File

@@ -0,0 +1,22 @@
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: PropertyMetaConfig<
string,
any,
any,
any
>[] = [richTextColumnConfig];
const queryBlockAllColumns = [...queryBlockColumns, ...queryBlockHiddenColumns];
export const queryBlockAllColumnMap = Object.fromEntries(
queryBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig])
);

View File

@@ -0,0 +1,51 @@
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { DatabaseTableViewIcon } from '@blocksuite/icons/lit';
import type { DataViewBlockComponent } from '../data-view-block';
import { ToDoListTooltip } from './tooltips';
export const dataViewSlashMenuConfig: SlashMenuConfig = {
disableWhen: ({ model }) => {
return model.flavour === 'affine:data-view';
},
items: [
{
name: 'Todo',
searchAlias: ['todo view'],
icon: DatabaseTableViewIcon(),
tooltip: {
figure: ToDoListTooltip,
caption: 'To-do List',
},
group: '7_Database@1',
when: ({ model, std }) =>
!isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text') &&
!!std.get(FeatureFlagService).getFlag('enable_block_query'),
action: ({ model, std }) => {
const { host } = std;
const parent = host.doc.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
const id = host.doc.addBlock(
'affine:data-view',
{},
host.doc.getParent(model),
index + 1
);
const dataViewModel = host.doc.getBlock(id)!;
const dataView = std.view.getBlock(
dataViewModel.id
) as DataViewBlockComponent | null;
dataView?.dataSource.viewManager.viewAdd('table');
if (model.text?.length === 0) {
model.doc.deleteBlock(model);
}
},
},
],
};

View File

@@ -0,0 +1,16 @@
import { html } from 'lit';
// prettier-ignore
export const ToDoListTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="170" height="68" rx="2" fill="white"/>
<mask id="mask0_16460_960" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
<rect width="170" height="68" rx="2" fill="white"/>
</mask>
<g mask="url(#mask0_16460_960)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6667 19C12.7462 19 12 19.7462 12 20.6667V27.3333C12 28.2538 12.7462 29 13.6667 29H20.3333C21.2538 29 22 28.2538 22 27.3333V20.6667C22 19.7462 21.2538 19 20.3333 19H13.6667ZM12.9091 20.6667C12.9091 20.2483 13.2483 19.9091 13.6667 19.9091H20.3333C20.7517 19.9091 21.0909 20.2483 21.0909 20.6667V27.3333C21.0909 27.7517 20.7517 28.0909 20.3333 28.0909H13.6667C13.2483 28.0909 12.9091 27.7517 12.9091 27.3333V20.6667Z" fill="#77757D"/>
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="28" y="27.6364">Here is an example of todo list.</tspan></text>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 40.6667C12 39.7462 12.7462 39 13.6667 39H20.3333C21.2538 39 22 39.7462 22 40.6667V47.3333C22 48.2538 21.2538 49 20.3333 49H13.6667C12.7462 49 12 48.2538 12 47.3333V40.6667ZM19.7457 42.5032C19.9232 42.3257 19.9232 42.0379 19.7457 41.8604C19.5681 41.6829 19.2803 41.6829 19.1028 41.8604L16.0909 44.8723L15.2002 43.9816C15.0227 43.8041 14.7349 43.8041 14.5574 43.9816C14.3799 44.1591 14.3799 44.4469 14.5574 44.6244L15.7695 45.8366C15.947 46.0141 16.2348 46.0141 16.4123 45.8366L19.7457 42.5032Z" fill="#1E96EB"/>
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="28" y="47.6364">Make a list for building preview.</tspan></text>
</g>
</svg>
`;

View File

@@ -0,0 +1,318 @@
import {
DatabaseBlockDataSource,
databasePropertyConverts,
} from '@blocksuite/affine-block-database';
import type { ColumnDataType } from '@blocksuite/affine-model';
import {
insertPositionToIndex,
type InsertToPosition,
} from '@blocksuite/affine-shared/utils';
import { DataSourceBase, type PropertyMetaConfig } from '@blocksuite/data-view';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import type { EditorHost } from '@blocksuite/std';
import type { Block, Store } from '@blocksuite/store';
import { Subject } from 'rxjs';
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 Subject(),
};
private get blocks() {
return [...this.blockMap.values()];
}
get properties(): string[] {
return [
...this.meta.properties.map(v => v.key),
...this.block.props.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.workspace;
}
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.getStore().blocks.peek())) {
if (this.meta.selector(block)) {
this.blockMap.set(block.id, block);
}
}
}
this.workspace.docs.forEach(doc => {
this.listenToDoc(doc.getStore());
});
this.workspace.slots.docCreated.subscribe(id => {
const doc = this.workspace.getDoc(id);
if (doc) {
this.listenToDoc(doc.getStore());
}
});
this.workspace.slots.docRemoved.subscribe(id => {
this.docDisposeMap.get(id)?.();
});
}
private getProperty(propertyId: string) {
const property = this.meta.properties.find(v => v.key === propertyId);
if (!property) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
`property ${propertyId} not found`
);
}
return property;
}
private newColumnName() {
let i = 1;
while (
this.block.props.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.props.cells[rowId] = {
...this.block.props.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.props.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.props.columns.find(v => v.id === id);
}
listenToDoc(doc: Store) {
this.docDisposeMap.set(
doc.id,
doc.slots.blockUpdated.subscribe(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.next(undefined);
}).unsubscribe
);
}
propertyAdd(
insertToPosition: InsertToPosition,
type: string | undefined
): string {
const doc = this.block.doc;
doc.captureSync();
const column = DatabaseBlockDataSource.propertiesMap.value[
type ?? propertyPresets.multiSelectPropertyConfig.type
].create(this.newColumnName());
const id = doc.workspace.idGenerator();
if (this.block.props.columns.some(v => v.id === id)) {
return id;
}
doc.transact(() => {
const col: ColumnDataType = {
...column,
id,
};
this.block.props.columns.splice(
insertPositionToIndex(insertToPosition, this.block.props.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.propertyData.default()
);
}
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.props.columns.findIndex(v => v.id === _id);
if (index >= 0) {
this.block.props.columns.splice(index, 1);
}
}
propertyDuplicate(_columnId: string): string | undefined {
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:
DatabaseBlockDataSource.propertiesMap.value[
toType
].config.propertyData.default(),
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.props.cells[rows[i]] = {
...this.block.props.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 {}
}

View File

@@ -0,0 +1,324 @@
import {
BlockRenderer,
DatabaseSelection,
NoteRenderer,
} from '@blocksuite/affine-block-database';
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { CopyIcon, DeleteIcon } from '@blocksuite/affine-components/icons';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
NotificationProvider,
type TelemetryEventMap,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import {
createRecordDetail,
createUniComponentFromWebComponent,
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 { MoreHorizontalIcon } from '@blocksuite/icons/lit';
import { type BlockComponent } from '@blocksuite/std';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
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 { 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.props.title,
placeholder: 'Untitled',
onChange: text => {
this.model.props.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,
}),
};
};
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.props.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.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
}
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;
}
}

View File

@@ -0,0 +1,102 @@
import type { ColumnDataType } from '@blocksuite/affine-model';
import {
arrayMove,
insertPositionToIndex,
type InsertToPosition,
} from '@blocksuite/affine-shared/utils';
import type { DataViewDataType } from '@blocksuite/data-view';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
type Props = {
title: string;
views: DataViewDataType[];
columns: ColumnDataType[];
cells: Record<string, Record<string, unknown>>;
};
export class DataViewBlockModel extends BlockModel<Props> {
constructor() {
super();
}
applyViewsUpdate() {
this.doc.updateBlock(this, {
views: this.props.views,
});
}
deleteView(id: string) {
this.doc.captureSync();
this.doc.transact(() => {
this.props.views = this.props.views.filter(v => v.id !== id);
});
}
duplicateView(id: string): string {
const newId = this.doc.workspace.idGenerator();
this.doc.transact(() => {
const index = this.props.views.findIndex(v => v.id === id);
const view = this.props.views[index];
if (view) {
this.props.views.splice(
index + 1,
0,
JSON.parse(JSON.stringify({ ...view, id: newId }))
);
}
});
return newId;
}
moveViewTo(id: string, position: InsertToPosition) {
this.doc.transact(() => {
this.props.views = arrayMove(
this.props.views,
v => v.id === id,
arr => insertPositionToIndex(position, arr)
);
});
this.applyViewsUpdate();
}
updateView(
id: string,
update: (data: DataViewDataType) => Partial<DataViewDataType>
) {
this.doc.transact(() => {
this.props.views = this.props.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();
},
});
export const DataViewBlockSchemaExtension =
BlockSchemaExtension(DataViewBlockSchema);

View File

@@ -0,0 +1,8 @@
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
export const DataViewBlockSpec: ExtensionType[] = [
FlavourExtension('affine:data-view'),
BlockViewExtension('affine:data-view', literal`affine-data-view`),
];

View File

@@ -0,0 +1,5 @@
import { DataViewBlockComponent } from './data-view-block';
export function effects() {
customElements.define('affine-data-view', DataViewBlockComponent);
}

View File

@@ -0,0 +1,3 @@
export * from './data-view-block.js';
export * from './data-view-model.js';
export * from './data-view-spec.js';

View File

@@ -0,0 +1,11 @@
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])
);