Files
AFFiNE-Mirror/libs/datasource/db-service/src/services/editor-block/index.ts

437 lines
13 KiB
TypeScript

import { diffArrays } from 'diff';
import { has } from '@toeverything/utils';
import { ServiceBaseClass } from '../base';
import type { ReturnUnobserve } from '../database/observer';
import {
CreateEditorBlock,
ReturnEditorBlock,
GetEditorBlock,
UpdateEditorBlock,
DeleteEditorBlock,
AddColumnProps,
RemoveColumnProps,
UpdateColumnProps,
BlockFlavorKeys,
} from './types';
import {
dbBlock2BusinessBlock,
serializeColumnConfig,
deserializeColumnConfig,
getOrInitBlockContentColumnsField,
addColumn,
Column,
} from './utils';
import { BlockImplInstance, MapOperation } from '@toeverything/datasource/jwt';
import { TemplateProperties, Template } from './templates/types';
export type ObserveCallback = (businessBlock: ReturnEditorBlock) => void;
export class EditorBlock extends ServiceBaseClass {
async create({
workspace,
type,
parentId,
}: CreateEditorBlock): Promise<ReturnEditorBlock> {
const db = await this.database.getDatabase(workspace);
const dbBlock = await db.get(type as 'block');
if (parentId) {
const parentBlock = await db.get(parentId as 'block');
if (parentBlock.id === parentId) {
parentBlock.insertChildren(dbBlock);
}
}
// Initialize the columns field of the block
getOrInitBlockContentColumnsField(dbBlock);
return dbBlock2BusinessBlock({
workspace,
dbBlock,
}) as ReturnEditorBlock;
}
async get({
workspace,
ids,
}: GetEditorBlock): Promise<Array<ReturnEditorBlock | null>> {
const blocks = await Promise.all(
ids.map(async id => {
const block = await this.getBlock(workspace, id);
return dbBlock2BusinessBlock({
workspace,
dbBlock: block,
});
})
);
return blocks;
}
async getBlockByFlavor(
workspace: string,
flavor: BlockFlavorKeys
): Promise<string[]> {
const db = await this.database.getDatabase(workspace);
const keys: string[] = await db.getBlockByFlavor(flavor);
return keys;
}
async getUserId(workspace: string): Promise<string> {
const db = await this.database.getDatabase(workspace);
return db.getUserId();
}
async update(businessBlock: UpdateEditorBlock): Promise<boolean> {
const db = await this.database.getDatabase(businessBlock.workspace);
if (!businessBlock.id) {
return false;
}
const db_block = await this.getBlock(
businessBlock.workspace,
businessBlock.id as 'block'
);
if (!db_block) {
return false;
}
if (
has(businessBlock, 'type') &&
businessBlock.type !== db_block.flavor
) {
db_block.setFlavor(businessBlock.type as 'text');
}
if (
has(businessBlock, 'parentId') &&
businessBlock.parentId !== db_block.parent?.id
) {
db_block.remove();
const parent = await db.get(businessBlock.id as 'block');
if (!parent) {
return false;
}
parent.append(db_block);
}
if (
has(businessBlock, 'children') &&
businessBlock.children !== db_block.children
) {
const patches = diffArrays(
db_block.children || [],
businessBlock.children || []
);
let position = 0;
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
if (patch.added) {
if (patch.value.length) {
for (let i = 0; i < patch.value.length; i++) {
const child = await db.get(
patch.value[i] as 'block'
);
if (child && child.id === patch.value[i]) {
db_block.insertChildren(child, {
pos: position,
});
position = position + 1;
}
}
}
} else if (patch.removed) {
patch.value.forEach(child_id => {
db_block.removeChildren(child_id);
});
} else if (patch.count) {
position = position + patch.count;
}
}
}
const decorations = db_block.getDecorations();
Object.entries(businessBlock.properties || {}).forEach(
([key, value]) => {
if (value === undefined) {
db_block.removeDecoration(key);
return;
}
if (decorations[key] !== value) {
db_block.setDecoration(key, value);
return;
}
}
);
return true;
}
async delete({ workspace, id }: DeleteEditorBlock): Promise<boolean> {
const db = await this.database.getDatabase(workspace);
const db_block = await db.get(id as 'block');
if (!db_block) {
return false;
}
db_block.remove();
return true;
}
async suspend(workspace: string, flag: boolean): Promise<void> {
const db = await this.database.getDatabase(workspace);
db.suspend(flag);
}
async addColumn({
workspace,
blockId,
column,
}: AddColumnProps): Promise<boolean> {
const db_block = await this.getBlock(workspace, blockId);
if (!db_block) {
return false;
}
const columns = getOrInitBlockContentColumnsField(db_block);
if (!columns) {
return false;
}
addColumn({
block: db_block,
columns,
columnConfig: column,
});
return true;
}
async updateColumn({
workspace,
blockId,
columnId,
column,
}: UpdateColumnProps): Promise<boolean> {
const db_block = await this.getBlock(workspace, blockId);
if (!db_block) {
return false;
}
const columns = getOrInitBlockContentColumnsField(db_block);
if (!columns) {
return false;
}
const old_column = columns.find<MapOperation<string>>(col => {
// @ts-ignore TODO: don't know why
return col.get('id') === columnId;
});
if (!old_column) {
return false;
}
const column_config = {
// @ts-ignore TODO: don't know why
...deserializeColumnConfig(old_column?.get('config') as string),
...column,
};
old_column?.set(
'config',
// @ts-ignore TODO: don't know why
serializeColumnConfig(column_config as Column)
);
return true;
}
async removeColumn({
workspace,
blockId,
columnId,
}: RemoveColumnProps): Promise<boolean> {
const db_block = await this.getBlock(workspace, blockId);
if (!db_block) {
return false;
}
const columns = getOrInitBlockContentColumnsField(db_block);
if (columns?.length) {
// @ts-ignore TODO: don't know why
const idx = columns?.findIndex(col => col.get('id') === columnId);
if (idx > -1) {
columns.delete(idx, 1);
}
}
return true;
}
private async decorate_page_title(
page_block: BlockImplInstance,
prefix: string
) {
const text = page_block.getDecoration('text');
if (page_block && text) {
const new_text = JSON.parse(JSON.stringify(text));
//@ts-ignore
new_text.value[0].text = prefix + new_text.value[0].text;
page_block.setDecoration('text', new_text);
}
}
private async update_page_title(
pageBlock: BlockImplInstance,
title: string
) {
if (title) {
pageBlock.setDecoration('text', { value: [{ text: title }] });
}
}
async copyPage(
workspace_id: string,
source_page_id: string,
new_page_id: string
): Promise<boolean> {
const db = await this.database.getDatabase(workspace_id);
const source_page = await this.getBlock(
workspace_id,
source_page_id as 'block'
);
const new_page = await this.getBlock(
workspace_id,
new_page_id as 'block'
);
if (!source_page) {
return false;
}
const source_page_children = source_page.children;
const decorations = source_page.getDecorations();
Object.entries(decorations).forEach(([key, value]) => {
new_page?.setDecoration(key, source_page.getDecoration(key));
});
//@ts-ignore
this.decorate_page_title(new_page, 'copy from ');
for (let i = 0; i < source_page_children.length; i++) {
const source_page_child = await db.get(
source_page_children[i] as 'block'
);
new_page?.insertChildren(source_page_child);
}
return true;
}
async copyTemplateToPage(
workspace: string,
sourcePageId: string,
templateData: Template
) {
const db = await this.database.getDatabase(workspace);
const sourcePage = await this.getBlock(
workspace,
sourcePageId as 'block'
);
if (!sourcePage) {
return false;
}
if (templateData.properties && templateData.properties.text) {
this.update_page_title(
sourcePage,
templateData.properties.text?.value[0].text
);
}
this.update_block_properies(sourcePage, templateData.properties);
if (!templateData.blocks) return false;
for (let i = 0; i < templateData.blocks.length; i++) {
const blockData = templateData.blocks[i];
const sourcPageChild = await db.get(blockData.type as 'block');
this.update_block_properies(sourcPageChild, blockData.properties);
sourcePage?.insertChildren(sourcPageChild);
await this.copyTemplateToBlocks(
workspace,
sourcPageChild.id,
blockData
);
}
return true;
}
private update_block_properies(
block: BlockImplInstance,
properties: TemplateProperties
) {
Object.entries(properties).forEach(([key, value]) => {
block.setDecoration(key, value);
});
}
async copyTemplateToBlocks(
workspace: string,
parentBlockId: string,
template: Template
) {
const db = await this.database.getDatabase(workspace);
const parentBlock = await this.getBlock(
workspace,
parentBlockId as 'block'
);
if (!parentBlock) {
return false;
}
if (!template.blocks) return true;
for (let i = 0; i < template.blocks.length; i++) {
const blockData = template.blocks[i];
const sourcPageChild = await db.get(blockData.type as 'block');
this.update_block_properies(sourcPageChild, blockData.properties);
parentBlock?.insertChildren(sourcPageChild);
if (blockData.blocks) {
this.copyTemplateToBlocks(
workspace,
sourcPageChild.id,
blockData
);
}
}
return true;
}
async observe(
{ workspace, id }: DeleteEditorBlock,
callback: ObserveCallback
): Promise<ReturnUnobserve> {
return await this._observe(workspace, id, async (states, block) => {
callback(
dbBlock2BusinessBlock({
workspace,
dbBlock: block,
}) as ReturnEditorBlock
);
});
}
async unobserve({ workspace, id }: DeleteEditorBlock) {
await this._unobserve(workspace, id);
}
}
export type {
CreateEditorBlock,
ReturnEditorBlock,
GetEditorBlock,
UpdateEditorBlock,
DeleteEditorBlock,
BlockFlavors,
BlockFlavorKeys,
} from './types';
export type {
Column,
ContentColumn,
NumberColumn,
EnumColumn,
DateColumn,
BooleanColumn,
FileColumn,
DefaultColumnsValue,
ContentColumnValue,
NumberColumnValue,
EnumColumnValue,
BooleanColumnValue,
DateColumnValue,
FileColumnValue,
StringColumnValue,
} from './utils/column';
export {
ColumnType,
isBooleanColumn,
isContentColumn,
isDateColumn,
isFileColumn,
isNumberColumn,
isEnumColumn,
isStringColumn,
} from './utils/column';