mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: make it unidirectional data flow by listening to document updates (#2347)
This commit is contained in:
parent
1ad2f6cef5
commit
eb78f9d36a
@ -1,18 +1,21 @@
|
|||||||
import { DocumentData, BlockType, DeltaItem } from '@/appflowy_app/interfaces/document';
|
import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
|
||||||
import { createContext, Dispatch } from 'react';
|
import { createContext } from 'react';
|
||||||
import { DocumentBackendService } from './document_bd_svc';
|
import { DocumentBackendService } from './document_bd_svc';
|
||||||
import { FlowyError, BlockActionPB, DocEventPB, DeltaTypePB, BlockActionTypePB } from '@/services/backend';
|
import { FlowyError, BlockActionPB, DocEventPB, BlockActionTypePB, BlockEventPayloadPB } from '@/services/backend';
|
||||||
import { DocumentObserver } from './document_observer';
|
import { DocumentObserver } from './document_observer';
|
||||||
import { documentActions, Node } from '@/appflowy_app/stores/reducers/document/slice';
|
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
import { Log } from '@/appflowy_app/utils/log';
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||||
|
|
||||||
export class DocumentController {
|
export class DocumentController {
|
||||||
private readonly backendService: DocumentBackendService;
|
private readonly backendService: DocumentBackendService;
|
||||||
private readonly observer: DocumentObserver;
|
private readonly observer: DocumentObserver;
|
||||||
|
|
||||||
constructor(public readonly viewId: string, private dispatch?: Dispatch<any>) {
|
constructor(
|
||||||
|
public readonly viewId: string,
|
||||||
|
private onDocChange?: (props: { isRemote: boolean; data: BlockEventPayloadPB }) => void
|
||||||
|
) {
|
||||||
this.backendService = new DocumentBackendService(viewId);
|
this.backendService = new DocumentBackendService(viewId);
|
||||||
this.observer = new DocumentObserver(viewId);
|
this.observer = new DocumentObserver(viewId);
|
||||||
}
|
}
|
||||||
@ -107,6 +110,7 @@ export class DocumentController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
dispose = async () => {
|
dispose = async () => {
|
||||||
|
this.onDocChange = undefined;
|
||||||
await this.backendService.close();
|
await this.backendService.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,65 +147,16 @@ export class DocumentController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private updated = (payload: Uint8Array) => {
|
private updated = (payload: Uint8Array) => {
|
||||||
const dispatch = this.dispatch;
|
if (!this.onDocChange) return;
|
||||||
if (!dispatch) return;
|
|
||||||
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
||||||
|
|
||||||
if (!is_remote) return;
|
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
event.event.forEach((_payload) => {
|
event.event.forEach((_payload) => {
|
||||||
const { path, id, value, command } = _payload;
|
this.onDocChange?.({
|
||||||
let valueJson;
|
isRemote: is_remote,
|
||||||
try {
|
data: _payload,
|
||||||
valueJson = JSON.parse(value);
|
});
|
||||||
} catch {
|
|
||||||
console.error('json parse error', value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
dispatch(documentActions.setBlockMap(block));
|
|
||||||
} else {
|
|
||||||
dispatch(
|
|
||||||
documentActions.setChildrenMap({
|
|
||||||
id,
|
|
||||||
childIds: valueJson,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// remove map key ( block map or children map)
|
|
||||||
if (path[0] === 'blocks') {
|
|
||||||
dispatch(documentActions.removeBlockMapKey(id));
|
|
||||||
} else {
|
|
||||||
dispatch(documentActions.removeChildrenMapKey(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockChangeValue2Node(value: { id: string; ty: string; parent: string; children: string; data: string }): Node {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
@ -42,13 +42,8 @@ const composeNodeThunk = createAsyncThunk(
|
|||||||
|
|
||||||
// move must be before delete
|
// move must be before delete
|
||||||
await controller.applyActions([...moveActions, deleteAction, updateAction]);
|
await controller.applyActions([...moveActions, deleteAction, updateAction]);
|
||||||
|
// update local node data
|
||||||
children.reverse().forEach((childId) => {
|
dispatch(documentActions.updateNodeData({ id: newNode.id, data: { delta: newNode.data.delta } }));
|
||||||
dispatch(documentActions.moveNode({ id: childId, newParentId: newNode.id, newPrevId: '' }));
|
|
||||||
});
|
|
||||||
dispatch(documentActions.setBlockMap(newNode));
|
|
||||||
dispatch(documentActions.removeBlockMapKey(node.id));
|
|
||||||
dispatch(documentActions.removeChildrenMapKey(node.children));
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions, DocumentState } from '../slice';
|
import { DocumentState } from '../slice';
|
||||||
|
|
||||||
export const deleteNodeThunk = createAsyncThunk(
|
export const deleteNodeThunk = createAsyncThunk(
|
||||||
'document/deleteNode',
|
'document/deleteNode',
|
||||||
@ -11,22 +11,5 @@ export const deleteNodeThunk = createAsyncThunk(
|
|||||||
const node = state.document.nodes[id];
|
const node = state.document.nodes[id];
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
await controller.applyActions([controller.getDeleteAction(node)]);
|
await controller.applyActions([controller.getDeleteAction(node)]);
|
||||||
|
|
||||||
const deleteNode = (deleteId: string) => {
|
|
||||||
const deleteItem = state.document.nodes[deleteId];
|
|
||||||
const children = state.document.children[deleteItem.children];
|
|
||||||
// delete children
|
|
||||||
if (children.length > 0) {
|
|
||||||
children.forEach((childId) => {
|
|
||||||
deleteNode(childId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dispatch(documentActions.removeBlockMapKey(deleteItem.id));
|
|
||||||
dispatch(documentActions.removeChildrenMapKey(deleteItem.children));
|
|
||||||
};
|
|
||||||
deleteNode(node.id);
|
|
||||||
|
|
||||||
if (!node.parent) return;
|
|
||||||
dispatch(documentActions.deleteChild({ id: node.parent, childId: node.id }));
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { BlockType } from '@/appflowy_app/interfaces/document';
|
import { BlockType } from '@/appflowy_app/interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions, DocumentState } from '../slice';
|
import { DocumentState } from '../slice';
|
||||||
|
|
||||||
export const indentNodeThunk = createAsyncThunk(
|
export const indentNodeThunk = createAsyncThunk(
|
||||||
'document/indentNode',
|
'document/indentNode',
|
||||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
const { id, controller } = payload;
|
const { id, controller } = payload;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { getState } = thunkAPI;
|
||||||
const state = (getState() as { document: DocumentState }).document;
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
if (!node.parent) return;
|
if (!node.parent) return;
|
||||||
@ -26,12 +26,5 @@ export const indentNodeThunk = createAsyncThunk(
|
|||||||
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
||||||
|
|
||||||
await controller.applyActions([controller.getMoveAction(node, newParentId, newPrevId)]);
|
await controller.applyActions([controller.getMoveAction(node, newParentId, newPrevId)]);
|
||||||
dispatch(
|
|
||||||
documentActions.moveNode({
|
|
||||||
id,
|
|
||||||
newParentId,
|
|
||||||
newPrevId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { BlockType, NestedBlock } from '@/appflowy_app/interfaces/document';
|
import { BlockType, NestedBlock } from '@/appflowy_app/interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions, DocumentState } from '../slice';
|
import { DocumentState } from '../slice';
|
||||||
import { generateId } from '@/appflowy_app/utils/block';
|
import { generateId } from '@/appflowy_app/utils/block';
|
||||||
import { setCursorAfterThunk } from './set_cursor';
|
|
||||||
export const insertAfterNodeThunk = createAsyncThunk(
|
export const insertAfterNodeThunk = createAsyncThunk(
|
||||||
'document/insertAfterNode',
|
'document/insertAfterNode',
|
||||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
@ -25,21 +25,5 @@ export const insertAfterNodeThunk = createAsyncThunk(
|
|||||||
children: generateId(),
|
children: generateId(),
|
||||||
};
|
};
|
||||||
await controller.applyActions([controller.getInsertAction(newNode, node.id)]);
|
await controller.applyActions([controller.getInsertAction(newNode, node.id)]);
|
||||||
dispatch(documentActions.setBlockMap(newNode));
|
|
||||||
dispatch(
|
|
||||||
documentActions.setChildrenMap({
|
|
||||||
id: newNode.children,
|
|
||||||
childIds: [],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// insert new node to parent
|
|
||||||
dispatch(
|
|
||||||
documentActions.insertChild({
|
|
||||||
id: parentId,
|
|
||||||
childId: newNode.id,
|
|
||||||
prevId: node.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await dispatch(setCursorAfterThunk({ id: newNode.id }));
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions, DocumentState } from '../slice';
|
import { DocumentState } from '../slice';
|
||||||
|
|
||||||
export const outdentNodeThunk = createAsyncThunk(
|
export const outdentNodeThunk = createAsyncThunk(
|
||||||
'document/outdentNode',
|
'document/outdentNode',
|
||||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
const { id, controller } = payload;
|
const { id, controller } = payload;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { getState } = thunkAPI;
|
||||||
const state = (getState() as { document: DocumentState }).document;
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
const newPrevId = node.parent;
|
const newPrevId = node.parent;
|
||||||
@ -15,12 +15,5 @@ export const outdentNodeThunk = createAsyncThunk(
|
|||||||
const newParentId = parent.parent;
|
const newParentId = parent.parent;
|
||||||
if (!newParentId) return;
|
if (!newParentId) return;
|
||||||
await controller.applyActions([controller.getMoveAction(node, newParentId, newPrevId)]);
|
await controller.applyActions([controller.getMoveAction(node, newParentId, newPrevId)]);
|
||||||
dispatch(
|
|
||||||
documentActions.moveNode({
|
|
||||||
id: node.id,
|
|
||||||
newParentId,
|
|
||||||
newPrevId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions, DocumentState, SelectionPoint, TextSelection } from '../slice';
|
import { documentActions, DocumentState, SelectionPoint, TextSelection } from '../slice';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
|
|||||||
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { generateId } from '@/appflowy_app/utils/block';
|
import { generateId } from '@/appflowy_app/utils/block';
|
||||||
import { documentActions, DocumentState, TextSelection } from '../slice';
|
import { documentActions, DocumentState } from '../slice';
|
||||||
import { setCursorBeforeThunk } from './set_cursor';
|
import { setCursorBeforeThunk } from './set_cursor';
|
||||||
|
|
||||||
export const splitNodeThunk = createAsyncThunk(
|
export const splitNodeThunk = createAsyncThunk(
|
||||||
@ -36,22 +36,8 @@ export const splitNodeThunk = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
await controller.applyActions([controller.getInsertAction(newNode, prevId), controller.getUpdateAction(retainNode)]);
|
await controller.applyActions([controller.getInsertAction(newNode, prevId), controller.getUpdateAction(retainNode)]);
|
||||||
dispatch(documentActions.setBlockMap(newNode));
|
// update local node data
|
||||||
dispatch(documentActions.setBlockMap(retainNode));
|
dispatch(documentActions.updateNodeData({ id: retainNode.id, data: { delta: retain } }));
|
||||||
dispatch(
|
|
||||||
documentActions.setChildrenMap({
|
|
||||||
id: newNode.children,
|
|
||||||
childIds: [],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
documentActions.insertChild({
|
|
||||||
id: parent.id,
|
|
||||||
childId: newNode.id,
|
|
||||||
prevId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// set cursor
|
// set cursor
|
||||||
await dispatch(setCursorBeforeThunk({ id: newNode.id }));
|
await dispatch(setCursorBeforeThunk({ id: newNode.id }));
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
import { TextDelta, NestedBlock } from '@/appflowy_app/interfaces/document';
|
||||||
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions, DocumentState, Node } from '../slice';
|
import { documentActions, DocumentState } from '../slice';
|
||||||
import { debounce } from '$app/utils/tool';
|
import { debounce } from '$app/utils/tool';
|
||||||
export const updateNodeDeltaThunk = createAsyncThunk(
|
export const updateNodeDeltaThunk = createAsyncThunk(
|
||||||
'document/updateNodeDelta',
|
'document/updateNodeDelta',
|
||||||
@ -9,25 +9,23 @@ export const updateNodeDeltaThunk = createAsyncThunk(
|
|||||||
const { id, delta, controller } = payload;
|
const { id, delta, controller } = payload;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const state = (getState() as { document: DocumentState }).document;
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
// The block map should be updated immediately
|
||||||
|
// or the component will use the old data to update the editor
|
||||||
|
dispatch(documentActions.updateNodeData({ id, data: { delta } }));
|
||||||
|
|
||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
const updateNode = {
|
// the transaction is delayed to avoid too many updates
|
||||||
|
debounceApplyUpdate(controller, {
|
||||||
...node,
|
...node,
|
||||||
id,
|
|
||||||
data: {
|
data: {
|
||||||
...node.data,
|
...node.data,
|
||||||
delta,
|
delta,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
// The block map should be updated immediately
|
|
||||||
// or the component will use the old data to update the editor
|
|
||||||
dispatch(documentActions.setBlockMap(updateNode));
|
|
||||||
|
|
||||||
// the transaction is delayed to avoid too many updates
|
|
||||||
debounceApplyUpdate(controller, updateNode);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const debounceApplyUpdate = debounce((controller: DocumentController, updateNode: Node) => {
|
const debounceApplyUpdate = debounce((controller: DocumentController, updateNode: NestedBlock) => {
|
||||||
void controller.applyActions([
|
void controller.applyActions([
|
||||||
controller.getUpdateAction({
|
controller.getUpdateAction({
|
||||||
...updateNode,
|
...updateNode,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document';
|
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 { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { DocumentController } from '../../effects/document/document_controller';
|
|
||||||
import { RegionGrid } from './region_grid';
|
import { RegionGrid } from './region_grid';
|
||||||
|
|
||||||
export type Node = NestedBlock;
|
export type Node = NestedBlock;
|
||||||
@ -131,78 +132,71 @@ export const documentSlice = createSlice({
|
|||||||
state.textSelections;
|
state.textSelections;
|
||||||
},
|
},
|
||||||
|
|
||||||
// insert block
|
// We need this action to update the local state before `onDataChange` to make the UI more smooth,
|
||||||
setBlockMap: (state, action: PayloadAction<Node>) => {
|
// because we often use `debounce` to send the change to db, so the db data will be updated later.
|
||||||
state.nodes[action.payload.id] = action.payload;
|
updateNodeData: (state, action: PayloadAction<{ id: string; data: Record<string, any> }>) => {
|
||||||
|
const { id, data } = action.payload;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
if (!node) return;
|
||||||
|
node.data = {
|
||||||
|
...node.data,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// update block when `type`, `parent` or `children` changed
|
// when we use `onDataChange` to handle the change, we don't need care about the change is from which client,
|
||||||
updateBlock: (state, action: PayloadAction<{ id: string; block: NestedBlock }>) => {
|
// because the data is always from db state, and then to UI.
|
||||||
const { id, block } = action.payload;
|
// Except the `updateNodeData` action, we will use it before `onDataChange` to update the local state,
|
||||||
const node = state.nodes[id];
|
// so we should skip update block's `data` field when the change is from local
|
||||||
if (!node || node.parent !== block.parent || node.type !== block.type || node.children !== block.children) {
|
onDataChange: (
|
||||||
state.nodes[action.payload.id] = block;
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
data: BlockEventPayloadPB;
|
||||||
|
isRemote: boolean;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
},
|
if (!valueJson) return;
|
||||||
|
|
||||||
// remove block
|
if (command === DeltaTypePB.Inserted || command === DeltaTypePB.Updated) {
|
||||||
removeBlockMapKey(state, action: PayloadAction<string>) {
|
// set map key and value ( block map or children map)
|
||||||
if (!state.nodes[action.payload]) return;
|
if (path[0] === 'blocks') {
|
||||||
const { id } = state.nodes[action.payload];
|
const block = blockChangeValue2Node(valueJson);
|
||||||
regionGrid.removeBlock(id);
|
if (command === DeltaTypePB.Updated && !isRemote) {
|
||||||
delete state.nodes[id];
|
// 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) {
|
||||||
// set block's relationship with its children
|
state.nodes[block.id] = block;
|
||||||
setChildrenMap: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => {
|
}
|
||||||
const { id, childIds } = action.payload;
|
} else {
|
||||||
state.children[id] = childIds;
|
state.nodes[block.id] = block;
|
||||||
},
|
}
|
||||||
|
} else {
|
||||||
// remove block's relationship with its children
|
state.children[id] = valueJson;
|
||||||
removeChildrenMapKey(state, action: PayloadAction<string>) {
|
}
|
||||||
if (state.children[action.payload]) {
|
} else {
|
||||||
delete state.children[action.payload];
|
// 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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// set block's relationship with its parent
|
|
||||||
insertChild: (state, action: PayloadAction<{ id: string; childId: string; prevId: string | null }>) => {
|
|
||||||
const { id, childId, prevId } = action.payload;
|
|
||||||
const parent = state.nodes[id];
|
|
||||||
const children = state.children[parent.children];
|
|
||||||
const index = prevId ? children.indexOf(prevId) + 1 : 0;
|
|
||||||
children.splice(index, 0, childId);
|
|
||||||
},
|
|
||||||
|
|
||||||
// remove block's relationship with its parent
|
|
||||||
deleteChild: (state, action: PayloadAction<{ id: string; childId: string }>) => {
|
|
||||||
const { id, childId } = action.payload;
|
|
||||||
const parent = state.nodes[id];
|
|
||||||
const children = state.children[parent.children];
|
|
||||||
const index = children.indexOf(childId);
|
|
||||||
children.splice(index, 1);
|
|
||||||
},
|
|
||||||
|
|
||||||
// move block to another parent
|
|
||||||
moveNode: (state, action: PayloadAction<{ id: string; newParentId: string; newPrevId: string | null }>) => {
|
|
||||||
const { id, newParentId, newPrevId } = action.payload;
|
|
||||||
const newParent = state.nodes[newParentId];
|
|
||||||
const oldParentId = state.nodes[id].parent;
|
|
||||||
if (!oldParentId) return;
|
|
||||||
const oldParent = state.nodes[oldParentId];
|
|
||||||
|
|
||||||
state.nodes[id] = {
|
|
||||||
...state.nodes[id],
|
|
||||||
parent: newParentId,
|
|
||||||
};
|
|
||||||
const index = state.children[oldParent.children].indexOf(id);
|
|
||||||
state.children[oldParent.children].splice(index, 1);
|
|
||||||
|
|
||||||
const newIndex = newPrevId ? state.children[newParent.children].indexOf(newPrevId) + 1 : 0;
|
|
||||||
state.children[newParent.children].splice(newIndex, 0, id);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { Descendant, Element, Text } from 'slate';
|
import { Descendant, Element, Text } from 'slate';
|
||||||
import { TextDelta } from '../interfaces/document';
|
import { TextDelta, BlockType, NestedBlock } from '../interfaces/document';
|
||||||
|
import { Log } from './log';
|
||||||
|
|
||||||
export function generateId() {
|
export function generateId() {
|
||||||
return nanoid(10);
|
return nanoid(10);
|
||||||
@ -34,3 +35,30 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
@ -4,6 +4,8 @@ import { DocumentData } from '../interfaces/document';
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { useAppDispatch } from '../stores/store';
|
import { useAppDispatch } from '../stores/store';
|
||||||
import { Log } from '../utils/log';
|
import { Log } from '../utils/log';
|
||||||
|
import { documentActions } from '../stores/reducers/document/slice';
|
||||||
|
import { BlockEventPayloadPB } from '@/services/backend/models/flowy-document2';
|
||||||
|
|
||||||
export const useDocument = () => {
|
export const useDocument = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -12,12 +14,16 @@ export const useDocument = () => {
|
|||||||
const [controller, setController] = useState<DocumentController | null>(null);
|
const [controller, setController] = useState<DocumentController | null>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const onDocumentChange = (props: { isRemote: boolean; data: BlockEventPayloadPB }) => {
|
||||||
|
dispatch(documentActions.onDataChange(props));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let documentController: DocumentController | null = null;
|
let documentController: DocumentController | null = null;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!params?.id) return;
|
if (!params?.id) return;
|
||||||
Log.debug('open document', params.id);
|
Log.debug('open document', params.id);
|
||||||
documentController = new DocumentController(params.id, dispatch);
|
documentController = new DocumentController(params.id, onDocumentChange);
|
||||||
setController(documentController);
|
setController(documentController);
|
||||||
try {
|
try {
|
||||||
const res = await documentController.open();
|
const res = await documentController.open();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user