mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
init: the first public commit for AFFiNE
This commit is contained in:
12
libs/datasource/db-service/.babelrc
Normal file
12
libs/datasource/db-service/.babelrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
18
libs/datasource/db-service/.eslintrc.json
Normal file
18
libs/datasource/db-service/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
libs/datasource/db-service/README.md
Normal file
7
libs/datasource/db-service/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# datasource-db-service
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test datasource-db-service` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
9
libs/datasource/db-service/jest.config.js
Normal file
9
libs/datasource/db-service/jest.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
displayName: 'datasource-db-service',
|
||||
preset: '../../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../../coverage/libs/datasource/db-service',
|
||||
};
|
||||
12
libs/datasource/db-service/package.json
Normal file
12
libs/datasource/db-service/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@toeverything/datasource/db-service",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff": "^5.1.0",
|
||||
"nanoid": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.2"
|
||||
}
|
||||
}
|
||||
44
libs/datasource/db-service/project.json
Normal file
44
libs/datasource/db-service/project.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"sourceRoot": "libs/datasource/db-service/src",
|
||||
"projectType": "library",
|
||||
"tags": ["datasource:db-services"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/datasource/db-service",
|
||||
"tsConfig": "libs/datasource/db-service/tsconfig.lib.json",
|
||||
"project": "libs/datasource/db-service/package.json",
|
||||
"entryFile": "libs/datasource/db-service/src/index.ts",
|
||||
"external": ["react/jsx-runtime"],
|
||||
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||
"compiler": "babel",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "libs/datasource/db-service/README.md",
|
||||
"input": ".",
|
||||
"output": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"libs/datasource/db-service/**/*.{ts,tsx,js,jsx}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/datasource/db-service"],
|
||||
"options": {
|
||||
"jestConfig": "libs/datasource/db-service/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
libs/datasource/db-service/src/index.ts
Normal file
62
libs/datasource/db-service/src/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { diContainer, serviceMapByCallName } from './services';
|
||||
import type { DbServicesMap } from './services';
|
||||
export type { Template } from './services/editor-block/templates/types';
|
||||
export {
|
||||
TemplateFactory,
|
||||
type TemplateMeta,
|
||||
} from './services/editor-block/templates';
|
||||
|
||||
export type { ReturnUnobserve } from './services/database';
|
||||
export type { Comment, CommentReply } from './services/comment/types';
|
||||
|
||||
const api = new Proxy<DbServicesMap>({} as DbServicesMap, {
|
||||
get(target, prop) {
|
||||
const token = serviceMapByCallName[prop as string]?.token;
|
||||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
return diContainer.getDependency(token);
|
||||
},
|
||||
});
|
||||
|
||||
export const services = {
|
||||
api,
|
||||
};
|
||||
(window as any)['services'] = services;
|
||||
|
||||
export type {
|
||||
CreateEditorBlock,
|
||||
ReturnEditorBlock,
|
||||
GetEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
DeleteEditorBlock,
|
||||
BlockFlavors,
|
||||
BlockFlavorKeys,
|
||||
Column,
|
||||
ContentColumn,
|
||||
NumberColumn,
|
||||
EnumColumn,
|
||||
DateColumn,
|
||||
BooleanColumn,
|
||||
FileColumn,
|
||||
DefaultColumnsValue,
|
||||
ContentColumnValue,
|
||||
NumberColumnValue,
|
||||
EnumColumnValue,
|
||||
BooleanColumnValue,
|
||||
DateColumnValue,
|
||||
FileColumnValue,
|
||||
StringColumnValue,
|
||||
} from './services';
|
||||
export {
|
||||
ColumnType,
|
||||
isBooleanColumn,
|
||||
isContentColumn,
|
||||
isDateColumn,
|
||||
isFileColumn,
|
||||
isNumberColumn,
|
||||
isEnumColumn,
|
||||
isStringColumn,
|
||||
} from './services';
|
||||
export { Protocol } from './protocol';
|
||||
export { DEFAULT_COLUMN_KEYS } from './services/editor-block/utils/column/default-config';
|
||||
45
libs/datasource/db-service/src/protocol/index.ts
Normal file
45
libs/datasource/db-service/src/protocol/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
const Protocol = {
|
||||
Block: {
|
||||
Type: {
|
||||
workspace: 'workspace',
|
||||
page: 'page',
|
||||
group: 'group',
|
||||
title: 'title',
|
||||
text: 'text',
|
||||
heading1: 'heading1',
|
||||
heading2: 'heading2',
|
||||
heading3: 'heading3',
|
||||
code: 'code',
|
||||
todo: 'todo',
|
||||
comments: 'comments',
|
||||
tag: 'tag',
|
||||
reference: 'reference',
|
||||
image: 'image',
|
||||
file: 'file',
|
||||
audio: 'audio',
|
||||
video: 'video',
|
||||
shape: 'shape',
|
||||
quote: 'quote',
|
||||
toc: 'toc',
|
||||
database: 'database',
|
||||
whiteboard: 'whiteboard',
|
||||
template: 'template',
|
||||
discussion: 'discussion',
|
||||
comment: 'comment',
|
||||
activity: 'activity',
|
||||
bullet: 'bullet',
|
||||
numbered: 'numbered',
|
||||
toggle: 'toggle',
|
||||
callout: 'callout',
|
||||
divider: 'divider',
|
||||
groupDivider: 'groupDivider',
|
||||
youtube: 'youtube',
|
||||
figma: 'figma',
|
||||
embedLink: 'embedLink',
|
||||
grid: 'grid',
|
||||
gridItem: 'gridItem',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export { Protocol };
|
||||
168
libs/datasource/db-service/src/services/base.ts
Normal file
168
libs/datasource/db-service/src/services/base.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
BlockClientInstance,
|
||||
BlockInitOptions,
|
||||
BlockImplInstance,
|
||||
BlockMatcher,
|
||||
BlockContentExporter,
|
||||
QueryIndexMetadata,
|
||||
} from '@toeverything/datasource/jwt';
|
||||
import { DependencyCallOrConstructProps } from '@toeverything/utils';
|
||||
import { Database } from './database';
|
||||
import type { ObserveCallback, ReturnUnobserve } from './database/observer';
|
||||
|
||||
export abstract class ServiceBaseClass {
|
||||
protected database: Database;
|
||||
protected get_dependency: DependencyCallOrConstructProps['getDependency'];
|
||||
constructor(props: DependencyCallOrConstructProps) {
|
||||
this.get_dependency = props.getDependency;
|
||||
this.database = this.get_dependency(Database);
|
||||
}
|
||||
|
||||
async getWorkspaceDbBlock(workspace: string, options?: BlockInitOptions) {
|
||||
const db = await this.database.getDatabase(workspace, options);
|
||||
return db.getWorkspace();
|
||||
}
|
||||
|
||||
async onHistoryChange(
|
||||
workspace: string,
|
||||
name: string,
|
||||
callback: (meta: Map<string, any>) => void
|
||||
) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
db.history.onPush(name, callback);
|
||||
}
|
||||
|
||||
async onHistoryRevoke(
|
||||
workspace: string,
|
||||
name: string,
|
||||
callback: (meta: Map<string, any>) => void
|
||||
) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
db.history.onPop(name, callback);
|
||||
}
|
||||
|
||||
async undo(workspace: string) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
return db.history.undo();
|
||||
}
|
||||
|
||||
async redo(workspace: string) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
return db.history.redo();
|
||||
}
|
||||
|
||||
async search(
|
||||
workspace: string,
|
||||
query: Parameters<BlockClientInstance['searchPages']>[0]
|
||||
) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
return db.searchPages(query);
|
||||
}
|
||||
|
||||
async query(
|
||||
workspace: string,
|
||||
query: QueryIndexMetadata
|
||||
): Promise<null | any[]> {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
return db.query(query);
|
||||
}
|
||||
|
||||
async clearUndoRedo(workspace: string) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
return db.history.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block, unlike db.get, if the id does not exist, it will return undefined
|
||||
* @param workspace
|
||||
* @param blockId
|
||||
* @returns
|
||||
*/
|
||||
async getBlock(
|
||||
workspace: string,
|
||||
blockId: string
|
||||
): Promise<BlockImplInstance | undefined> {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const db_block = await db.get(blockId as 'block');
|
||||
if (db_block.id !== blockId) {
|
||||
return undefined;
|
||||
}
|
||||
return db_block;
|
||||
}
|
||||
|
||||
async registerContentExporter(
|
||||
workspace: string,
|
||||
name: string,
|
||||
matcher: BlockMatcher,
|
||||
exporter: BlockContentExporter
|
||||
) {
|
||||
await this.database.registerContentExporter(
|
||||
workspace,
|
||||
name,
|
||||
matcher,
|
||||
exporter
|
||||
);
|
||||
}
|
||||
|
||||
async unregisterContentExporter(workspace: string, name: string) {
|
||||
await this.database.unregisterContentExporter(workspace, name);
|
||||
}
|
||||
|
||||
async registerMetadataExporter(
|
||||
workspace: string,
|
||||
name: string,
|
||||
matcher: BlockMatcher,
|
||||
exporter: BlockContentExporter<
|
||||
Array<[string, number | string | string[]]>
|
||||
>
|
||||
) {
|
||||
await this.database.registerMetadataExporter(
|
||||
workspace,
|
||||
name,
|
||||
matcher,
|
||||
exporter
|
||||
);
|
||||
}
|
||||
|
||||
async unregisterMetadataExporter(workspace: string, name: string) {
|
||||
await this.database.unregisterMetadataExporter(workspace, name);
|
||||
}
|
||||
|
||||
async registerTagExporter(
|
||||
workspace: string,
|
||||
name: string,
|
||||
matcher: BlockMatcher,
|
||||
exporter: BlockContentExporter<string[]>
|
||||
) {
|
||||
await this.database.registerTagExporter(
|
||||
workspace,
|
||||
name,
|
||||
matcher,
|
||||
exporter
|
||||
);
|
||||
}
|
||||
|
||||
async unregisterTagExporter(workspace: string, name: string) {
|
||||
await this.database.unregisterTagExporter(workspace, name);
|
||||
}
|
||||
|
||||
protected async _observe(
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
callback: ObserveCallback
|
||||
): Promise<ReturnUnobserve> {
|
||||
return await this.database.observe(workspace, blockId, async states => {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const new_block = await db.get(blockId as 'block');
|
||||
callback(states, new_block);
|
||||
});
|
||||
}
|
||||
|
||||
protected async _unobserve(
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
callback?: ObserveCallback
|
||||
) {
|
||||
return await this.database.unobserve(workspace, blockId, callback);
|
||||
}
|
||||
}
|
||||
315
libs/datasource/db-service/src/services/comment/comment.ts
Normal file
315
libs/datasource/db-service/src/services/comment/comment.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import { DependencyCallOrConstructProps } from '@toeverything/utils';
|
||||
import type { ReturnUnobserve } from '../database/observer';
|
||||
import {
|
||||
DeleteEditorBlock,
|
||||
GetEditorBlock,
|
||||
ReturnEditorBlock,
|
||||
} from '../editor-block/types';
|
||||
import { EditorBlock, ObserveCallback } from '../editor-block';
|
||||
import {
|
||||
CommentReply,
|
||||
CreateCommentBlock,
|
||||
CreateReplyBlock,
|
||||
UpdateCommentBlock,
|
||||
UpdateReplyBlock,
|
||||
GetCommentsBlock,
|
||||
Comment,
|
||||
} from './types';
|
||||
import { DefaultColumnsValue } from './../index';
|
||||
import { CommentColumnValue } from '../editor-block/utils/column/types';
|
||||
import { WORKSPACE_COMMENTS } from '../../utils';
|
||||
|
||||
export class CommentService {
|
||||
protected editor_block: EditorBlock;
|
||||
protected get_dependency: DependencyCallOrConstructProps['getDependency'];
|
||||
constructor(props: DependencyCallOrConstructProps) {
|
||||
this.get_dependency = props.getDependency;
|
||||
this.editor_block = this.get_dependency(EditorBlock);
|
||||
}
|
||||
|
||||
async createComment({
|
||||
workspace,
|
||||
pageId,
|
||||
attachedToBlocksIds,
|
||||
quote,
|
||||
content,
|
||||
}: CreateCommentBlock): Promise<{ commentsId: string } | undefined> {
|
||||
const rootCommentId = await this.get_root_comment_id({
|
||||
workspace,
|
||||
pageId,
|
||||
});
|
||||
const discussionBlock = await this.editor_block.create({
|
||||
workspace: workspace,
|
||||
type: 'comments',
|
||||
});
|
||||
|
||||
if (!discussionBlock) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parentBlock = await this.editor_block.get({
|
||||
workspace,
|
||||
ids: [rootCommentId],
|
||||
});
|
||||
|
||||
if (parentBlock.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const children = parentBlock[0]?.children || [];
|
||||
children.push(discussionBlock.id);
|
||||
const success = await this.editor_block.update({
|
||||
id: rootCommentId,
|
||||
workspace: workspace,
|
||||
children: children,
|
||||
});
|
||||
if (!success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let result = false;
|
||||
result = await this.updateComment({
|
||||
workspace: workspace,
|
||||
id: discussionBlock.id,
|
||||
attachedToBlocksIds: attachedToBlocksIds || [],
|
||||
quote: quote,
|
||||
pageId: pageId,
|
||||
});
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const reply = await this.createReply({
|
||||
workspace: workspace,
|
||||
parentId: discussionBlock.id,
|
||||
content: content,
|
||||
});
|
||||
|
||||
return { commentsId: discussionBlock.id };
|
||||
}
|
||||
|
||||
async createReply({
|
||||
workspace,
|
||||
parentId,
|
||||
content,
|
||||
}: CreateReplyBlock): Promise<boolean> {
|
||||
const reply_block = await this.editor_block.create({
|
||||
workspace: workspace,
|
||||
type: 'comment',
|
||||
});
|
||||
if (!reply_block) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent_block = await this.editor_block.get({
|
||||
workspace,
|
||||
ids: [parentId],
|
||||
});
|
||||
|
||||
if (parent_block.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const children = parent_block[0]?.children || [];
|
||||
children.push(reply_block.id);
|
||||
const success = await this.editor_block.update({
|
||||
id: parentId,
|
||||
workspace: workspace,
|
||||
children: children,
|
||||
});
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this.updateReply({
|
||||
workspace,
|
||||
id: reply_block.id,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
async updateComment({
|
||||
workspace,
|
||||
id,
|
||||
pageId,
|
||||
attachedToBlocksIds,
|
||||
quote,
|
||||
resolve,
|
||||
}: UpdateCommentBlock): Promise<boolean> {
|
||||
const properties: Partial<DefaultColumnsValue> = {};
|
||||
if (quote !== undefined) {
|
||||
properties.text = quote;
|
||||
}
|
||||
const blocks = await this.editor_block.get({
|
||||
workspace,
|
||||
ids: [id],
|
||||
});
|
||||
if (blocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const commentValue: CommentColumnValue = {} as CommentColumnValue;
|
||||
Object.assign(commentValue, blocks[0]?.properties?.comment);
|
||||
if (pageId !== undefined) {
|
||||
commentValue.pageId = pageId;
|
||||
}
|
||||
if (attachedToBlocksIds !== undefined) {
|
||||
commentValue.attachedToBlocksIds = attachedToBlocksIds;
|
||||
}
|
||||
if (resolve !== undefined) {
|
||||
commentValue.resolve = resolve;
|
||||
commentValue.finishTime = resolve ? Date.now() : undefined;
|
||||
commentValue.resolveUserId = resolve
|
||||
? await this.editor_block.getUserId(workspace)
|
||||
: undefined;
|
||||
}
|
||||
properties.comment = commentValue;
|
||||
return await this.editor_block.update({
|
||||
id: id,
|
||||
workspace: workspace,
|
||||
properties: properties,
|
||||
});
|
||||
}
|
||||
|
||||
async updateReply({
|
||||
workspace,
|
||||
id,
|
||||
content,
|
||||
}: UpdateReplyBlock): Promise<boolean> {
|
||||
return await this.editor_block.update({
|
||||
id: id,
|
||||
workspace: workspace,
|
||||
properties: {
|
||||
text: content,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete({ workspace, id }: DeleteEditorBlock): Promise<boolean> {
|
||||
return await this.editor_block.delete({
|
||||
workspace: workspace,
|
||||
id: id,
|
||||
});
|
||||
}
|
||||
|
||||
async getPageComments({
|
||||
workspace,
|
||||
pageId,
|
||||
}: GetCommentsBlock): Promise<Comment | null> {
|
||||
const root_comment_id = await this.get_root_comment_id({
|
||||
workspace,
|
||||
pageId,
|
||||
});
|
||||
const comments = await this.getComments({
|
||||
workspace,
|
||||
ids: [root_comment_id],
|
||||
});
|
||||
return comments.length > 0 ? comments[0] : null;
|
||||
}
|
||||
|
||||
async getComments({
|
||||
workspace,
|
||||
ids,
|
||||
}: GetEditorBlock): Promise<Array<Comment>> {
|
||||
const blocks = (await this.get({ workspace, ids })).filter(block => {
|
||||
return (
|
||||
block &&
|
||||
block?.type === 'comments' &&
|
||||
!block.properties?.comment?.resolve
|
||||
);
|
||||
}) as ReturnEditorBlock[];
|
||||
const comments = blocks.map(block => {
|
||||
return {
|
||||
id: block.id,
|
||||
workspace: block.workspace,
|
||||
type: block.type,
|
||||
parentId: block.parentId,
|
||||
attachedToBlocksIds:
|
||||
block.properties?.comment?.attachedToBlocksIds || [],
|
||||
children: block.children,
|
||||
quote: block.properties?.text || [],
|
||||
resolve: block.properties?.comment?.resolve || false,
|
||||
resolveUserId: block.properties?.comment?.resolveUserId || '',
|
||||
created: block.created,
|
||||
lastUpdated: block.lastUpdated,
|
||||
creator: block.creator,
|
||||
} as Comment;
|
||||
});
|
||||
return comments;
|
||||
}
|
||||
|
||||
async getReplyList({
|
||||
workspace,
|
||||
ids,
|
||||
}: GetEditorBlock): Promise<Array<CommentReply>> {
|
||||
const blocks = (await this.editor_block.get({ workspace, ids })).filter(
|
||||
block => block?.type === 'comment'
|
||||
) as ReturnEditorBlock[];
|
||||
|
||||
const replyList = blocks.map(block => {
|
||||
return {
|
||||
id: block.id,
|
||||
workspace: block.workspace,
|
||||
type: block.type,
|
||||
parentId: block.parentId,
|
||||
children: block.children,
|
||||
content: block.properties?.text || [],
|
||||
created: block.created,
|
||||
lastUpdated: block.lastUpdated,
|
||||
creator: block.creator,
|
||||
} as CommentReply;
|
||||
});
|
||||
return replyList;
|
||||
}
|
||||
|
||||
async get({
|
||||
workspace,
|
||||
ids,
|
||||
}: GetEditorBlock): Promise<Array<ReturnEditorBlock | null>> {
|
||||
const blocks = await this.editor_block.get({
|
||||
workspace,
|
||||
ids,
|
||||
});
|
||||
return blocks;
|
||||
}
|
||||
|
||||
async observe(
|
||||
{ workspace, id }: DeleteEditorBlock,
|
||||
callback: ObserveCallback
|
||||
): Promise<ReturnUnobserve> {
|
||||
return await this.editor_block.observe({ workspace, id }, callback);
|
||||
}
|
||||
|
||||
async unobserve({ workspace, id }: DeleteEditorBlock) {
|
||||
await this.editor_block.unobserve({ workspace, id });
|
||||
}
|
||||
|
||||
private async get_root_comment_id({
|
||||
workspace,
|
||||
pageId,
|
||||
}: GetCommentsBlock): Promise<string> {
|
||||
const workspace_db_block = await this.editor_block.getWorkspaceDbBlock(
|
||||
workspace
|
||||
);
|
||||
const workspace_comments: any[] =
|
||||
workspace_db_block.getDecoration(WORKSPACE_COMMENTS) || [];
|
||||
let root_comment = workspace_comments.find(
|
||||
item => item.pageId === pageId
|
||||
);
|
||||
if (!root_comment) {
|
||||
const discussion_block = await this.editor_block.create({
|
||||
workspace: workspace,
|
||||
type: 'comments',
|
||||
});
|
||||
root_comment = {
|
||||
pageId: pageId,
|
||||
rootCommentId: discussion_block.id,
|
||||
};
|
||||
workspace_comments.push(root_comment);
|
||||
workspace_db_block.setDecoration(
|
||||
WORKSPACE_COMMENTS,
|
||||
workspace_comments
|
||||
);
|
||||
}
|
||||
return root_comment.rootCommentId;
|
||||
}
|
||||
}
|
||||
2
libs/datasource/db-service/src/services/comment/index.ts
Normal file
2
libs/datasource/db-service/src/services/comment/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './comment';
|
||||
export * from './types';
|
||||
64
libs/datasource/db-service/src/services/comment/types.ts
Normal file
64
libs/datasource/db-service/src/services/comment/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ContentColumnValue, BlockFlavorKeys } from '../index';
|
||||
export interface CommentReply {
|
||||
id: string;
|
||||
workspace: string;
|
||||
type: BlockFlavorKeys;
|
||||
parentId?: string;
|
||||
children: string[];
|
||||
content: ContentColumnValue;
|
||||
created: number;
|
||||
lastUpdated: number;
|
||||
creator?: string;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
workspace: string;
|
||||
type: BlockFlavorKeys;
|
||||
parentId?: string;
|
||||
/** store the block-ids where comment is on,
|
||||
* useful when comment is not page level but on a specific block in page */
|
||||
attachedToBlocksIds?: string[];
|
||||
children: string[];
|
||||
quote: ContentColumnValue;
|
||||
resolve: boolean;
|
||||
resolveUserId?: string;
|
||||
created: number;
|
||||
lastUpdated: number;
|
||||
creator?: string;
|
||||
}
|
||||
|
||||
export interface CreateCommentBlock {
|
||||
workspace: string;
|
||||
pageId: string;
|
||||
attachedToBlocksIds?: string[];
|
||||
quote: ContentColumnValue;
|
||||
content: ContentColumnValue;
|
||||
}
|
||||
|
||||
export interface CreateReplyBlock {
|
||||
workspace: string;
|
||||
parentId: string;
|
||||
content: ContentColumnValue;
|
||||
}
|
||||
|
||||
export interface UpdateCommentBlock {
|
||||
workspace: string;
|
||||
id: string;
|
||||
pageId?: string;
|
||||
attachedToBlocksIds?: string[];
|
||||
quote?: ContentColumnValue;
|
||||
resolve?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateReplyBlock {
|
||||
workspace: string;
|
||||
id: string;
|
||||
content: ContentColumnValue;
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface GetCommentsBlock {
|
||||
workspace: string;
|
||||
pageId: string;
|
||||
}
|
||||
180
libs/datasource/db-service/src/services/database/index.ts
Normal file
180
libs/datasource/db-service/src/services/database/index.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { getAuth, type User } from 'firebase/auth';
|
||||
|
||||
import {
|
||||
BlockClient,
|
||||
BlockClientInstance,
|
||||
BlockContentExporter,
|
||||
BlockMatcher,
|
||||
BlockInitOptions,
|
||||
} from '@toeverything/datasource/jwt';
|
||||
import { sleep } from '@toeverything/utils';
|
||||
|
||||
import { ObserverManager, getObserverName } from './observer';
|
||||
import type { ObserveCallback, ReturnUnobserve } from './observer';
|
||||
export type { ObserveCallback, ReturnUnobserve } from './observer';
|
||||
|
||||
const workspaces: Record<string, BlockClientInstance> = {};
|
||||
|
||||
const loading = new Set();
|
||||
|
||||
const waitLoading = async (key: string) => {
|
||||
while (loading.has(key)) {
|
||||
await sleep();
|
||||
}
|
||||
};
|
||||
|
||||
async function _getCurrentToken() {
|
||||
if (process.env['NX_FREE_LOGIN']) {
|
||||
return 'NX_FREE_LOGIN';
|
||||
}
|
||||
const token = await getAuth().currentUser?.getIdToken();
|
||||
if (token) return token;
|
||||
return new Promise<string>(resolve => {
|
||||
getAuth().onIdTokenChanged((user: User | null) => {
|
||||
if (user) resolve(user.getIdToken());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function _getBlockDatabase(
|
||||
workspace: string,
|
||||
options?: BlockInitOptions
|
||||
) {
|
||||
if (loading.has(workspace)) {
|
||||
await waitLoading(workspace);
|
||||
}
|
||||
|
||||
// if (
|
||||
// options?.userId &&
|
||||
// workspaces[workspace]?.getUserId() !== options?.userId
|
||||
// ) {
|
||||
// delete workspaces[workspace];
|
||||
// }
|
||||
|
||||
if (!workspaces[workspace]) {
|
||||
loading.add(workspace);
|
||||
|
||||
workspaces[workspace] = await BlockClient.init(workspace, {
|
||||
...options,
|
||||
token: await _getCurrentToken(),
|
||||
});
|
||||
(window as any).client = workspaces[workspace];
|
||||
await workspaces[workspace].buildIndex();
|
||||
loading.delete(workspace);
|
||||
}
|
||||
return workspaces[workspace];
|
||||
}
|
||||
|
||||
interface DatabaseProps {
|
||||
options?: BlockInitOptions;
|
||||
}
|
||||
|
||||
export class Database {
|
||||
readonly #observers = new ObserverManager();
|
||||
|
||||
readonly #options?: BlockInitOptions;
|
||||
constructor(props: DatabaseProps) {
|
||||
this.#options = props.options;
|
||||
}
|
||||
|
||||
async getDatabase(workspace: string, options?: BlockInitOptions) {
|
||||
const db = await _getBlockDatabase(workspace, options);
|
||||
return db;
|
||||
}
|
||||
|
||||
async registerContentExporter(
|
||||
workspace: string,
|
||||
name: string,
|
||||
matcher: BlockMatcher,
|
||||
exporter: BlockContentExporter
|
||||
) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
db.registerContentExporter(name, matcher, exporter);
|
||||
}
|
||||
|
||||
async unregisterContentExporter(workspace: string, name: string) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
db.unregisterContentExporter(name);
|
||||
}
|
||||
|
||||
async registerMetadataExporter(
|
||||
workspace: string,
|
||||
name: string,
|
||||
matcher: BlockMatcher,
|
||||
exporter: BlockContentExporter<
|
||||
Array<[string, number | string | string[]]>
|
||||
>
|
||||
) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
db.registerMetadataExporter(name, matcher, exporter);
|
||||
}
|
||||
|
||||
async unregisterMetadataExporter(workspace: string, name: string) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
db.unregisterMetadataExporter(name);
|
||||
}
|
||||
|
||||
async registerTagExporter(
|
||||
workspace: string,
|
||||
name: string,
|
||||
matcher: BlockMatcher,
|
||||
exporter: BlockContentExporter<string[]>
|
||||
) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
db.registerTagExporter(name, matcher, exporter);
|
||||
}
|
||||
|
||||
async unregisterTagExporter(workspace: string, name: string) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
db.unregisterTagExporter(name);
|
||||
}
|
||||
|
||||
async observe(
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
callback: ObserveCallback
|
||||
): Promise<ReturnUnobserve> {
|
||||
const observer_name = getObserverName(workspace, blockId);
|
||||
const unobserve = this.#observers.addCallback(observer_name, callback);
|
||||
if (this.#observers.getStatus(observer_name) === 'observing') {
|
||||
return unobserve;
|
||||
}
|
||||
const db = await this.getDatabase(workspace, this.#options);
|
||||
const block = await db.get(blockId as 'block');
|
||||
if (block) {
|
||||
const listener: Parameters<
|
||||
typeof block['on']
|
||||
>[2] = async states => {
|
||||
const new_block = await db.get(blockId as 'block');
|
||||
this.#observers.getCallbacks(observer_name).forEach(cb => {
|
||||
cb(states, new_block);
|
||||
});
|
||||
};
|
||||
|
||||
block.on('children', observer_name, listener);
|
||||
block.on('content', observer_name, listener);
|
||||
block.on('parent', observer_name, listener);
|
||||
}
|
||||
this.#observers.setStatus(observer_name, 'observing');
|
||||
|
||||
return unobserve;
|
||||
}
|
||||
|
||||
async unobserve(
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
callback?: ObserveCallback
|
||||
) {
|
||||
const observer_name = getObserverName(workspace, blockId);
|
||||
this.#observers.removeCallback(observer_name, callback);
|
||||
if (!this.#observers.getCallbacks(observer_name).length) {
|
||||
const db = await this.getDatabase(workspace, this.#options);
|
||||
const block = await db.get(blockId as 'block');
|
||||
if (block) {
|
||||
block.off('children', observer_name);
|
||||
block.off('content', observer_name);
|
||||
block.off('parent', observer_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
libs/datasource/db-service/src/services/database/observer.ts
Normal file
69
libs/datasource/db-service/src/services/database/observer.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ChangedStates, BlockImplInstance } from '@toeverything/datasource/jwt';
|
||||
|
||||
export type ObserveCallback = (
|
||||
changeStates: ChangedStates,
|
||||
block: BlockImplInstance
|
||||
) => void;
|
||||
export type ReturnUnobserve = () => void;
|
||||
|
||||
type ObserverStatus = {
|
||||
status: 'observing' | 'removing' | 'none';
|
||||
callbacks: ObserveCallback[];
|
||||
};
|
||||
|
||||
export class ObserverManager {
|
||||
private observe_callbacks: Record<string, ObserverStatus | undefined> = {};
|
||||
addCallback(key: string, callback: ObserveCallback) {
|
||||
if (!this.observe_callbacks[key]) {
|
||||
this.observe_callbacks[key] = {
|
||||
status: 'none',
|
||||
callbacks: [],
|
||||
};
|
||||
}
|
||||
|
||||
const observer = this.observe_callbacks[key] as ObserverStatus;
|
||||
observer.callbacks.push(callback);
|
||||
return () => {
|
||||
const index = observer.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
observer.callbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
removeCallback(key: string, callback?: ObserveCallback) {
|
||||
const observer = this.observe_callbacks[key];
|
||||
if (!observer) {
|
||||
return;
|
||||
}
|
||||
if (callback) {
|
||||
const index = observer.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
observer.callbacks.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
observer.callbacks = [];
|
||||
}
|
||||
}
|
||||
getCallbacks(key: string) {
|
||||
return this.observe_callbacks[key]?.callbacks || [];
|
||||
}
|
||||
getStatus(key: string) {
|
||||
return this.observe_callbacks[key]?.status || 'none';
|
||||
}
|
||||
setStatus(key: string, status: 'observing' | 'removing' | 'none') {
|
||||
if (!this.observe_callbacks[key]) {
|
||||
this.observe_callbacks[key] = {
|
||||
status: 'none',
|
||||
callbacks: [],
|
||||
};
|
||||
}
|
||||
(this.observe_callbacks[key] as ObserverStatus).status = status;
|
||||
}
|
||||
removeObserve(key: string) {
|
||||
this.observe_callbacks[key] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getObserverName(workspace: string, blockId: string) {
|
||||
return `${workspace}_${blockId}`;
|
||||
}
|
||||
431
libs/datasource/db-service/src/services/editor-block/index.ts
Normal file
431
libs/datasource/db-service/src/services/editor-block/index.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
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 (decorations[key] !== value) {
|
||||
db_block.setDecoration(key, value);
|
||||
}
|
||||
}
|
||||
);
|
||||
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';
|
||||
@@ -0,0 +1,604 @@
|
||||
//@ts-nocheck
|
||||
import { GroupTemplate } from './types';
|
||||
type GroupTemplateMap = Record<GroupTemplateKeys, GroupTemplate>;
|
||||
const groupTemplateMap: GroupTemplateMap = {
|
||||
empty: {
|
||||
type: 'group',
|
||||
properties: {},
|
||||
blocks: [
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ text: '' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
todolist: {
|
||||
type: 'group',
|
||||
properties: {},
|
||||
blocks: [
|
||||
{
|
||||
type: 'heading1',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ text: '🎓Graduating from the project' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Congratulations! Now you can start create your own projects!',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'To start using workspaces and folders hit the ',
|
||||
},
|
||||
{
|
||||
bold: true,
|
||||
text: 'button at the top left of the screen',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
text: 'At any time if you feel lost do visit our 📚Blog: ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
url: 'https://blog.affine.pro/',
|
||||
id: 'link.qx4yhw81or54',
|
||||
children: [
|
||||
{
|
||||
text: 'https://blog.affine.pro',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
collapsed: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
text: 'If you have any suggestions drop a post in our Reddit Channel:',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
url: 'https://www.reddit.com/r/Affine/',
|
||||
id: 'link.zeafc4ogfvrb',
|
||||
children: [
|
||||
{
|
||||
text: 'https://www.reddit.com/r/Affine/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: ' ',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading2',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: '🎉 The Essentials. Check things off after you tried them!',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{ text: ' ✅ ' },
|
||||
{ bold: true, text: 'Check' },
|
||||
{
|
||||
text: ' the text box here to complete the task!',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{ text: '' },
|
||||
{ text: '' },
|
||||
{ text: ' 👋 ' },
|
||||
{ bold: true, text: 'Drag' },
|
||||
{
|
||||
text: ' the ⠟ button left of the checkbox to reorder tasks',
|
||||
},
|
||||
{ text: '' },
|
||||
{ text: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{ text: '' },
|
||||
{ text: '' },
|
||||
{ text: ' ➡️ ' },
|
||||
{ bold: true, text: 'Fold' },
|
||||
{ text: ' and ' },
|
||||
{ bold: true, text: 'Unfold' },
|
||||
{
|
||||
text: ' a task to simplify your list using the arrow on the right ⤵️',
|
||||
},
|
||||
],
|
||||
},
|
||||
numberType: 'type1',
|
||||
collapsed: { value: false },
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: 'figma',
|
||||
properties: {
|
||||
embedLink: {
|
||||
value: 'https://www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2F7pyx5gMz6CN0qSRADmScQ7%2FAFFINE%3Fnode-id%3D40%253A2',
|
||||
name: 'figma',
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
blog: {
|
||||
type: 'group',
|
||||
properties: {},
|
||||
blocks: [
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'As a collaborative real-time editor, Affine aims to resolve problems in three situations:',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'bullet',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Multi-master replication: the synchronization of data between equipment and applications;',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'bullet',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Eventual consistency: the consistence of data regardless of network latency and outage;',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'bullet',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Conflict resolution: the resolution of conflict between simultaneous edits.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'To achieve these aims, a proper collaborative algorithm should be used. There are hundreds of collaborative algorithms being invented over the past three decades, but they usually fall into two categories: either operational transformation (OT) or conflict-free replicated data type (CRDT). We think CRDT is a better choice.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading1',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ bold: true, text: 'What does CRDT do' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'CRDT is capable of discovering and resolving conflicts while ensuring the effective distribution and merge of date. It may sound like magic, but think about historical study, it is very possible to discover the truth from scattered evidence.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'For CRDT, every piece of data is like a "historic fragment". We keep collecting the fragments from other clients and then restore the truth by excluding repeated data and correcting false information.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading1',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ bold: true, text: 'Why CRDT is better' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'In contrast to OT, CRDT possesses three big advantages:',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading2',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ bold: true, text: 'Flexibility' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'CRDT supports more data types. For example, Yjs supports Array, Map, and Treelike, and therefore applies to more business scenarios.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading2',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ bold: true, text: 'Performance' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'CRDT tolerates higher latency and can wait longer for solving conflicts, whereas the calculation of OT in the same condition may become too overwhelming for a server to sustain.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading2',
|
||||
properties: {
|
||||
text: {
|
||||
value: [{ bold: true, text: 'Extensibility' }],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Because CRDT supports more data types and editor elements, it is more extensible.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading1',
|
||||
properties: {
|
||||
text: { value: [{ text: 'Conclusion' }] },
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: { text: { value: [{ text: '' }] } },
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{ text: '' },
|
||||
{
|
||||
type: 'link',
|
||||
url: 'https://blog.affine.pro/',
|
||||
id: 'link.stubssslo0rq',
|
||||
children: [{ text: '← View all posts' }],
|
||||
},
|
||||
{ text: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
grid: {
|
||||
type: 'group',
|
||||
properties: {},
|
||||
blocks: [
|
||||
{
|
||||
type: 'heading2',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
bold: true,
|
||||
text: 'Performance',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'CRDT tolerates higher latency and can wait longer for solving conflicts, whereas the calculation of OT in the same condition may become too overwhelming for a server to sustain.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading2',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
bold: true,
|
||||
text: 'Extensibility',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Because CRDT supports more data types and editor elements, it is more extensible.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'heading1',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Conclusion',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'grid',
|
||||
properties: {},
|
||||
blocks: [
|
||||
{
|
||||
type: 'gridItem',
|
||||
properties: {
|
||||
gridItemWidth: '50%',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'gridItem',
|
||||
properties: {
|
||||
gridItemWidth: '50%',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: 'Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: {
|
||||
value: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
url: 'https://blog.affine.pro/',
|
||||
id: 'link.stubssslo0rq',
|
||||
children: [
|
||||
{
|
||||
text: '← View all posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export type GroupTemplateKeys = 'todolist' | 'blog' | 'empty' | 'grid';
|
||||
export { groupTemplateMap };
|
||||
@@ -0,0 +1,3 @@
|
||||
export { TemplateFactory } from './template-factory';
|
||||
|
||||
export * from './types';
|
||||
@@ -0,0 +1,40 @@
|
||||
import { groupTemplateMap } from './group-templates';
|
||||
import { Template, TemplateMeta } from './types';
|
||||
const defaultTemplateList: Array<TemplateMeta> = [
|
||||
{
|
||||
name: 'New From Quick Start',
|
||||
groupKeys: ['todolist'],
|
||||
},
|
||||
{ name: 'New From Grid System', groupKeys: ['grid'] },
|
||||
{ name: 'New From Blog', groupKeys: ['blog'] },
|
||||
{ name: ' New Todolist', groupKeys: ['todolist'] },
|
||||
{ name: ' New Empty Page', groupKeys: ['empty'] },
|
||||
];
|
||||
const TemplateFactory = {
|
||||
defaultTemplateList: defaultTemplateList,
|
||||
generatePageTemplateByGroupKeys(props: TemplateMeta): Template {
|
||||
const newTitle = props.name || 'Get Started with Affine';
|
||||
const keys = props.groupKeys || [];
|
||||
const blankPage: Template = {
|
||||
type: 'page',
|
||||
properties: {
|
||||
text: { value: [{ text: newTitle }] },
|
||||
fullWidthChecked: false,
|
||||
},
|
||||
blocks: [],
|
||||
};
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key in groupTemplateMap) {
|
||||
blankPage.blocks = blankPage.blocks || [];
|
||||
if (groupTemplateMap[key]) {
|
||||
blankPage.blocks.push(groupTemplateMap[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blankPage;
|
||||
},
|
||||
};
|
||||
|
||||
export { TemplateFactory };
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DefaultColumnsValue, BlockFlavorKeys } from './../index';
|
||||
import { groupTemplateMap, GroupTemplateKeys } from './group-templates';
|
||||
|
||||
// interface Block {
|
||||
// type: BlockFlavorKeys;
|
||||
// properties: Partial<DefaultColumnsValue>;
|
||||
// }
|
||||
export type TemplateProperties = Partial<DefaultColumnsValue>;
|
||||
export interface Template {
|
||||
type: BlockFlavorKeys;
|
||||
properties: TemplateProperties;
|
||||
blocks?: Template[];
|
||||
}
|
||||
|
||||
export interface GroupTemplate {
|
||||
type: BlockFlavorKeys;
|
||||
properties: TemplateProperties;
|
||||
blocks?: Template[];
|
||||
}
|
||||
export interface TemplateMeta {
|
||||
name: string | null;
|
||||
groupKeys: Array<GroupTemplateKeys> | [];
|
||||
}
|
||||
|
||||
// export { Template, TemplateProperties };
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Protocol } from '../../protocol';
|
||||
import { Column, DefaultColumnsValue } from './utils/column';
|
||||
|
||||
export type BlockFlavors = typeof Protocol.Block.Type;
|
||||
export type BlockFlavorKeys = keyof typeof Protocol.Block.Type;
|
||||
|
||||
export interface CreateEditorBlock {
|
||||
workspace: string;
|
||||
type: keyof BlockFlavors;
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface ReturnEditorBlock {
|
||||
id: string;
|
||||
workspace: string;
|
||||
type: BlockFlavorKeys;
|
||||
parentId?: string;
|
||||
pageId?: string;
|
||||
closestGroupId?: string;
|
||||
columns?: Column[];
|
||||
children: string[];
|
||||
properties?: Partial<DefaultColumnsValue>;
|
||||
created: number;
|
||||
lastUpdated: number;
|
||||
creator?: string;
|
||||
}
|
||||
|
||||
export interface GetEditorBlock {
|
||||
ids: string[];
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export interface UpdateEditorBlock
|
||||
extends Partial<
|
||||
Pick<ReturnEditorBlock, 'type' | 'parentId' | 'children' | 'properties'>
|
||||
> {
|
||||
id: string;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export interface DeleteEditorBlock {
|
||||
id: string;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export interface AddColumnProps {
|
||||
workspace: string;
|
||||
/**
|
||||
* block id
|
||||
* Support group, page block setting columns
|
||||
*/
|
||||
blockId: string;
|
||||
column: Column;
|
||||
}
|
||||
|
||||
export interface UpdateColumnProps {
|
||||
workspace: string;
|
||||
/**
|
||||
* block id
|
||||
* Support group, page block setting columns
|
||||
*/
|
||||
blockId: string;
|
||||
columnId: string;
|
||||
column: Partial<Column>;
|
||||
}
|
||||
|
||||
export interface RemoveColumnProps {
|
||||
workspace: string;
|
||||
/**
|
||||
* block id
|
||||
* Support group, page block setting columns
|
||||
*/
|
||||
blockId: string;
|
||||
columnId: string;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
ContentColumnValue,
|
||||
BooleanColumnValue,
|
||||
StringColumnValue,
|
||||
FileColumnValue,
|
||||
DateColumnValue,
|
||||
EnumColumnValue,
|
||||
CommentColumnValue,
|
||||
FilterConstraint,
|
||||
SorterConstraint,
|
||||
} from './types';
|
||||
|
||||
export enum GroupScene {
|
||||
page = 'page',
|
||||
table = 'table',
|
||||
kanban = 'kanban',
|
||||
whiteboard = 'whiteboard',
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export enum BlockStatus {
|
||||
notStart = 'notStart',
|
||||
progress = 'progress',
|
||||
done = 'done',
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export type DefaultColumnsValue = {
|
||||
scene: string;
|
||||
visibleColumnKeys: EnumColumnValue;
|
||||
shapeProps: StringColumnValue;
|
||||
text: ContentColumnValue;
|
||||
textStyle: Record<'textAlign', string>;
|
||||
checked: BooleanColumnValue;
|
||||
collapsed: BooleanColumnValue;
|
||||
embedLink: StringColumnValue;
|
||||
image: FileColumnValue;
|
||||
file: FileColumnValue;
|
||||
endDate: DateColumnValue;
|
||||
status: EnumColumnValue<BlockStatus>;
|
||||
gridItemWidth: string;
|
||||
reference: string;
|
||||
numberType: any;
|
||||
image_style: any;
|
||||
lang: any;
|
||||
fullWidthChecked: boolean;
|
||||
comment: CommentColumnValue;
|
||||
filterConstraint: FilterConstraint;
|
||||
filterWeakSqlConstraint: string;
|
||||
sorterConstraint: SorterConstraint;
|
||||
};
|
||||
|
||||
export const DEFAULT_COLUMN_KEYS = {
|
||||
Text: 'text',
|
||||
Checked: 'checked',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const DEFAULT_COLUMNS: Column[] = [
|
||||
/** System internal variables */ {
|
||||
// Display mode of group / page
|
||||
name: 'Scene',
|
||||
type: ColumnType.enum,
|
||||
key: 'scene',
|
||||
multiple: 1,
|
||||
options: [
|
||||
{ id: 'todo', name: 'Todo List', value: GroupScene.page },
|
||||
{ id: 'page', name: 'Table', value: GroupScene.table },
|
||||
],
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
// block selected displayed columns
|
||||
name: 'visibleColumnKeys',
|
||||
type: ColumnType.enum,
|
||||
key: 'scene',
|
||||
multiple: 1,
|
||||
/**
|
||||
* All columns that the user can see, empty means unlimited options
|
||||
*/
|
||||
options: [],
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'collapsed',
|
||||
type: ColumnType.boolean,
|
||||
key: 'collapsed',
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'shapeProps',
|
||||
type: ColumnType.string,
|
||||
key: 'shapeProps',
|
||||
mode: 'text',
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
// text content
|
||||
name: 'Content',
|
||||
type: ColumnType.content,
|
||||
key: DEFAULT_COLUMN_KEYS.Text,
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
type: ColumnType.enum,
|
||||
key: 'status',
|
||||
options: [
|
||||
{
|
||||
id: 'notStart',
|
||||
name: 'Not Start',
|
||||
value: BlockStatus.notStart,
|
||||
color: '#E53535',
|
||||
background: '#FFCECE',
|
||||
},
|
||||
{
|
||||
id: 'progress',
|
||||
name: 'Progress',
|
||||
value: BlockStatus.progress,
|
||||
color: '#A77F1A',
|
||||
background: '#FFF5AB',
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
name: 'Done',
|
||||
value: BlockStatus.done,
|
||||
color: '#3C8867',
|
||||
background: '#C5FBE0',
|
||||
},
|
||||
],
|
||||
multiple: 1,
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'Checked',
|
||||
type: ColumnType.boolean,
|
||||
key: DEFAULT_COLUMN_KEYS.Checked,
|
||||
options: [
|
||||
{ id: 'checked', name: 'checked', value: true },
|
||||
{ id: 'unChecked', name: 'unChecked', value: false },
|
||||
],
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
name: 'Embed Link',
|
||||
type: ColumnType.string,
|
||||
key: 'embedLink',
|
||||
mode: 'url',
|
||||
supportAsTag: true,
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
type: ColumnType.file,
|
||||
key: 'image',
|
||||
accept: 'image/gif,image/jpeg,image/jpg,image/png,image/svg',
|
||||
multiple: 1,
|
||||
supportAsTag: true,
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'File',
|
||||
type: ColumnType.file,
|
||||
key: 'file',
|
||||
multiple: 1,
|
||||
supportAsTag: true,
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'Start Date',
|
||||
type: ColumnType.date,
|
||||
key: 'startDate',
|
||||
format: 'YYYY-MM-dd HH:mm:ss',
|
||||
supportAsTag: true,
|
||||
innerColumn: true,
|
||||
},
|
||||
{
|
||||
name: 'End Date',
|
||||
type: ColumnType.date,
|
||||
key: 'endDate',
|
||||
format: 'YYYY-MM-dd HH:mm:ss',
|
||||
supportAsTag: true,
|
||||
innerColumn: true,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,117 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
BlockImplInstance,
|
||||
MapOperation,
|
||||
ArrayOperation,
|
||||
} from '@toeverything/datasource/jwt';
|
||||
import type { Column } from './types';
|
||||
import { DEFAULT_COLUMNS } from './default-config';
|
||||
export const serializeColumnConfig = (column: Column): string => {
|
||||
// TODO: Do the type check of the column parameter here
|
||||
return JSON.stringify(column);
|
||||
};
|
||||
|
||||
export const deserializeColumnConfig = (config: string): Column => {
|
||||
// TODO: do the column check here
|
||||
return JSON.parse(config) as Column;
|
||||
};
|
||||
|
||||
/**
|
||||
* Support for adding column blocks
|
||||
*/
|
||||
const SUPPORT_COLUMN_FLAVORS = ['group', 'page'];
|
||||
|
||||
interface AddColumnProps {
|
||||
block: BlockImplInstance;
|
||||
columns: ArrayOperation<MapOperation<string>>;
|
||||
columnConfig: Column;
|
||||
}
|
||||
|
||||
export const addColumn = ({ block, columns, columnConfig }: AddColumnProps) => {
|
||||
const content = block.getContent();
|
||||
|
||||
const column_id = nanoid(16);
|
||||
const config = serializeColumnConfig({
|
||||
...columnConfig,
|
||||
id: column_id,
|
||||
});
|
||||
|
||||
const db_column = content.createMap<string>();
|
||||
// @ts-ignore TODO: don't know why
|
||||
db_column.set('id', column_id);
|
||||
// @ts-ignore TODO: don't know why
|
||||
db_column.set('config', config);
|
||||
columns?.insert(columns.length, [db_column]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const getOrInitBlockContentColumnsField = (
|
||||
block: BlockImplInstance
|
||||
): ArrayOperation<MapOperation<string>> | undefined => {
|
||||
if (!SUPPORT_COLUMN_FLAVORS.includes(block.flavor)) {
|
||||
return undefined;
|
||||
}
|
||||
const content = block.getContent();
|
||||
if (!content.has('columns')) {
|
||||
const columns = content.createArray();
|
||||
content.set('columns', columns);
|
||||
DEFAULT_COLUMNS.forEach(col => {
|
||||
addColumn({
|
||||
block,
|
||||
columns: columns.asArray() as ArrayOperation<
|
||||
MapOperation<string>
|
||||
>,
|
||||
columnConfig: col,
|
||||
});
|
||||
});
|
||||
}
|
||||
return content.get('columns')?.asArray();
|
||||
};
|
||||
|
||||
export const getBlockColumns = (
|
||||
block: BlockImplInstance
|
||||
): Column[] | undefined => {
|
||||
const columns = getOrInitBlockContentColumnsField(block)?.map<Column>(
|
||||
column => {
|
||||
const config_string = column.get('config') as unknown as string;
|
||||
return {
|
||||
id: column.get('id'),
|
||||
...(config_string
|
||||
? deserializeColumnConfig(config_string)
|
||||
: {}),
|
||||
} as Column;
|
||||
}
|
||||
);
|
||||
return columns;
|
||||
};
|
||||
|
||||
export type {
|
||||
Column,
|
||||
ContentColumn,
|
||||
NumberColumn,
|
||||
EnumColumn,
|
||||
DateColumn,
|
||||
BooleanColumn,
|
||||
FileColumn,
|
||||
ContentColumnValue,
|
||||
NumberColumnValue,
|
||||
EnumColumnValue,
|
||||
BooleanColumnValue,
|
||||
DateColumnValue,
|
||||
FileColumnValue,
|
||||
StringColumnValue,
|
||||
} from './types';
|
||||
export { ColumnType } from './types';
|
||||
export type { DefaultColumnsValue } from './default-config';
|
||||
|
||||
export {
|
||||
isContentColumn,
|
||||
isDateColumn,
|
||||
isFileColumn,
|
||||
isNumberColumn,
|
||||
isEnumColumn,
|
||||
isStringColumn,
|
||||
isBooleanColumn,
|
||||
} from './utils';
|
||||
@@ -0,0 +1,204 @@
|
||||
/** Column */
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
export enum ColumnType {
|
||||
/**
|
||||
* the content of the text base block
|
||||
*/
|
||||
content = 'content',
|
||||
number = 'number',
|
||||
enum = 'enum',
|
||||
date = 'date',
|
||||
boolean = 'boolean',
|
||||
file = 'file',
|
||||
string = 'string',
|
||||
}
|
||||
interface BaseColumn {
|
||||
id?: string;
|
||||
name: string;
|
||||
/**
|
||||
* key when assigning
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Properties used by the program, not as the display column of the Table
|
||||
* @deprecated
|
||||
*/
|
||||
innerColumn?: boolean;
|
||||
/**
|
||||
* Whether to support adding to the tag below the block
|
||||
*/
|
||||
supportAsTag?: boolean;
|
||||
}
|
||||
export interface ContentColumn extends BaseColumn {
|
||||
type: ColumnType.content;
|
||||
}
|
||||
|
||||
export interface NumberColumn extends BaseColumn {
|
||||
type: ColumnType.number;
|
||||
/** not implemented */
|
||||
format: 'number' | 'percent';
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
id: string;
|
||||
name: string;
|
||||
background?: string;
|
||||
value: string | boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface EnumColumn extends BaseColumn {
|
||||
type: ColumnType.enum;
|
||||
options: SelectOption[];
|
||||
/**
|
||||
* Limit the number of choices, if it is 1, it is a single choice
|
||||
*/
|
||||
multiple: number;
|
||||
}
|
||||
|
||||
export interface DateColumn extends BaseColumn {
|
||||
type: ColumnType.date;
|
||||
/**
|
||||
* Date format, such as: YYYY-MM-DD hh:mm:ss
|
||||
*ref: https://date-fns.org/v2.28.0/docs/format
|
||||
*/
|
||||
format: string;
|
||||
}
|
||||
|
||||
export interface BooleanColumn extends BaseColumn {
|
||||
type: ColumnType.boolean;
|
||||
options?: SelectOption[];
|
||||
sorter?: boolean;
|
||||
}
|
||||
|
||||
export interface FileColumn extends BaseColumn {
|
||||
type: ColumnType.file;
|
||||
/**
|
||||
*ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept
|
||||
*/
|
||||
accept?: string;
|
||||
/**
|
||||
* Limit the number of choices, you can only choose one at a time, you can choose multiple times
|
||||
*/
|
||||
multiple: number;
|
||||
}
|
||||
|
||||
export interface StringColumn extends BaseColumn {
|
||||
type: ColumnType.string;
|
||||
mode: 'text' | 'url';
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export type Column =
|
||||
| ContentColumn
|
||||
| NumberColumn
|
||||
| EnumColumn
|
||||
| DateColumn
|
||||
| BooleanColumn
|
||||
| FileColumn
|
||||
| StringColumn;
|
||||
|
||||
/**
|
||||
* ColumnValue
|
||||
* @deprecated
|
||||
*/
|
||||
export interface ContentColumnValue {
|
||||
value: Array<{ text: string; bold?: boolean }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface NumberColumnValue {
|
||||
value: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface EnumColumnValue<T = string> {
|
||||
value: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
type Timestamp = number;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface DateColumnValue {
|
||||
value: Timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface BooleanColumnValue {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
type FileBlockId = string;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
type UrlString = string;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface FileColumnValue {
|
||||
value: FileBlockId;
|
||||
url?: UrlString;
|
||||
name: string;
|
||||
/**
|
||||
* the size of the file in bytes
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* ref file.type: https://developer.mozilla.org/en-US/docs/Web/API/File
|
||||
*/
|
||||
type: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface StringColumnValue {
|
||||
value: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface CommentColumnValue {
|
||||
pageId: string;
|
||||
attachedToBlocksIds?: string[];
|
||||
resolve: boolean;
|
||||
resolveUserId?: string;
|
||||
finishTime?: number;
|
||||
}
|
||||
export type FilterConstraint = Array<{
|
||||
key: string;
|
||||
checked: boolean;
|
||||
type: string;
|
||||
fieldValue: string;
|
||||
opSelectValue: string;
|
||||
valueSelectValue:
|
||||
| string
|
||||
| string[]
|
||||
| { title?: string; value?: string | boolean }[];
|
||||
}>;
|
||||
|
||||
export type SorterConstraint = Array<{
|
||||
field: string;
|
||||
rule: string;
|
||||
}>;
|
||||
@@ -0,0 +1,38 @@
|
||||
import type {
|
||||
Column,
|
||||
ContentColumn,
|
||||
StringColumn,
|
||||
NumberColumn,
|
||||
EnumColumn,
|
||||
DateColumn,
|
||||
FileColumn,
|
||||
BooleanColumn,
|
||||
} from './types';
|
||||
|
||||
export const isContentColumn = (column: Column): column is ContentColumn => {
|
||||
return column.type === 'content';
|
||||
};
|
||||
|
||||
export const isStringColumn = (column: Column): column is StringColumn => {
|
||||
return column.type === 'string';
|
||||
};
|
||||
|
||||
export const isNumberColumn = (column: Column): column is NumberColumn => {
|
||||
return column.type === 'number';
|
||||
};
|
||||
|
||||
export const isEnumColumn = (column: Column): column is EnumColumn => {
|
||||
return column.type === 'enum';
|
||||
};
|
||||
|
||||
export const isDateColumn = (column: Column): column is DateColumn => {
|
||||
return column.type === 'date';
|
||||
};
|
||||
|
||||
export const isFileColumn = (column: Column): column is FileColumn => {
|
||||
return column.type === 'file';
|
||||
};
|
||||
|
||||
export const isBooleanColumn = (column: Column): column is BooleanColumn => {
|
||||
return column.type === 'boolean';
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { BlockImplInstance } from '@toeverything/datasource/jwt';
|
||||
|
||||
type Condition = (block: BlockImplInstance | undefined) => boolean;
|
||||
/**
|
||||
* Find the block closest to the block up
|
||||
* @param block
|
||||
* @param condition conditional function, return true to indicate found
|
||||
* @returns
|
||||
*/
|
||||
export const getClosestBlock = (
|
||||
block: BlockImplInstance,
|
||||
condition: Condition
|
||||
): BlockImplInstance | undefined => {
|
||||
let group: BlockImplInstance | undefined = block;
|
||||
while (!condition(group)) {
|
||||
group = group?.parent;
|
||||
}
|
||||
return group;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the closest Page up
|
||||
* @param block
|
||||
* @returns
|
||||
*/
|
||||
export const getClosestPage = (
|
||||
block: BlockImplInstance
|
||||
): BlockImplInstance | undefined => {
|
||||
return getClosestBlock(block, block => {
|
||||
return !block || block.flavor === 'page';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the closest group up
|
||||
* @param block
|
||||
* @returns
|
||||
*/
|
||||
export const getClosestGroup = (
|
||||
block: BlockImplInstance
|
||||
): BlockImplInstance | undefined => {
|
||||
return getClosestBlock(block, block => {
|
||||
return !block || block.flavor === 'group';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Look up the group or page closest to the block
|
||||
* @param block
|
||||
* @returns
|
||||
*/
|
||||
export const getClosestGroupOrPage = (
|
||||
block: BlockImplInstance
|
||||
): BlockImplInstance | undefined => {
|
||||
return getClosestBlock(block, block => {
|
||||
return !block || ['group', 'page'].includes(block?.flavor);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { BlockImplInstance } from '@toeverything/datasource/jwt';
|
||||
import { ReturnEditorBlock } from '../types';
|
||||
import { getClosestGroup } from './common';
|
||||
import { getBlockColumns } from './column';
|
||||
|
||||
interface DbBlock2BusinessBlockProps {
|
||||
workspace: string;
|
||||
dbBlock?: BlockImplInstance | null;
|
||||
}
|
||||
|
||||
export const dbBlock2BusinessBlock = ({
|
||||
workspace,
|
||||
dbBlock,
|
||||
}: DbBlock2BusinessBlockProps): ReturnEditorBlock | null => {
|
||||
if (!dbBlock) {
|
||||
return null;
|
||||
}
|
||||
const block = {} as ReturnEditorBlock;
|
||||
block.id = dbBlock.id;
|
||||
block.type = dbBlock.flavor;
|
||||
block.workspace = workspace;
|
||||
block.parentId = dbBlock.parent?.id;
|
||||
block.closestGroupId = getClosestGroup(dbBlock)?.id;
|
||||
block.children = dbBlock.children || [];
|
||||
block.properties = dbBlock.getDecorations();
|
||||
block.created = dbBlock.created;
|
||||
block.lastUpdated = dbBlock.lastUpdated;
|
||||
block.columns = getBlockColumns(dbBlock);
|
||||
block.creator = dbBlock.creator;
|
||||
return block;
|
||||
};
|
||||
|
||||
export {
|
||||
getOrInitBlockContentColumnsField,
|
||||
serializeColumnConfig,
|
||||
deserializeColumnConfig,
|
||||
addColumn,
|
||||
ColumnType,
|
||||
isBooleanColumn,
|
||||
isContentColumn,
|
||||
isDateColumn,
|
||||
isFileColumn,
|
||||
isNumberColumn,
|
||||
isEnumColumn,
|
||||
isStringColumn,
|
||||
} from './column';
|
||||
export type {
|
||||
Column,
|
||||
DefaultColumnsValue,
|
||||
ContentColumnValue,
|
||||
NumberColumnValue,
|
||||
EnumColumnValue,
|
||||
BooleanColumnValue,
|
||||
DateColumnValue,
|
||||
FileColumnValue,
|
||||
StringColumnValue,
|
||||
} from './column';
|
||||
52
libs/datasource/db-service/src/services/file/index.ts
Normal file
52
libs/datasource/db-service/src/services/file/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ServiceBaseClass } from '../base';
|
||||
|
||||
interface CreateParams {
|
||||
file: File;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
interface ImageResult {
|
||||
id: string;
|
||||
url: string;
|
||||
}
|
||||
export class FileService extends ServiceBaseClass {
|
||||
urlMap: Record<string, string> = {};
|
||||
|
||||
async create(params: CreateParams): Promise<ImageResult> {
|
||||
const { file, workspace } = params;
|
||||
// Get the current workspace block
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
// Get the workspace database link
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const binary = await file.arrayBuffer();
|
||||
// create a block of type file
|
||||
const file_db_block = await db.get('binary', {
|
||||
flavor: 'file',
|
||||
binary,
|
||||
});
|
||||
// add the file block to the workspace
|
||||
workspace_db_block.append(file_db_block);
|
||||
// cache the file
|
||||
const file_key = file_db_block.id + workspace;
|
||||
this.urlMap[file_key] = URL.createObjectURL(new Blob([binary]));
|
||||
return { id: file_db_block.id, url: this.urlMap[file_key] };
|
||||
}
|
||||
async get(file_block_id: string, workspace: string): Promise<ImageResult> {
|
||||
const file_key = file_block_id + workspace;
|
||||
if (this.urlMap[file_key]) {
|
||||
// lookup cache
|
||||
return { id: file_block_id, url: this.urlMap[file_key] };
|
||||
}
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const file_block = await db.get(file_block_id as 'binary');
|
||||
const file_buffer = file_block.getBinary();
|
||||
if (file_buffer) {
|
||||
this.urlMap[file_key] = URL.createObjectURL(
|
||||
new Blob([file_buffer])
|
||||
);
|
||||
} else {
|
||||
this.urlMap[file_key] = '';
|
||||
}
|
||||
return { id: file_block.id, url: this.urlMap[file_key] };
|
||||
}
|
||||
}
|
||||
108
libs/datasource/db-service/src/services/index.ts
Normal file
108
libs/datasource/db-service/src/services/index.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { DiContainer } from '@toeverything/utils';
|
||||
import type { RegisterDependencyConfig } from '@toeverything/utils';
|
||||
import { Database } from './database';
|
||||
import { PageTree } from './workspace/page-tree';
|
||||
import { UserConfig } from './workspace/user-config';
|
||||
import { EditorBlock } from './editor-block';
|
||||
import { FileService } from './file';
|
||||
import { CommentService } from './comment';
|
||||
|
||||
export type {
|
||||
CreateEditorBlock,
|
||||
ReturnEditorBlock,
|
||||
GetEditorBlock,
|
||||
DeleteEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
BlockFlavors,
|
||||
BlockFlavorKeys,
|
||||
Column,
|
||||
ContentColumn,
|
||||
NumberColumn,
|
||||
EnumColumn,
|
||||
DateColumn,
|
||||
BooleanColumn,
|
||||
FileColumn,
|
||||
DefaultColumnsValue,
|
||||
ContentColumnValue,
|
||||
NumberColumnValue,
|
||||
EnumColumnValue,
|
||||
BooleanColumnValue,
|
||||
DateColumnValue,
|
||||
FileColumnValue,
|
||||
StringColumnValue,
|
||||
} from './editor-block';
|
||||
export {
|
||||
ColumnType,
|
||||
isBooleanColumn,
|
||||
isContentColumn,
|
||||
isDateColumn,
|
||||
isFileColumn,
|
||||
isNumberColumn,
|
||||
isEnumColumn,
|
||||
isStringColumn,
|
||||
} from './editor-block';
|
||||
|
||||
export interface DbServicesMap {
|
||||
editorBlock: EditorBlock;
|
||||
pageTree: PageTree;
|
||||
userConfig: UserConfig;
|
||||
file: FileService;
|
||||
commentService: CommentService;
|
||||
}
|
||||
|
||||
interface RegisterDependencyConfigWithName extends RegisterDependencyConfig {
|
||||
callName: string;
|
||||
}
|
||||
|
||||
const dbServiceConfig: RegisterDependencyConfigWithName[] = [
|
||||
{
|
||||
type: 'value',
|
||||
callName: 'database',
|
||||
token: Database,
|
||||
value: new Database({}),
|
||||
dependencies: [],
|
||||
},
|
||||
{
|
||||
type: 'class',
|
||||
callName: 'editorBlock',
|
||||
token: EditorBlock,
|
||||
value: EditorBlock,
|
||||
dependencies: [{ token: Database }],
|
||||
},
|
||||
{
|
||||
type: 'class',
|
||||
callName: 'pageTree',
|
||||
token: PageTree,
|
||||
value: PageTree,
|
||||
dependencies: [{ token: Database }],
|
||||
},
|
||||
{
|
||||
type: 'class',
|
||||
callName: 'userConfig',
|
||||
token: UserConfig,
|
||||
value: UserConfig,
|
||||
dependencies: [{ token: Database }, { token: PageTree, lazy: true }],
|
||||
},
|
||||
{
|
||||
type: 'class',
|
||||
callName: 'file',
|
||||
token: FileService,
|
||||
value: FileService,
|
||||
dependencies: [{ token: Database }],
|
||||
},
|
||||
{
|
||||
type: 'class',
|
||||
callName: 'commentService',
|
||||
token: CommentService,
|
||||
value: CommentService,
|
||||
dependencies: [{ token: EditorBlock }],
|
||||
},
|
||||
];
|
||||
|
||||
export const serviceMapByCallName = dbServiceConfig.reduce((acc, cur) => {
|
||||
acc[cur.callName] = cur;
|
||||
return acc;
|
||||
}, {} as Record<string, RegisterDependencyConfigWithName>);
|
||||
|
||||
export const diContainer = new DiContainer();
|
||||
diContainer.register(dbServiceConfig);
|
||||
225
libs/datasource/db-service/src/services/workspace/page-tree.ts
Normal file
225
libs/datasource/db-service/src/services/workspace/page-tree.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import type { BlockClientInstance } from '@toeverything/datasource/jwt';
|
||||
import { PAGE_TREE } from '../../utils';
|
||||
import type { ReturnUnobserve } from '../database/observer';
|
||||
import { ServiceBaseClass } from '../base';
|
||||
import { TreeItem } from './types';
|
||||
|
||||
export type ObserveCallback = () => void;
|
||||
|
||||
export class PageTree extends ServiceBaseClass {
|
||||
private async fetch_page_tree<TreeItem>(workspace: string) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const page_tree_config =
|
||||
workspace_db_block.getDecoration<TreeItem[]>(PAGE_TREE);
|
||||
return page_tree_config;
|
||||
}
|
||||
|
||||
async getPageTree<TreeItem>(workspace: string): Promise<TreeItem[]> {
|
||||
try {
|
||||
const page_tree = await this.fetch_page_tree(workspace);
|
||||
if (page_tree && page_tree.length) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
|
||||
const pages = await update_tree_items_title(
|
||||
db,
|
||||
page_tree as [],
|
||||
{}
|
||||
);
|
||||
return pages;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @deprecated should implement more fine-grained crud methods instead of replacing each time with a new array */
|
||||
async setPageTree<TreeItem>(workspace: string, treeData: TreeItem[]) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
workspace_db_block.setDecoration(PAGE_TREE, treeData);
|
||||
}
|
||||
|
||||
async addPage<TreeItem>(workspace: string, treeData: TreeItem[] | string) {
|
||||
// TODO: rewrite
|
||||
if (typeof treeData === 'string') {
|
||||
await this.setPageTree(workspace, [{ id: treeData, children: [] }]);
|
||||
}
|
||||
}
|
||||
async removePage(workspace: string, blockId: string) {
|
||||
const dbBlock = await this.getBlock(workspace, blockId);
|
||||
await dbBlock?.remove();
|
||||
}
|
||||
|
||||
async addPageToWorkspacee(
|
||||
target_workspace_id: string,
|
||||
new_page_id: string
|
||||
) {
|
||||
const items = await this.getPageTree<TreeItem>(target_workspace_id);
|
||||
await this.setPageTree(target_workspace_id, [
|
||||
{ id: new_page_id, children: [] },
|
||||
...items,
|
||||
]);
|
||||
}
|
||||
|
||||
async addChildPageToWorkspace(
|
||||
target_workspace_id: string,
|
||||
parent_page_id: string,
|
||||
new_page_id: string
|
||||
) {
|
||||
const pages = await this.getPageTree<TreeItem>(target_workspace_id);
|
||||
this.build_items_for_child_page(parent_page_id, new_page_id, pages);
|
||||
|
||||
await this.setPageTree<TreeItem>(target_workspace_id, [...pages]);
|
||||
}
|
||||
async addPrevPageToWorkspace(
|
||||
target_workspace_id: string,
|
||||
parent_page_id: string,
|
||||
new_page_id: string
|
||||
) {
|
||||
const pages = await this.getPageTree<TreeItem>(target_workspace_id);
|
||||
this.build_items_for_prev_page(parent_page_id, new_page_id, pages);
|
||||
await this.setPageTree<TreeItem>(target_workspace_id, [...pages]);
|
||||
}
|
||||
async addNextPageToWorkspace(
|
||||
target_workspace_id: string,
|
||||
parent_page_id: string,
|
||||
new_page_id: string
|
||||
) {
|
||||
const pages = await this.getPageTree<TreeItem>(target_workspace_id);
|
||||
this.build_items_for_next_page(parent_page_id, new_page_id, pages);
|
||||
await this.setPageTree<TreeItem>(target_workspace_id, [...pages]);
|
||||
}
|
||||
private build_items_for_next_page(
|
||||
parent_page_id: string,
|
||||
new_page_id: string,
|
||||
children: TreeItem[]
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child_page = children[i];
|
||||
if (child_page.id === parent_page_id) {
|
||||
const new_page = {
|
||||
id: new_page_id,
|
||||
title: 'Untitled',
|
||||
children: [] as TreeItem[],
|
||||
};
|
||||
children = children.splice(i + 1, 0, new_page);
|
||||
} else if (child_page.children && child_page.children.length) {
|
||||
this.build_items_for_next_page(
|
||||
parent_page_id,
|
||||
new_page_id,
|
||||
child_page.children
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
private build_items_for_prev_page(
|
||||
parent_page_id: string,
|
||||
new_page_id: string,
|
||||
children: TreeItem[]
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child_page = children[i];
|
||||
if (child_page.id === parent_page_id) {
|
||||
const new_page = {
|
||||
id: new_page_id,
|
||||
title: 'Untitled',
|
||||
children: [] as TreeItem[],
|
||||
};
|
||||
children = children.splice(i - 1, 0, new_page);
|
||||
} else if (child_page.children && child_page.children.length) {
|
||||
this.build_items_for_prev_page(
|
||||
parent_page_id,
|
||||
new_page_id,
|
||||
child_page.children
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
private build_items_for_child_page(
|
||||
parent_page_id: string,
|
||||
new_page_id: string,
|
||||
children: TreeItem[]
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child_page = children[i];
|
||||
if (child_page.id === parent_page_id) {
|
||||
child_page.children = child_page.children || [];
|
||||
child_page.children.push({
|
||||
id: new_page_id,
|
||||
title: 'Untitled',
|
||||
children: [],
|
||||
});
|
||||
} else if (child_page.children && child_page.children.length) {
|
||||
this.build_items_for_child_page(
|
||||
parent_page_id,
|
||||
new_page_id,
|
||||
child_page.children
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: handles unobserve
|
||||
async observe(
|
||||
{ workspace, page }: { workspace: string; page: string },
|
||||
callback: ObserveCallback
|
||||
): Promise<ReturnUnobserve> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const unobserveWorkspace = await this._observe(
|
||||
workspace,
|
||||
workspace_db_block.id,
|
||||
(states, block) => {
|
||||
callback();
|
||||
}
|
||||
);
|
||||
const unobservePage = await this._observe(
|
||||
workspace,
|
||||
page,
|
||||
(states, block) => {
|
||||
callback();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unobserveWorkspace();
|
||||
unobservePage();
|
||||
};
|
||||
}
|
||||
|
||||
async unobserve({ workspace }: { workspace: string }) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
await this._unobserve(workspace, workspace_db_block.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function update_tree_items_title<
|
||||
TreeItem extends { id: string; title: string; children: TreeItem[] }
|
||||
>(
|
||||
db: BlockClientInstance,
|
||||
items: TreeItem[],
|
||||
cache: Record<string, string>
|
||||
): Promise<TreeItem[]> {
|
||||
for (const item of items) {
|
||||
if (cache[item.id]) {
|
||||
item.title = cache[item.id];
|
||||
} else {
|
||||
const page = await db.get(item.id as 'page');
|
||||
item.title =
|
||||
page
|
||||
.getDecoration<{ value: Array<{ text: string }> }>('text')
|
||||
?.value?.map(v => v.text)
|
||||
.join('') || 'Untitled';
|
||||
cache[item.id] = item.title;
|
||||
}
|
||||
|
||||
if (item.children.length) {
|
||||
item.children = await update_tree_items_title(
|
||||
db,
|
||||
item.children,
|
||||
cache
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [...items];
|
||||
}
|
||||
13
libs/datasource/db-service/src/services/workspace/types.ts
Normal file
13
libs/datasource/db-service/src/services/workspace/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
interface TreeItem {
|
||||
id: string;
|
||||
title: string;
|
||||
children?: TreeItem[];
|
||||
}
|
||||
|
||||
interface PageConfigItem {
|
||||
id: string;
|
||||
title?: string;
|
||||
lastOpenTime: number;
|
||||
}
|
||||
|
||||
export type { TreeItem, PageConfigItem };
|
||||
124
libs/datasource/db-service/src/services/workspace/user-config.ts
Normal file
124
libs/datasource/db-service/src/services/workspace/user-config.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { RECENT_PAGES, WORKSPACE_CONFIG } from '../../utils';
|
||||
import { ServiceBaseClass } from '../base';
|
||||
import { ObserveCallback, ReturnUnobserve } from '../database';
|
||||
import { PageTree } from './page-tree';
|
||||
import { PageConfigItem } from './types';
|
||||
|
||||
/** Operate the user configuration at the workspace level */
|
||||
export class UserConfig extends ServiceBaseClass {
|
||||
private async fetch_recent_pages(
|
||||
workspace: string
|
||||
): Promise<Record<string, Array<PageConfigItem>>> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const recent_work_pages =
|
||||
workspace_db_block.getDecoration<
|
||||
Record<string, Array<PageConfigItem>>
|
||||
>(RECENT_PAGES) || {};
|
||||
return recent_work_pages;
|
||||
}
|
||||
|
||||
private async save_recent_pages(
|
||||
workspace: string,
|
||||
recentPages: Record<string, Array<PageConfigItem>>
|
||||
) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
workspace_db_block.setDecoration(RECENT_PAGES, recentPages);
|
||||
}
|
||||
|
||||
async getUserInitialPage(
|
||||
workspace: string,
|
||||
userId: string
|
||||
): Promise<string> {
|
||||
const recent_pages = await this.getRecentPages(workspace, userId);
|
||||
if (recent_pages.length > 0) {
|
||||
return recent_pages[0].id;
|
||||
}
|
||||
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const new_page = await db.get('page');
|
||||
|
||||
await this.get_dependency(PageTree).addPage(workspace, new_page.id);
|
||||
await this.addRecentPage(workspace, userId, new_page.id);
|
||||
return new_page.id;
|
||||
}
|
||||
|
||||
async getRecentPages(
|
||||
workspace: string,
|
||||
userId: string,
|
||||
topNumber = 5
|
||||
): Promise<PageConfigItem[]> {
|
||||
const recent_work_pages = await this.fetch_recent_pages(workspace);
|
||||
const recent_pages = (recent_work_pages[userId] || []).slice(
|
||||
0,
|
||||
topNumber
|
||||
);
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
for (const item of recent_pages) {
|
||||
const page = await db.get(item.id as 'page');
|
||||
item.title =
|
||||
page
|
||||
.getDecoration<{ value: Array<{ text: string }> }>('text')
|
||||
?.value?.map(v => v.text)
|
||||
.join('') || 'Untitled';
|
||||
}
|
||||
return recent_pages;
|
||||
}
|
||||
|
||||
async addRecentPage(workspace: string, userId: string, pageId: string) {
|
||||
const recent_work_pages = await this.fetch_recent_pages(workspace);
|
||||
let recent_pages = recent_work_pages[userId] || [];
|
||||
recent_pages = recent_pages.filter(item => item.id !== pageId);
|
||||
recent_pages.unshift({
|
||||
id: pageId,
|
||||
lastOpenTime: Date.now(),
|
||||
});
|
||||
recent_work_pages[userId] = recent_pages;
|
||||
await this.save_recent_pages(workspace, recent_work_pages);
|
||||
}
|
||||
|
||||
async removePage(workspace: string, pageId: string) {
|
||||
const recent_work_pages = await this.fetch_recent_pages(workspace);
|
||||
for (const key in recent_work_pages) {
|
||||
recent_work_pages[key] = recent_work_pages[key].filter(
|
||||
item => item.id !== pageId
|
||||
);
|
||||
}
|
||||
await this.save_recent_pages(workspace, recent_work_pages);
|
||||
}
|
||||
|
||||
async observe(
|
||||
{ workspace }: { workspace: string },
|
||||
callback: ObserveCallback
|
||||
): Promise<ReturnUnobserve> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const unobserveWorkspace = await this._observe(
|
||||
workspace,
|
||||
workspace_db_block.id,
|
||||
(states, block) => {
|
||||
callback(states, block);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unobserveWorkspace();
|
||||
};
|
||||
}
|
||||
|
||||
async unobserve({ workspace }: { workspace: string }) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
await this._unobserve(workspace, workspace_db_block.id);
|
||||
}
|
||||
|
||||
async getWorkspaceName(workspace: string): Promise<string> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const workspaceName =
|
||||
workspace_db_block.getDecoration<string>(WORKSPACE_CONFIG) ||
|
||||
workspace_db_block.id;
|
||||
return workspaceName;
|
||||
}
|
||||
|
||||
async setWorkspaceName(workspace: string, workspaceName: string) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
workspace_db_block.setDecoration(WORKSPACE_CONFIG, workspaceName);
|
||||
}
|
||||
}
|
||||
5
libs/datasource/db-service/src/utils/constants.ts
Normal file
5
libs/datasource/db-service/src/utils/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// blockdb configuration item related property name key
|
||||
export const PAGE_TREE = 'page_tree';
|
||||
export const RECENT_PAGES = 'activities';
|
||||
export const WORKSPACE_COMMENTS = 'workspace_comments';
|
||||
export const WORKSPACE_CONFIG = 'workspace_config';
|
||||
1
libs/datasource/db-service/src/utils/index.ts
Normal file
1
libs/datasource/db-service/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
24
libs/datasource/db-service/tsconfig.json
Normal file
24
libs/datasource/db-service/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
// "noImplicitAny": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
libs/datasource/db-service/tsconfig.lib.json
Normal file
22
libs/datasource/db-service/tsconfig.lib.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
},
|
||||
"files": [
|
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
19
libs/datasource/db-service/tsconfig.spec.json
Normal file
19
libs/datasource/db-service/tsconfig.spec.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user