init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View 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": {}
}
]
}

View 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).

View 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',
};

View 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"
}
}

View 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
}
}
}
}

View 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';

View 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 };

View 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);
}
}

View 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;
}
}

View File

@@ -0,0 +1,2 @@
export * from './comment';
export * from './types';

View 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;
}

View 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);
}
}
}
}

View 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}`;
}

View 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';

View File

@@ -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 };

View File

@@ -0,0 +1,3 @@
export { TemplateFactory } from './template-factory';
export * from './types';

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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;
}

View File

@@ -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,
},
];

View File

@@ -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';

View File

@@ -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;
}>;

View File

@@ -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';
};

View File

@@ -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);
});
};

View File

@@ -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';

View 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] };
}
}

View 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);

View 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];
}

View 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 };

View 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);
}
}

View 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';

View File

@@ -0,0 +1 @@
export * from './constants';

View 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"
}
]
}

View 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"]
}

View 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"
]
}