Browse Source

fix: refactor match change code (#2352)

qinluhe 2 years ago
parent
commit
9717dfa3c4
28 changed files with 326 additions and 176 deletions
  1. 1 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx
  2. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx
  3. 2 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx
  4. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx
  5. 1 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx
  6. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
  7. 1 7
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
  8. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
  9. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
  10. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx
  11. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
  12. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts
  13. 3 0
      frontend/appflowy_tauri/src/appflowy_app/constants/block.ts
  14. 48 7
      frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
  15. 23 28
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
  16. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/backspace.ts
  17. 2 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/delete.ts
  18. 1 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/indent.ts
  19. 1 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/insert.ts
  20. 2 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/outdent.ts
  21. 2 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/set_cursor.ts
  22. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/split.ts
  23. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async_actions/update.ts
  24. 8 65
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
  25. 16 26
      frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
  26. 185 0
      frontend/appflowy_tauri/src/appflowy_app/utils/block_change.ts
  27. 0 0
      frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts
  28. 14 15
      frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts

+ 1 - 2
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<string, string> = {
   1: 'mt-8 text-3xl',

+ 1 - 1
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,

+ 2 - 1
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,

+ 1 - 1
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,

+ 1 - 2
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(() => {

+ 1 - 1
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<HTMLDivElement>) {
   const { node, childIds, isSelected, ref } = useNode(id);

+ 1 - 7
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());

+ 2 - 2
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);

+ 1 - 1
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,

+ 1 - 1
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,

+ 1 - 1
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<Node | null>(null);
 
 /**

+ 2 - 2
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) {

+ 3 - 0
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';

+ 48 - 7
frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts

@@ -37,13 +37,6 @@ export interface TextDelta {
   insert: string;
   attributes?: Record<string, string | boolean>;
 }
-export interface DocumentData {
-  rootId: string;
-  blocks: Record<string, NestedBlock>;
-  meta: {
-    childrenMap: Record<string, string[]>;
-  };
-}
 
 // 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<string, Node>;
+  // map of block id to children block ids
+  children: Record<string, string[]>;
+}
+export interface DocumentState {
+  // map of block id to block
+  nodes: Record<string, Node>;
+  // map of block id to children block ids
+  children: Record<string, string[]>;
+  // selected block ids
+  selections: string[];
+  // map of block id to text selection
+  textSelections: Record<string, TextSelection>;
+}
+
+// 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;
+}

+ 23 - 28
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<DocumentController | null>(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<Map<string, BlockPB>>(document.val, [BLOCK_MAP_NAME]).forEach((block) => {
+        nodes[block.id] = blockPB2Node(block);
       });
-      const childrenMap: Record<string, string[]> = {};
-      document.val.meta.children_map.forEach((child, key) => {
-        childrenMap[key] = child.children;
+      const children: Record<string, string[]> = {};
+      get<Map<string, ChildrenPB>>(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,

+ 2 - 2
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';
 

+ 2 - 1
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',

+ 1 - 2
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',

+ 1 - 2
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(

+ 2 - 1
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',

+ 2 - 1
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',

+ 2 - 2
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(

+ 2 - 2
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',

+ 8 - 65
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<string, Node>;
-  // map of block id to children block ids
-  children: Record<string, string[]>;
-  // selected block ids
-  selections: string[];
-  // map of block id to text selection
-  textSelections: Record<string, TextSelection>;
-}
+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);
     },
   },
 });

+ 16 - 26
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;
 }

+ 185 - 0
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;
+}

+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts → frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts


+ 14 - 15
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<T = any>(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<T>(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<T>(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;
     }