diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx index 58526cae7c..252b72b7b9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx @@ -1,6 +1,5 @@ import TextBlock from '../TextBlock'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; -import { HeadingBlockData } from '@/appflowy_app/interfaces/document'; +import { HeadingBlockData, Node } from '@/appflowy_app/interfaces/document'; const fontSize: Record = { 1: 'mt-8 text-3xl', diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx index 00349acf8a..0555d1f116 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx @@ -1,6 +1,6 @@ -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { Circle } from '@mui/icons-material'; import NodeComponent from '../Node'; +import { Node } from '$app/interfaces/document'; export default function BulletedListBlock({ title, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx index 2a24900eb2..b39905545d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import ColumnBlock from '../ColumnBlock'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; + +import { Node } from '$app/interfaces/document'; export default function ColumnListBlock({ node, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx index 5c66f61133..774397c8dc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx @@ -1,5 +1,5 @@ -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import NodeComponent from '../Node'; +import { Node } from '$app/interfaces/document'; export default function NumberedListBlock({ title, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx index b156bfa55e..37af080afe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx @@ -3,8 +3,7 @@ import TextBlock from '../TextBlock'; import NumberedListBlock from './NumberedListBlock'; import BulletedListBlock from './BulletedListBlock'; import ColumnListBlock from './ColumnListBlock'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; -import { TextDelta } from '@/appflowy_app/interfaces/document'; +import { Node, TextDelta } from '@/appflowy_app/interfaces/document'; export default function ListBlock({ node }: { node: Node }) { const title = useMemo(() => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index 5477252e58..8338fd8087 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -2,9 +2,9 @@ import React, { useCallback } from 'react'; import { useNode } from './Node.hooks'; import { withErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import TextBlock from '../TextBlock'; import { NodeContext } from '../_shared/SubscribeNode.hooks'; +import { Node } from '$app/interfaces/document'; function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) { const { node, childIds, isSelected, ref } = useNode(id); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx index 6153978af9..a1baf60d50 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -5,15 +5,9 @@ import { documentActions } from '$app/stores/reducers/document/slice'; export function useParseTree(documentData: DocumentData) { const dispatch = useAppDispatch(); - const { blocks, meta } = documentData; useEffect(() => { - dispatch( - documentActions.create({ - nodes: blocks, - children: meta.childrenMap, - }) - ); + dispatch(documentActions.create(documentData)); return () => { dispatch(documentActions.clear()); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts index ad4f7a2d56..87c70139e6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -1,7 +1,7 @@ import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey'; import { useCallback, useContext } from 'react'; import { Range, Editor, Element, Text, Location } from 'slate'; -import { TextDelta } from '$app/interfaces/document'; +import { TextDelta, TextSelection } from '$app/interfaces/document'; import { useTextInput } from '../_shared/TextInput.hooks'; import { useAppDispatch } from '@/appflowy_app/stores/store'; import { DocumentControllerContext } from '@/appflowy_app/stores/effects/document/document_controller'; @@ -10,7 +10,7 @@ import { indentNodeThunk, splitNodeThunk, } from '@/appflowy_app/stores/reducers/document/async_actions'; -import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice'; +import { documentActions } from '@/appflowy_app/stores/reducers/document/slice'; export function useTextBlock(id: string) { const { editor, onChange, value } = useTextInput(id); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index 4538391bd0..03a2cc1880 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -1,10 +1,10 @@ import { Slate, Editable } from 'slate-react'; import Leaf from './Leaf'; import { useTextBlock } from './TextBlock.hooks'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import NodeComponent from '../Node'; import HoveringToolbar from '../_shared/HoveringToolbar'; import React, { useEffect } from 'react'; +import { Node } from '$app/interfaces/document'; function TextBlock({ node, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx index 3dc35d5b19..4a5c917b3c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { useVirtualizedList } from './VirtualizedList.hooks'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import DocumentTitle from '../DocumentTitle'; import Overlay from '../Overlay'; +import { Node } from '$app/interfaces/document'; export default function VirtualizedList({ childIds, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts index 1381c7aad4..4815ce6564 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -1,6 +1,6 @@ -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { useAppSelector } from '@/appflowy_app/stores/store'; import { useMemo, createContext } from 'react'; +import { Node } from '$app/interfaces/document'; export const NodeContext = createContext(null); /** diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts index 588ac08015..0d3d32d535 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useMemo, useRef, useEffect, useState } from 'react'; import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; -import { TextDelta } from '$app/interfaces/document'; +import { TextDelta, TextSelection } from '$app/interfaces/document'; import { NodeContext } from './SubscribeNode.hooks'; import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store'; @@ -10,7 +10,7 @@ import { withReact, ReactEditor } from 'slate-react'; import * as Y from 'yjs'; import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; import { updateNodeDeltaThunk } from '@/appflowy_app/stores/reducers/document/async_actions/update'; -import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice'; +import { documentActions } from '@/appflowy_app/stores/reducers/document/slice'; import { deltaToSlateValue, getDeltaFromSlateNodes } from '@/appflowy_app/utils/block'; export function useTextInput(id: string) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/block.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/block.ts new file mode 100644 index 0000000000..78dfceaf6d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/block.ts @@ -0,0 +1,3 @@ +export const BLOCK_MAP_NAME = 'blocks'; +export const META_NAME = 'meta'; +export const CHILDREN_MAP_NAME = 'children_map'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index d5672ffde3..70b41f1efc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -37,13 +37,6 @@ export interface TextDelta { insert: string; attributes?: Record; } -export interface DocumentData { - rootId: string; - blocks: Record; - meta: { - childrenMap: Record; - }; -} // eslint-disable-next-line no-shadow export enum BlockActionType { @@ -60,3 +53,51 @@ export interface DeltaItem { value?: NestedBlock | string[]; }; } + +export type Node = NestedBlock; + +export interface SelectionPoint { + path: [number, number]; + offset: number; +} + +export interface TextSelection { + anchor: SelectionPoint; + focus: SelectionPoint; +} + +export interface DocumentData { + rootId: string; + // map of block id to block + nodes: Record; + // map of block id to children block ids + children: Record; +} +export interface DocumentState { + // map of block id to block + nodes: Record; + // map of block id to children block ids + children: Record; + // selected block ids + selections: string[]; + // map of block id to text selection + textSelections: Record; +} + +// eslint-disable-next-line no-shadow +export enum ChangeType { + BlockInsert, + BlockUpdate, + BlockDelete, + ChildrenMapInsert, + ChildrenMapUpdate, + ChildrenMapDelete, +} + +export interface BlockPBValue { + id: string; + ty: string; + parent: string; + children: string; + data: string; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts index c1da46ee34..23afdaa555 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -1,10 +1,20 @@ -import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document'; +import { DocumentData, Node } from '@/appflowy_app/interfaces/document'; import { createContext } from 'react'; import { DocumentBackendService } from './document_bd_svc'; -import { FlowyError, BlockActionPB, DocEventPB, BlockActionTypePB, BlockEventPayloadPB } from '@/services/backend'; +import { + FlowyError, + BlockActionPB, + DocEventPB, + BlockActionTypePB, + BlockEventPayloadPB, + BlockPB, + ChildrenPB, +} from '@/services/backend'; import { DocumentObserver } from './document_observer'; -import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import * as Y from 'yjs'; +import { blockPB2Node } from '@/appflowy_app/utils/block'; +import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '@/appflowy_app/constants/block'; +import { get } from '@/appflowy_app/utils/tool'; export const DocumentControllerContext = createContext(null); @@ -34,33 +44,18 @@ export class DocumentController { const document = await this.backendService.open(); if (document.ok) { - const blocks: DocumentData['blocks'] = {}; - document.val.blocks.forEach((block) => { - let data = {}; - try { - data = JSON.parse(block.data); - } catch { - console.log('json parse error', block.data); - } - - blocks[block.id] = { - id: block.id, - type: block.ty as BlockType, - parent: block.parent_id, - children: block.children_id, - data, - }; + const nodes: DocumentData['nodes'] = {}; + get>(document.val, [BLOCK_MAP_NAME]).forEach((block) => { + nodes[block.id] = blockPB2Node(block); }); - const childrenMap: Record = {}; - document.val.meta.children_map.forEach((child, key) => { - childrenMap[key] = child.children; + const children: Record = {}; + get>(document.val, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => { + children[key] = child.children; }); return { rootId: document.val.page_id, - blocks, - meta: { - childrenMap, - }, + nodes, + children, }; } @@ -150,8 +145,8 @@ export class DocumentController { if (!this.onDocChange) return; const { events, is_remote } = DocEventPB.deserializeBinary(payload); - events.forEach((event) => { - event.event.forEach((_payload) => { + events.forEach((blockEvent) => { + blockEvent.event.forEach((_payload) => { this.onDocChange?.({ isRemote: is_remote, data: _payload, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/backspace.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/backspace.ts index 2dfea617b7..9445a4da86 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/backspace.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/backspace.ts @@ -1,7 +1,7 @@ -import { BlockType } from '@/appflowy_app/interfaces/document'; +import { BlockType, DocumentState } from '@/appflowy_app/interfaces/document'; import { DocumentController } from '$app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { documentActions, DocumentState } from '../slice'; +import { documentActions } from '../slice'; import { outdentNodeThunk } from './outdent'; import { setCursorAfterThunk } from './set_cursor'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/delete.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/delete.ts index 5110827f37..7bcf90f25d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/delete.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/delete.ts @@ -1,6 +1,7 @@ import { DocumentController } from '$app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { DocumentState } from '../slice'; + +import { DocumentState } from '$app/interfaces/document'; export const deleteNodeThunk = createAsyncThunk( 'document/deleteNode', diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/indent.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/indent.ts index a9236b88ab..9a03823d7c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/indent.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/indent.ts @@ -1,7 +1,6 @@ -import { BlockType } from '@/appflowy_app/interfaces/document'; +import { BlockType, DocumentState } from '@/appflowy_app/interfaces/document'; import { DocumentController } from '$app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { DocumentState } from '../slice'; export const indentNodeThunk = createAsyncThunk( 'document/indentNode', diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/insert.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/insert.ts index 9bf3c69704..e8d57ae501 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/insert.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/insert.ts @@ -1,7 +1,6 @@ -import { BlockType, NestedBlock } from '@/appflowy_app/interfaces/document'; +import { BlockType, DocumentState, NestedBlock } from '@/appflowy_app/interfaces/document'; import { DocumentController } from '$app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { DocumentState } from '../slice'; import { generateId } from '@/appflowy_app/utils/block'; export const insertAfterNodeThunk = createAsyncThunk( diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/outdent.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/outdent.ts index 5b2fa32cfe..f0f5fc0bbe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/outdent.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/outdent.ts @@ -1,6 +1,7 @@ import { DocumentController } from '$app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { DocumentState } from '../slice'; + +import { DocumentState } from '$app/interfaces/document'; export const outdentNodeThunk = createAsyncThunk( 'document/outdentNode', diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/set_cursor.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/set_cursor.ts index 38917f7420..b222decfb9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/set_cursor.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/set_cursor.ts @@ -1,5 +1,6 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import { documentActions, DocumentState, SelectionPoint, TextSelection } from '../slice'; +import { documentActions } from '../slice'; +import { DocumentState, SelectionPoint, TextSelection } from '$app/interfaces/document'; export const setCursorBeforeThunk = createAsyncThunk( 'document/setCursorBefore', diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/split.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/split.ts index 30d0149dca..e78ad4f5ca 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/split.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/split.ts @@ -1,8 +1,8 @@ -import { BlockType, TextDelta } from '@/appflowy_app/interfaces/document'; +import { BlockType, DocumentState, TextDelta } from '@/appflowy_app/interfaces/document'; import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { generateId } from '@/appflowy_app/utils/block'; -import { documentActions, DocumentState } from '../slice'; +import { documentActions } from '../slice'; import { setCursorBeforeThunk } from './set_cursor'; export const splitNodeThunk = createAsyncThunk( diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/update.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/update.ts index 21419b8096..99c8c4863c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/update.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/update.ts @@ -1,7 +1,7 @@ -import { TextDelta, NestedBlock } from '@/appflowy_app/interfaces/document'; +import { TextDelta, NestedBlock, DocumentState } from '@/appflowy_app/interfaces/document'; import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { documentActions, DocumentState } from '../slice'; +import { documentActions } from '../slice'; import { debounce } from '$app/utils/tool'; export const updateNodeDeltaThunk = createAsyncThunk( 'document/updateNodeDelta', diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts index 0c942b2077..738177a6f9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -1,32 +1,8 @@ -import { NestedBlock } from '@/appflowy_app/interfaces/document'; -import { blockChangeValue2Node } from '@/appflowy_app/utils/block'; -import { Log } from '@/appflowy_app/utils/log'; -import { BlockEventPayloadPB, DeltaTypePB } from '@/services/backend'; -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { RegionGrid } from './region_grid'; - -export type Node = NestedBlock; - -export interface SelectionPoint { - path: [number, number]; - offset: number; -} - -export interface TextSelection { - anchor: SelectionPoint; - focus: SelectionPoint; -} - -export interface DocumentState { - // map of block id to block - nodes: Record; - // map of block id to children block ids - children: Record; - // selected block ids - selections: string[]; - // map of block id to text selection - textSelections: Record; -} +import { DocumentState, Node, TextSelection } from '@/appflowy_app/interfaces/document'; +import { BlockEventPayloadPB } from '@/services/backend'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { RegionGrid } from '@/appflowy_app/utils/region_grid'; +import { parseValue, matchChange } from '@/appflowy_app/utils/block_change'; const regionGrid = new RegionGrid(50); @@ -158,44 +134,11 @@ export const documentSlice = createSlice({ const { path, id, value, command } = action.payload.data; const isRemote = action.payload.isRemote; - let valueJson; - try { - valueJson = JSON.parse(value); - } catch { - Log.error('[onDataChange] json parse error', value); - return; - } + const valueJson = parseValue(value); if (!valueJson) return; - if (command === DeltaTypePB.Inserted || command === DeltaTypePB.Updated) { - // set map key and value ( block map or children map) - if (path[0] === 'blocks') { - const block = blockChangeValue2Node(valueJson); - if (command === DeltaTypePB.Updated && !isRemote) { - // the `data` from local is already updated in local, so we just need to update other fields - const node = state.nodes[block.id]; - if (!node || node.parent !== block.parent || node.type !== block.type || node.children !== block.children) { - state.nodes[block.id] = block; - } - } else { - state.nodes[block.id] = block; - } - } else { - state.children[id] = valueJson; - } - } else { - // remove map key ( block map or children map) - if (path[0] === 'blocks') { - if (state.selections.indexOf(id)) { - state.selections.splice(state.selections.indexOf(id), 1); - } - regionGrid.removeBlock(id); - delete state.textSelections[id]; - delete state.nodes[id]; - } else { - delete state.children[id]; - } - } + // match change + matchChange(state, { path, id, value: valueJson, command }, isRemote); }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts index 16a182f6d1..639428dbda 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts @@ -1,8 +1,8 @@ +import { BlockPB } from '@/services/backend/models/flowy-document2'; import { nanoid } from 'nanoid'; import { Descendant, Element, Text } from 'slate'; -import { TextDelta, BlockType, NestedBlock } from '../interfaces/document'; +import { BlockType, TextDelta } from '../interfaces/document'; import { Log } from './log'; - export function generateId() { return nanoid(10); } @@ -36,29 +36,19 @@ export function getDeltaFromSlateNodes(slateNodes: Descendant[]) { }); } -export function blockChangeValue2Node(value: { - id: string; - ty: string; - parent: string; - children: string; - data: string; -}): NestedBlock { - const block = { - id: value.id, - type: value.ty as BlockType, - parent: value.parent, - children: value.children, - data: {}, - }; - if ('data' in value && typeof value.data === 'string') { - try { - Object.assign(block, { - data: JSON.parse(value.data), - }); - } catch { - Log.error('valueJson data parse error', block.data); - } +export function blockPB2Node(block: BlockPB) { + let data = {}; + try { + data = JSON.parse(block.data); + } catch { + Log.error('[Document Open] json parse error', block.data); } - - return block; + const node = { + id: block.id, + type: block.ty as BlockType, + parent: block.parent_id, + children: block.children_id, + data, + }; + return node; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_change.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_change.ts new file mode 100644 index 0000000000..df3e348e90 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block_change.ts @@ -0,0 +1,185 @@ +import { DeltaTypePB } from '@/services/backend/models/flowy-document2'; +import { BlockType, NestedBlock, DocumentState, ChangeType, BlockPBValue } from '../interfaces/document'; +import { Log } from './log'; +import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '../constants/block'; + +// This is a list of all the possible changes that can happen to document data +const matchCases = [ + { match: matchBlockInsert, type: ChangeType.BlockInsert, onMatch: onMatchBlockInsert }, + { match: matchBlockUpdate, type: ChangeType.BlockUpdate, onMatch: onMatchBlockUpdate }, + { match: matchBlockDelete, type: ChangeType.BlockDelete, onMatch: onMatchBlockDelete }, + { match: matchChildrenMapInsert, type: ChangeType.ChildrenMapInsert, onMatch: onMatchChildrenInsert }, + { match: matchChildrenMapUpdate, type: ChangeType.ChildrenMapUpdate, onMatch: onMatchChildrenUpdate }, + { match: matchChildrenMapDelete, type: ChangeType.ChildrenMapDelete, onMatch: onMatchChildrenDelete }, +]; + +export function matchChange( + state: DocumentState, + { + command, + path, + id, + value, + }: { + command: DeltaTypePB; + path: string[]; + id: string; + value: BlockPBValue & string[]; + }, + isRemote?: boolean +) { + const matchCase = matchCases.find((item) => item.match(command, path)); + + if (matchCase) { + matchCase.onMatch(state, id, value, isRemote); + } +} + +/** + * @param command DeltaTypePB.Inserted + * @param path [BLOCK_MAP_NAME] + */ +function matchBlockInsert(command: DeltaTypePB, path: string[]) { + if (path.length !== 1) return false; + return command === DeltaTypePB.Inserted && path[0] === BLOCK_MAP_NAME; +} + +/** + * @param command DeltaTypePB.Updated + * @param path [BLOCK_MAP_NAME, blockId] + */ +function matchBlockUpdate(command: DeltaTypePB, path: string[]) { + if (path.length !== 2) return false; + return command === DeltaTypePB.Updated && path[0] === BLOCK_MAP_NAME && typeof path[1] === 'string'; +} + +/** + * @param command DeltaTypePB.Removed + * @param path [BLOCK_MAP_NAME, blockId] + */ +function matchBlockDelete(command: DeltaTypePB, path: string[]) { + if (path.length !== 2) return false; + return command === DeltaTypePB.Removed && path[0] === BLOCK_MAP_NAME && typeof path[1] === 'string'; +} + +/** + * @param command DeltaTypePB.Inserted + * @param path [META_NAME, CHILDREN_MAP_NAME] + */ +function matchChildrenMapInsert(command: DeltaTypePB, path: string[]) { + if (path.length !== 2) return false; + return command === DeltaTypePB.Inserted && path[0] === META_NAME && path[1] === CHILDREN_MAP_NAME; +} + +/** + * @param command DeltaTypePB.Updated + * @param path [META_NAME, CHILDREN_MAP_NAME, id] + */ +function matchChildrenMapUpdate(command: DeltaTypePB, path: string[]) { + if (path.length !== 3) return false; + return ( + command === DeltaTypePB.Updated && + path[0] === META_NAME && + path[1] === CHILDREN_MAP_NAME && + typeof path[2] === 'string' + ); +} + +/** + * @param command DeltaTypePB.Removed + * @param path [META_NAME, CHILDREN_MAP_NAME, id] + */ +function matchChildrenMapDelete(command: DeltaTypePB, path: string[]) { + if (path.length !== 3) return false; + return ( + command === DeltaTypePB.Removed && + path[0] === META_NAME && + path[1] === CHILDREN_MAP_NAME && + typeof path[2] === 'string' + ); +} + +function onMatchBlockInsert(state: DocumentState, blockId: string, blockValue: BlockPBValue, _isRemote?: boolean) { + const block = blockChangeValue2Node(blockValue); + state.nodes[blockId] = block; +} + +function onMatchBlockUpdate(state: DocumentState, blockId: string, blockValue: BlockPBValue, isRemote?: boolean) { + const block = blockChangeValue2Node(blockValue); + const node = state.nodes[blockId]; + if (!node) return; + // if the change is from remote, we should update all fields + if (isRemote) { + state.nodes[blockId] = block; + return; + } + // if the change is from local, we should update all fields except `data`, + // because we will update `data` field in `updateNodeData` action + const shouldUpdate = node.parent !== block.parent || node.type !== block.type || node.children !== block.children; + if (shouldUpdate) { + state.nodes[blockId] = { + ...block, + data: node.data, + }; + } + return; +} + +function onMatchBlockDelete(state: DocumentState, blockId: string, blockValue: BlockPBValue, _isRemote?: boolean) { + const index = state.selections.indexOf(blockId); + if (index > -1) { + state.selections.splice(index, 1); + } + delete state.textSelections[blockId]; + delete state.nodes[blockId]; +} + +function onMatchChildrenInsert(state: DocumentState, id: string, children: string[], _isRemote?: boolean) { + state.children[id] = children; +} + +function onMatchChildrenUpdate(state: DocumentState, id: string, newChildren: string[], _isRemote?: boolean) { + const children = state.children[id]; + if (!children) return; + state.children[id] = newChildren; +} + +function onMatchChildrenDelete(state: DocumentState, id: string, _children: string[], _isRemote?: boolean) { + delete state.children[id]; +} + +/** + * convert block change value to node + * @param value + */ +export function blockChangeValue2Node(value: BlockPBValue): NestedBlock { + const block = { + id: value.id, + type: value.ty as BlockType, + parent: value.parent, + children: value.children, + data: {}, + }; + if ('data' in value && typeof value.data === 'string') { + try { + Object.assign(block, { + data: JSON.parse(value.data), + }); + } catch { + Log.error('[onDataChange] valueJson data parse error', block.data); + } + } + + return block; +} + +export function parseValue(value: string) { + let valueJson; + try { + valueJson = JSON.parse(value); + } catch { + Log.error('[onDataChange] json parse error', value); + return; + } + return valueJson; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts rename to frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts index 6bf8d0ebde..2886f73da2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -1,30 +1,30 @@ export function debounce(fn: (...args: any[]) => void, delay: number) { let timeout: NodeJS.Timeout; return (...args: any[]) => { - clearTimeout(timeout) - timeout = setTimeout(()=>{ + clearTimeout(timeout); + timeout = setTimeout(() => { // eslint-disable-next-line prefer-spread - fn.apply(undefined, args) - }, delay) - } + fn.apply(undefined, args); + }, delay); + }; } export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) { - let timeout: NodeJS.Timeout | null = null + let timeout: NodeJS.Timeout | null = null; return (...args: any[]) => { if (!timeout) { timeout = setTimeout(() => { - timeout = null + timeout = null; // eslint-disable-next-line prefer-spread - !immediate && fn.apply(undefined, args) - }, delay) + !immediate && fn.apply(undefined, args); + }, delay); // eslint-disable-next-line prefer-spread - immediate && fn.apply(undefined, args) + immediate && fn.apply(undefined, args); } - } + }; } -export function get(obj: any, path: string[], defaultValue?: any) { +export function get(obj: any, path: string[], defaultValue?: any): T { let value = obj; for (const prop of path) { value = value[prop]; @@ -55,7 +55,6 @@ export function isEqual(value1: T, value2: T): boolean { return value1 === value2; } - if (Array.isArray(value1)) { if (!Array.isArray(value2) || value1.length !== value2.length) { return false; @@ -77,9 +76,9 @@ export function isEqual(value1: T, value2: T): boolean { return false; } - for (const key of keys1) { + for (const key of keys1) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error + // @ts-expect-error if (!isEqual(value1[key], value2[key])) { return false; }