mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
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:
37
blocksuite/affine/blocks/data-view/src/block-meta/base.ts
Normal file
37
blocksuite/affine/blocks/data-view/src/block-meta/base.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { BlockMeta } from './base.js';
|
||||
import { todoMeta } from './todo.js';
|
||||
|
||||
export const blockMetaMap = {
|
||||
todo: todoMeta,
|
||||
} satisfies Record<string, BlockMeta>;
|
||||
13
blocksuite/affine/blocks/data-view/src/block-meta/todo.ts
Normal file
13
blocksuite/affine/blocks/data-view/src/block-meta/todo.ts
Normal 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';
|
||||
},
|
||||
});
|
||||
22
blocksuite/affine/blocks/data-view/src/columns/index.ts
Normal file
22
blocksuite/affine/blocks/data-view/src/columns/index.ts
Normal 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])
|
||||
);
|
||||
51
blocksuite/affine/blocks/data-view/src/configs/slash-menu.ts
Normal file
51
blocksuite/affine/blocks/data-view/src/configs/slash-menu.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
16
blocksuite/affine/blocks/data-view/src/configs/tooltips.ts
Normal file
16
blocksuite/affine/blocks/data-view/src/configs/tooltips.ts
Normal 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>
|
||||
`;
|
||||
318
blocksuite/affine/blocks/data-view/src/data-source.ts
Normal file
318
blocksuite/affine/blocks/data-view/src/data-source.ts
Normal 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 {}
|
||||
}
|
||||
324
blocksuite/affine/blocks/data-view/src/data-view-block.ts
Normal file
324
blocksuite/affine/blocks/data-view/src/data-view-block.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
102
blocksuite/affine/blocks/data-view/src/data-view-model.ts
Normal file
102
blocksuite/affine/blocks/data-view/src/data-view-model.ts
Normal 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);
|
||||
8
blocksuite/affine/blocks/data-view/src/data-view-spec.ts
Normal file
8
blocksuite/affine/blocks/data-view/src/data-view-spec.ts
Normal 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`),
|
||||
];
|
||||
5
blocksuite/affine/blocks/data-view/src/effects.ts
Normal file
5
blocksuite/affine/blocks/data-view/src/effects.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DataViewBlockComponent } from './data-view-block';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-data-view', DataViewBlockComponent);
|
||||
}
|
||||
3
blocksuite/affine/blocks/data-view/src/index.ts
Normal file
3
blocksuite/affine/blocks/data-view/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './data-view-block.js';
|
||||
export * from './data-view-model.js';
|
||||
export * from './data-view-spec.js';
|
||||
11
blocksuite/affine/blocks/data-view/src/views/index.ts
Normal file
11
blocksuite/affine/blocks/data-view/src/views/index.ts
Normal 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])
|
||||
);
|
||||
Reference in New Issue
Block a user