Merge pull request #2258 from qinluhe/feat/refactor-tauri-document

Feat/refactor tauri document
This commit is contained in:
qinluhe 2023-04-14 10:14:29 +08:00 committed by GitHub
commit 0e5a03a282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1865 additions and 1761 deletions

View File

@ -74,4 +74,4 @@
"uuid": "^9.0.0",
"vite": "^4.0.0"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -72,7 +72,7 @@ export function useBlockSelection({
});
}, []);
const calcIntersectBlocks = useCallback(
const updateSelctionsByPoint = useCallback(
(clientX: number, clientY: number) => {
if (!isDragging) return;
const [startX, startY] = pointRef.current;
@ -86,7 +86,7 @@ export function useBlockSelection({
endY,
});
disaptch(
documentActions.changeSelectionByIntersectRect({
documentActions.setSelectionByRect({
startX: Math.min(startX, endX),
startY: Math.min(startY, endY),
endX: Math.max(startX, endX),
@ -102,7 +102,7 @@ export function useBlockSelection({
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
calcIntersectBlocks(e.clientX, e.clientY);
updateSelctionsByPoint(e.clientX, e.clientY);
const { top, bottom } = container.getBoundingClientRect();
if (e.clientY >= bottom) {
@ -124,7 +124,7 @@ export function useBlockSelection({
}
if (!isDragging) return;
e.preventDefault();
calcIntersectBlocks(e.clientX, e.clientY);
updateSelctionsByPoint(e.clientX, e.clientY);
setDragging(false);
setRect(null);
},

View File

@ -1,4 +1,4 @@
import { BlockType } from '@/appflowy_app/interfaces/document';
import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
import { useAppSelector } from '@/appflowy_app/stores/store';
import { debounce } from '@/appflowy_app/utils/tool';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
@ -43,9 +43,10 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
el.style.zIndex = '1';
el.style.top = '1px';
if (node?.type === BlockType.HeadingBlock) {
if (node.data.style?.level === 1) {
const nodeData = node.data as HeadingBlockData;
if (nodeData.level === 1) {
el.style.top = '8px';
} else if (node.data.style?.level === 2) {
} else if (nodeData.level === 2) {
el.style.top = '6px';
} else {
el.style.top = '5px';
@ -80,16 +81,7 @@ function useController() {
const parentId = node.parent;
if (!parentId || !controller) return;
controller.transact([
() => {
const newNode = {
id: v4(),
delta: [],
type: BlockType.TextBlock,
};
controller.insert(newNode, parentId, node.id);
},
]);
//
}, []);
return {

View File

@ -1,8 +1,7 @@
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
export function useDocumentTitle(id: string) {
const { node, delta } = useSubscribeNode(id);
const { node } = useSubscribeNode(id);
return {
node,
delta
}
}
};
}

View File

@ -3,11 +3,18 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
import TextBlock from '../TextBlock';
export default function DocumentTitle({ id }: { id: string }) {
const { node, delta } = useDocumentTitle(id);
const { node } = useDocumentTitle(id);
if (!node) return null;
return (
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
<TextBlock placeholder='Untitled' childIds={[]} delta={delta || []} node={node} />
<TextBlock placeholder='Untitled' childIds={[]} node={{
...node,
data: {
...node.data,
delta: node.data.delta || [],
}
}} />
</div>
);
}

View File

@ -11,7 +11,7 @@ const fontSize: Record<string, string> = {
export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
return (
<div className={`${fontSize[node.data.style?.level]} font-semibold `}>
<TextBlock node={node} childIds={[]} delta={delta} />
{/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
</div>
);
}

View File

@ -11,7 +11,7 @@ export default function ListBlock({ node, delta }: { node: Node; delta: TextDelt
if (node.data.style?.type === 'column') return <></>;
return (
<div className='flex-1'>
<TextBlock delta={delta} node={node} childIds={[]} />
{/*<TextBlock delta={delta} node={node} childIds={[]} />*/}
</div>
);
}, [node, delta]);

View File

@ -1,11 +1,10 @@
import { useEffect, useRef } from 'react';
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
import { useAppDispatch } from '$app/stores/store';
import { documentActions } from '$app/stores/reducers/document/slice';
export function useNode(id: string) {
const { node, childIds, delta, isSelected } = useSubscribeNode(id);
const { node, childIds, isSelected } = useSubscribeNode(id);
const ref = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();
@ -15,22 +14,23 @@ export function useNode(id: string) {
const rect = ref.current.getBoundingClientRect();
const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement;
dispatch(documentActions.updateNodePosition({
id,
rect: {
x: rect.x,
y: rect.y + scrollContainer.scrollTop,
height: rect.height,
width: rect.width
}
}))
}, [])
dispatch(
documentActions.updateNodePosition({
id,
rect: {
x: rect.x,
y: rect.y + scrollContainer.scrollTop,
height: rect.height,
width: rect.width,
},
})
);
}, []);
return {
ref,
node,
childIds,
delta,
isSelected
}
}
isSelected,
};
}

View File

@ -7,14 +7,26 @@ import TextBlock from '../TextBlock';
import { TextDelta } from '@/appflowy_app/interfaces/document';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, delta, isSelected, ref } = useNode(id);
const { node, childIds, isSelected, ref } = useNode(id);
console.log('=====', id);
const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => {
const renderBlock = useCallback((_props: { node: Node; childIds?: string[] }) => {
switch (_props.node.type) {
case 'text':
if (!_props.delta) return null;
return <TextBlock {..._props} delta={_props.delta} />;
case 'text': {
const delta = _props.node.data.delta;
if (!delta) return null;
return (
<TextBlock
node={{
..._props.node,
data: {
delta,
},
}}
childIds={childIds}
/>
);
}
default:
break;
}
@ -27,7 +39,6 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
{renderBlock({
node,
childIds,
delta,
})}
<div className='block-overlay' />
{isSelected ? <div className='pointer-events-none absolute inset-0 z-[-1] rounded-[4px] bg-[#E0F8FF]' /> : null}

View File

@ -5,14 +5,13 @@ import { documentActions } from '$app/stores/reducers/document/slice';
export function useParseTree(documentData: DocumentData) {
const dispatch = useAppDispatch();
const { blocks, ytexts, yarrays } = documentData;
const { blocks, meta } = documentData;
useEffect(() => {
dispatch(
documentActions.createTree({
documentActions.create({
nodes: blocks,
delta: ytexts,
children: yarrays,
children: meta.childrenMap,
})
);

View File

@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks';
import Node from '../Node';
import { withErrorBoundary } from 'react-error-boundary';
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
import VirtualizerList from '../VirtualizerList';
import VirtualizedList from '../VirtualizedList';
import { Skeleton } from '@mui/material';
function Root({ documentData }: { documentData: DocumentData }) {
@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) {
return (
<div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
<VirtualizerList node={node} childIds={childIds} renderNode={renderNode} />
<VirtualizedList node={node} childIds={childIds} renderNode={renderNode} />
</div>
);
}

View File

@ -1,61 +0,0 @@
import { useEffect, useMemo, useRef } from "react";
import { createEditor } from "slate";
import { withReact } from "slate-react";
import * as Y from 'yjs';
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
import { Delta } from '@slate-yjs/core/dist/model/types';
import { TextDelta } from '@/appflowy_app/interfaces/document';
const initialValue = [{
type: 'paragraph',
children: [{ text: '' }],
}];
export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
const yTextRef = useRef<Y.XmlText>();
// Create a yjs document and get the shared type
const sharedType = useMemo(() => {
const ydoc = new Y.Doc()
const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText;
const insertDelta = slateNodesToInsertDelta(initialValue);
// Load the initial value into the yjs document
_sharedType.applyDelta(insertDelta);
const yText = insertDelta[0].insert as Y.XmlText;
yTextRef.current = yText;
return _sharedType;
}, []);
const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
useEffect(() => {
YjsEditor.connect(editor);
return () => {
yTextRef.current = undefined;
YjsEditor.disconnect(editor);
}
}, [editor]);
useEffect(() => {
const yText = yTextRef.current;
if (!yText) return;
const textEventHandler = (event: Y.YTextEvent) => {
update(event.changes.delta as TextDelta[]);
}
yText.applyDelta(delta);
yText.observe(textEventHandler);
return () => {
yText.unobserve(textEventHandler);
}
}, [delta])
return { editor }
}

View File

@ -1,71 +1,18 @@
import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
import { useCallback, useContext, useMemo, useRef, useState } from "react";
import { Descendant, Range } from "slate";
import { useBindYjs } from "./BindYjs.hooks";
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
import { useCallback, useState } from 'react';
import { Descendant, Range } from 'slate';
import { TextDelta } from '$app/interfaces/document';
import { debounce } from "@/appflowy_app/utils/tool";
import { useTextInput } from '../_shared/TextInput.hooks';
function useController(textId: string) {
const docController = useContext(DocumentControllerContext);
const update = useCallback(
(delta: TextDelta[]) => {
docController?.yTextApply(textId, delta)
},
[textId],
);
const transact = useCallback(
(actions: (() => void)[]) => {
docController?.transact(actions)
},
[textId],
)
return {
update,
transact
}
}
function useTransact(textId: string) {
const pendingActions = useRef<(() => void)[]>([]);
const { update, transact } = useController(textId);
const sendTransact = useCallback(
() => {
const actions = pendingActions.current;
transact(actions);
},
[transact],
)
const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
const sendDelta = useCallback(
(delta: TextDelta[]) => {
const action = () => update(delta);
pendingActions.current.push(action);
debounceSendTransact()
},
[update, debounceSendTransact],
);
return {
sendDelta
}
}
export function useTextBlock(text: string, delta: TextDelta[]) {
const { sendDelta } = useTransact(text);
const { editor } = useBindYjs(delta, sendDelta);
export function useTextBlock(delta: TextDelta[]) {
const { editor } = useTextInput(delta);
const [value, setValue] = useState<Descendant[]>([]);
const onChange = useCallback(
(e: Descendant[]) => {
setValue(e);
},
[editor],
[editor]
);
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
@ -73,14 +20,13 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
case 'Enter': {
event.stopPropagation();
event.preventDefault();
return;
}
case 'Backspace': {
if (!editor.selection) return;
const { anchor } = editor.selection;
const isCollapase = Range.isCollapsed(editor.selection);
if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') {
const isCollapsed = Range.isCollapsed(editor.selection);
if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
event.stopPropagation();
event.preventDefault();
return;
@ -88,16 +34,15 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
}
}
triggerHotkey(event, editor);
}
};
const onDOMBeforeInput = useCallback((e: InputEvent) => {
// COMPAT: in Apple, `compositionend` is dispatched after the
// `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
// Here, prevent the beforeInput event and wait for the compositionend event to take effect
// COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition".
// It will cause repeated characters when inputting Chinese.
// Here, prevent the beforeInput event and wait for the compositionend event to take effect.
if (e.inputType === 'insertFromComposition') {
e.preventDefault();
}
}, []);
return {
@ -105,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
onKeyDownCapture,
onDOMBeforeInput,
editor,
value
}
value,
};
}

View File

@ -3,23 +3,21 @@ 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 '../HoveringToolbar';
import { TextDelta } from '@/appflowy_app/interfaces/document';
import HoveringToolbar from '../_shared/HoveringToolbar';
import React from 'react';
import { TextDelta } from '@/appflowy_app/interfaces/document';
function TextBlock({
node,
childIds,
placeholder,
delta,
...props
}: {
node: Node;
delta: TextDelta[];
node: Node & { data: { delta: TextDelta[] } };
childIds?: string[];
placeholder?: string;
} & React.HTMLAttributes<HTMLDivElement>) {
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta);
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.delta);
return (
<div {...props} className={`py-[2px] ${props.className}`}>

View File

@ -3,10 +3,10 @@ import { useRef } from 'react';
const defaultSize = 60;
export function useVirtualizerList(count: number) {
export function useVirtualizedList(count: number) {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
const virtualize = useVirtualizer({
count,
getScrollElement: () => parentRef.current,
estimateSize: () => {
@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) {
});
return {
rowVirtualizer,
virtualize,
parentRef,
};
}

View File

@ -1,10 +1,10 @@
import React from 'react';
import { useVirtualizerList } from './VirtualizerList.hooks';
import { useVirtualizedList } from './VirtualizedList.hooks';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import DocumentTitle from '../DocumentTitle';
import Overlay from '../Overlay';
export default function VirtualizerList({
export default function VirtualizedList({
childIds,
node,
renderNode,
@ -13,9 +13,8 @@ export default function VirtualizerList({
node: Node;
renderNode: (nodeId: string) => JSX.Element;
}) {
const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
const virtualItems = rowVirtualizer.getVirtualItems();
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
const virtualItems = virtualize.getVirtualItems();
return (
<>
@ -26,7 +25,7 @@ export default function VirtualizerList({
<div
className='doc-body max-w-screen w-[900px] min-w-0'
style={{
height: rowVirtualizer.getTotalSize(),
height: virtualize.getTotalSize(),
position: 'relative',
}}
>
@ -43,7 +42,7 @@ export default function VirtualizerList({
{virtualItems.map((virtualRow) => {
const id = childIds[virtualRow.index];
return (
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
{renderNode(id)}
</div>

View File

@ -1,4 +1,4 @@
import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
import { toggleFormat, isFormatActive } from '$app/utils/slate/format';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';

View File

@ -1,8 +1,6 @@
import { useEffect, useRef } from 'react';
import { useFocused, useSlate } from 'slate-react';
import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
import { calcToolbarPosition } from '$app/utils/slate/toolbar';
export function useHoveringToolbar(id: string) {
const editor = useSlate();
const inFocus = useFocused();
@ -29,6 +27,6 @@ export function useHoveringToolbar(id: string) {
return {
ref,
inFocus,
editor
}
}
editor,
};
}

View File

@ -1,5 +1,5 @@
import FormatButton from './FormatButton';
import Portal from '../BlockPortal';
import Portal from '../../BlockPortal';
import { useHoveringToolbar } from './index.hooks';
const HoveringToolbar = ({ id }: { id: string }) => {

View File

@ -1,32 +1,38 @@
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { useAppSelector } from '@/appflowy_app/stores/store';
import { useMemo } from 'react';
import { TextDelta } from '@/appflowy_app/interfaces/document';
/**
* Subscribe to a node and its children
* It will be change when the node or its children is changed
* And it will not be change when other node is changed
* @param id
*/
export function useSubscribeNode(id: string) {
const node = useAppSelector<Node>(state => state.document.nodes[id]);
const childIds = useAppSelector<string[] | undefined>(state => {
const node = useAppSelector<Node>((state) => state.document.nodes[id]);
const childIds = useAppSelector<string[] | undefined>((state) => {
const childrenId = state.document.nodes[id]?.children;
if (!childrenId) return;
return state.document.children[childrenId];
});
const delta = useAppSelector<TextDelta[] | undefined>(state => {
const deltaId = state.document.nodes[id]?.data?.text;
if (!deltaId) return;
return state.document.delta[deltaId];
});
const isSelected = useAppSelector<boolean>(state => {
const isSelected = useAppSelector<boolean>((state) => {
return state.document.selections?.includes(id) || false;
});
const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
// Memoize the node and its children
// So that the component will not be re-rendered when other node is changed
// It very important for performance
const memoizedNode = useMemo(
() => node,
[node?.id, JSON.stringify(node?.data), node?.parent, node?.type, node?.children]
);
const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
return {
node: memoizedNode,
childIds: memoizedChildIds,
delta: memoizedDelta,
isSelected
isSelected,
};
}
}

View File

@ -0,0 +1,114 @@
import { useCallback, useContext, useMemo, useRef, useEffect } from 'react';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { TextDelta } from '$app/interfaces/document';
import { debounce } from '@/appflowy_app/utils/tool';
import { createEditor } from 'slate';
import { withReact } from 'slate-react';
import * as Y from 'yjs';
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
export function useTextInput(delta: TextDelta[]) {
const { sendDelta } = useTransact();
const { editor } = useBindYjs(delta, sendDelta);
return {
editor,
};
}
function useController() {
const docController = useContext(DocumentControllerContext);
const update = useCallback(
(delta: TextDelta[]) => {
docController?.applyActions([
{
type: 'update',
payload: {
block: {
data: {
delta,
},
},
},
},
]);
},
[docController]
);
return {
update,
};
}
function useTransact() {
const { update } = useController();
const sendDelta = useCallback(
(delta: TextDelta[]) => {
update(delta);
},
[update]
);
const debounceSendDelta = useMemo(() => debounce(sendDelta, 300), [sendDelta]);
return {
sendDelta: debounceSendDelta,
};
}
const initialValue = [
{
type: 'paragraph',
children: [{ text: '' }],
},
];
function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
const yTextRef = useRef<Y.XmlText>();
// Create a yjs document and get the shared type
const sharedType = useMemo(() => {
const doc = new Y.Doc();
const _sharedType = doc.get('content', Y.XmlText) as Y.XmlText;
const insertDelta = slateNodesToInsertDelta(initialValue);
// Load the initial value into the yjs document
_sharedType.applyDelta(insertDelta);
const yText = insertDelta[0].insert as Y.XmlText;
yTextRef.current = yText;
return _sharedType;
}, []);
const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
useEffect(() => {
YjsEditor.connect(editor);
return () => {
yTextRef.current = undefined;
YjsEditor.disconnect(editor);
};
}, [editor]);
useEffect(() => {
const yText = yTextRef.current;
if (!yText) return;
const textEventHandler = (event: Y.YTextEvent) => {
const textDelta = event.target.toDelta();
console.log('delta', textDelta);
update(textDelta);
};
yText.applyDelta(delta);
yText.observe(textEventHandler);
return () => {
yText.unobserve(textEventHandler);
};
}, [delta]);
return { editor };
}

View File

@ -8,7 +8,7 @@ async function testCreateDocument() {
const document = await svc.open().then((result) => result.unwrap());
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const content = JSON.parse(document.content);
// const content = JSON.parse(document.content);
// The initial document content:
// {
// "document": {

View File

@ -10,8 +10,19 @@ export enum BlockType {
DividerBlock = 'divider',
MediaBlock = 'media',
TableBlock = 'table',
ColumnBlock = 'column'
ColumnBlock = 'column',
}
export interface HeadingBlockData {
level: number;
}
export interface TextBlockData {
delta: TextDelta[];
}
export interface PageBlockData extends TextBlockData {}
export interface NestedBlock {
id: string;
type: BlockType;
@ -26,6 +37,7 @@ export interface TextDelta {
export interface DocumentData {
rootId: string;
blocks: Record<string, NestedBlock>;
ytexts: Record<string, TextDelta[]>;
yarrays: Record<string, string[]>;
meta: {
childrenMap: Record<string, string[]>;
};
}

View File

@ -24,22 +24,7 @@ import {
export class DocumentBackendService {
constructor(public readonly viewId: string) {}
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 });
return DocumentEventGetDocument(payload);
};
applyEdit = (operations: string) => {
const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations });
return DocumentEventApplyEdit(payload);
};
close = () => {
const payload = ViewIdPB.fromObject({ value: this.viewId });
return FolderEventCloseView(payload);
};
openV2 = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
const payload = OpenDocumentPayloadPBV2.fromObject({
document_id: this.viewId,
});
@ -54,7 +39,7 @@ export class DocumentBackendService {
return DocumentEvent2ApplyAction(payload);
};
closeV2 = (): Promise<Result<void, FlowyError>> => {
close = (): Promise<Result<void, FlowyError>> => {
const payload = CloseDocumentPayloadPBV2.fromObject({
document_id: this.viewId,
});

View File

@ -1,10 +1,8 @@
import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
import { createContext } from 'react';
import { DocumentBackendService } from './document_bd_svc';
import { Err } from 'ts-results';
import { BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockPB, FlowyError } from '@/services/backend';
import { FlowyError } from '@/services/backend';
import { DocumentObserver } from './document_observer';
import { nanoid } from 'nanoid';
export const DocumentControllerContext = createContext<DocumentController | null>(null);
@ -17,7 +15,7 @@ export class DocumentController {
this.observer = new DocumentObserver(viewId);
}
open = async (): Promise<DocumentData | null> => {
open = async (): Promise<DocumentData | FlowyError> => {
// example:
await this.observer.subscribe({
didReceiveUpdate: () => {
@ -25,55 +23,39 @@ export class DocumentController {
},
});
const document = await this.backendService.openV2();
let root_id = '';
const document = await this.backendService.open();
if (document.ok) {
root_id = document.val.page_id;
console.log(document.val.blocks);
}
await this.backendService.applyActions([
BlockActionPB.fromObject({
action: BlockActionTypePB.Insert,
payload: BlockActionPayloadPB.fromObject({
block: BlockPB.fromObject({
id: nanoid(10),
ty: 'text',
parent_id: root_id,
}),
}),
}),
]);
const openDocumentResult = await this.backendService.open();
if (openDocumentResult.ok) {
console.log(document.val);
const blocks: DocumentData["blocks"] = {};
document.val.blocks.forEach((block) => {
blocks[block.id] = {
id: block.id,
type: block.ty as BlockType,
parent: block.parent_id,
children: block.children_id,
data: JSON.parse(block.data),
};
});
const childrenMap: Record<string, string[]> = {};
document.val.meta.children_map.forEach((child, key) => { childrenMap[key] = child.children; });
return {
rootId: '',
blocks: {},
ytexts: {},
yarrays: {},
};
} else {
return null;
rootId: document.val.page_id,
blocks,
meta: {
childrenMap
}
}
}
return document.val;
};
insert(
node: {
id: string;
type: BlockType;
delta?: TextDelta[];
},
parentId: string,
prevId: string
) {
//
}
transact(actions: (() => void)[]) {
//
}
yTextApply = (yTextId: string, delta: TextDelta[]) => {
applyActions = (
actions: {
type: string;
payload: any;
}[]
) => {
//
};

View File

@ -29,8 +29,8 @@ export class DocumentObserver {
};
unsubscribe = async () => {
this.appListNotifier.unsubscribe();
this.workspaceNotifier.unsubscribe();
// this.appListNotifier.unsubscribe();
// this.workspaceNotifier.unsubscribe();
await this.listener?.stop();
};
}

View File

@ -1,22 +1,12 @@
import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { RegionGrid } from "./region_grid";
import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RegionGrid } from './region_grid';
export interface Node {
id: string;
type: BlockType;
data: {
text?: string;
style?: Record<string, any>
};
parent: string | null;
children: string;
}
export type Node = NestedBlock;
export interface NodeState {
nodes: Record<string, Node>;
children: Record<string, string[]>;
delta: Record<string, TextDelta[]>;
selections: string[];
}
@ -25,7 +15,6 @@ const regionGrid = new RegionGrid(50);
const initialState: NodeState = {
nodes: {},
children: {},
delta: {},
selections: [],
};
@ -33,46 +22,56 @@ export const documentSlice = createSlice({
name: 'document',
initialState: initialState,
reducers: {
clear: (state, action: PayloadAction) => {
clear: () => {
return initialState;
},
createTree: (state, action: PayloadAction<{
nodes: Record<string, Node>;
children: Record<string, string[]>;
delta: Record<string, TextDelta[]>;
}>) => {
const { nodes, children, delta } = action.payload;
create: (
state,
action: PayloadAction<{
nodes: Record<string, Node>;
children: Record<string, string[]>;
}>
) => {
const { nodes, children } = action.payload;
state.nodes = nodes;
state.children = children;
state.delta = delta;
},
updateSelections: (state, action: PayloadAction<string[]>) => {
state.selections = action.payload;
},
changeSelectionByIntersectRect: (state, action: PayloadAction<{
startX: number;
startY: number;
endX: number;
endY: number
}>) => {
setSelectionByRect: (
state,
action: PayloadAction<{
startX: number;
startY: number;
endX: number;
endY: number;
}>
) => {
const { startX, startY, endX, endY } = action.payload;
const blocks = regionGrid.getIntersectBlocks(startX, startY, endX, endY);
state.selections = blocks.map(block => block.id);
state.selections = blocks.map((block) => block.id);
},
updateNodePosition: (state, action: PayloadAction<{id: string; rect: {
x: number;
y: number;
width: number;
height: number;
}}>) => {
updateNodePosition: (
state,
action: PayloadAction<{
id: string;
rect: {
x: number;
y: number;
width: number;
height: number;
};
}>
) => {
const { id, rect } = action.payload;
const position = {
id,
...rect
...rect,
};
regionGrid.updateBlock(id, position);
},
@ -81,13 +80,13 @@ export const documentSlice = createSlice({
state.nodes[action.payload.id] = action.payload;
},
addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => {
addChild: (state, action: PayloadAction<{ parentId: string; childId: string; prevId: string }>) => {
const { parentId, childId, prevId } = action.payload;
const parentChildrenId = state.nodes[parentId].children;
const children = state.children[parentChildrenId];
const prevIndex = children.indexOf(prevId);
if (prevIndex === -1) {
children.push(childId)
children.push(childId);
} else {
children.splice(prevIndex + 1, 0, childId);
}
@ -98,32 +97,28 @@ export const documentSlice = createSlice({
state.children[id] = childIds;
},
updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => {
const { id, delta } = action.payload;
state.delta[id] = delta;
},
updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => {
updateNode: (state, action: PayloadAction<{ id: string; data: any }>) => {
state.nodes[action.payload.id] = {
...state.nodes[action.payload.id],
...action.payload
}
...action.payload,
};
},
removeNode: (state, action: PayloadAction<string>) => {
const { children, data, parent } = state.nodes[action.payload];
// remove from parent
if (parent) {
const index = state.children[state.nodes[parent].children].indexOf(action.payload);
if (index > -1) {
state.children[state.nodes[parent].children].splice(index, 1);
}
}
// remove children
if (children) {
delete state.children[children];
}
if (data && data.text) {
delete state.delta[data.text];
}
// remove node
delete state.nodes[action.payload];
},
},

View File

@ -23,7 +23,7 @@ export const useDocument = () => {
const res = await c.open();
console.log(res)
if (!res) return;
setDocumentData(res)
// setDocumentData(res)
setDocumentId(params.id)
})();

View File

@ -14,7 +14,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use nanoid::nanoid;
use parking_lot::Mutex;
use crate::entities::{BlockMapPB, BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
#[derive(Clone)]
pub struct Document(Arc<Mutex<InnerDocument>>);
@ -96,7 +96,7 @@ impl From<DocumentDataWrapper> for DocumentDataPB2 {
.collect::<HashMap<String, ChildrenPB>>();
Self {
page_id: data.0.page_id,
blocks: BlockMapPB { blocks },
blocks,
meta: MetaPB { children_map },
}
}

View File

@ -31,18 +31,12 @@ pub struct DocumentDataPB2 {
pub page_id: String,
#[pb(index = 2)]
pub blocks: BlockMapPB,
pub blocks: HashMap<String, BlockPB>,
#[pb(index = 3)]
pub meta: MetaPB,
}
#[derive(Default, ProtoBuf)]
pub struct BlockMapPB {
#[pb(index = 1)]
pub blocks: HashMap<String, BlockPB>,
}
#[derive(Default, ProtoBuf)]
pub struct BlockPB {
#[pb(index = 1)]