mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
437 lines
13 KiB
TypeScript
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';
|