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

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)]