mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: create redo/undo bridge (#2760)
* chore: create redo/undo bridge * chore: update test * chore: review update * chore: review update * chore: react redo/undo * chore: review update * chore: add test * chore: review update * chore: generate document id * chore: update undo/redo * chore: update cargo lock
This commit is contained in:
parent
27dd719aa8
commit
95f8b2e9a4
593
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
593
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,10 +6,12 @@ import BlockSlash from '$app/components/document/BlockSlash';
|
||||
import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy';
|
||||
import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
|
||||
import LinkEditPopover from '$app/components/document/_shared/TextLink/LinkEditPopover';
|
||||
import { useUndoRedo } from '$app/components/document/_shared/UndoHooks/useUndoRedo';
|
||||
|
||||
export default function Overlay({ container }: { container: HTMLDivElement }) {
|
||||
useCopy(container);
|
||||
usePaste(container);
|
||||
useUndoRedo(container);
|
||||
return (
|
||||
<>
|
||||
<BlockSideToolbar container={container} />
|
||||
|
@ -88,9 +88,11 @@ export function useKeyDown(id: string) {
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
|
||||
filteredEvents.forEach((event) => event.handler(e));
|
||||
filteredEvents.forEach((event) => {
|
||||
e.stopPropagation();
|
||||
event.handler(e);
|
||||
});
|
||||
},
|
||||
[interceptEvents]
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ export function useSlateYjs({ delta }: { delta?: Delta }) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
|
||||
const editor = useMemo(() => withReact(withYjs(createEditor(), sharedType)), []);
|
||||
|
||||
// Connect editor in useEffect to comply with concurrent mode requirements.
|
||||
useEffect(() => {
|
||||
|
@ -0,0 +1,39 @@
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { Keyboard } from '@/appflowy_app/constants/document/keyboard';
|
||||
|
||||
export function useUndoRedo(container: HTMLDivElement) {
|
||||
const controller = useContext(DocumentControllerContext);
|
||||
|
||||
const onUndo = useCallback(() => {
|
||||
if (!controller) return;
|
||||
controller.undo();
|
||||
}, [controller]);
|
||||
|
||||
const onRedo = useCallback(() => {
|
||||
if (!controller) return;
|
||||
controller.redo();
|
||||
}, [controller]);
|
||||
|
||||
const handleKeyDownCapture = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (isHotkey(Keyboard.keys.UNDO, e)) {
|
||||
e.stopPropagation();
|
||||
onUndo();
|
||||
}
|
||||
if (isHotkey(Keyboard.keys.REDO, e)) {
|
||||
e.stopPropagation();
|
||||
onRedo();
|
||||
}
|
||||
},
|
||||
[onRedo, onUndo]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
container.addEventListener('keydown', handleKeyDownCapture, true);
|
||||
return () => {
|
||||
container.removeEventListener('keydown', handleKeyDownCapture, true);
|
||||
};
|
||||
}, [container, handleKeyDownCapture]);
|
||||
}
|
@ -10,8 +10,6 @@ import { AppObserver } from '$app/stores/effects/folder/app/app_observer';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
|
||||
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
@ -118,9 +116,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
layoutType: ViewLayoutPB.Document,
|
||||
});
|
||||
try {
|
||||
const c = new DocumentController(newView.id);
|
||||
await c.create();
|
||||
await c.dispose();
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
folderId: folder.id,
|
||||
|
@ -38,5 +38,7 @@ export const Keyboard = {
|
||||
COPY: 'Mod+c',
|
||||
CUT: 'Mod+x',
|
||||
PASTE: 'Mod+v',
|
||||
REDO: 'Mod+Shift+z',
|
||||
UNDO: 'Mod+z',
|
||||
},
|
||||
};
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
ApplyActionPayloadPB,
|
||||
BlockActionPB,
|
||||
CloseDocumentPayloadPB,
|
||||
DocumentRedoUndoPayloadPB,
|
||||
DocumentRedoUndoResponsePB,
|
||||
} from '@/services/backend';
|
||||
import { Result } from 'ts-results';
|
||||
import {
|
||||
@ -13,18 +15,14 @@ import {
|
||||
DocumentEventCloseDocument,
|
||||
DocumentEventOpenDocument,
|
||||
DocumentEventCreateDocument,
|
||||
DocumentEventCanUndoRedo,
|
||||
DocumentEventRedo,
|
||||
DocumentEventUndo,
|
||||
} from '@/services/backend/events/flowy-document2';
|
||||
|
||||
export class DocumentBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
create = (): Promise<Result<void, FlowyError>> => {
|
||||
const payload = CreateDocumentPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEventCreateDocument(payload);
|
||||
};
|
||||
|
||||
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
@ -46,4 +44,25 @@ export class DocumentBackendService {
|
||||
});
|
||||
return DocumentEventCloseDocument(payload);
|
||||
};
|
||||
|
||||
canUndoRedo = (): Promise<Result<DocumentRedoUndoResponsePB, FlowyError>> => {
|
||||
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEventCanUndoRedo(payload);
|
||||
};
|
||||
|
||||
undo = (): Promise<Result<DocumentRedoUndoResponsePB, FlowyError>> => {
|
||||
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEventUndo(payload);
|
||||
};
|
||||
|
||||
redo = (): Promise<Result<DocumentRedoUndoResponsePB, FlowyError>> => {
|
||||
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEventRedo(payload);
|
||||
};
|
||||
}
|
||||
|
@ -31,13 +31,6 @@ export class DocumentController {
|
||||
this.observer = new DocumentObserver(documentId);
|
||||
}
|
||||
|
||||
create = async (): Promise<FlowyError | void> => {
|
||||
const result = await this.backendService.create();
|
||||
if (result.ok) {
|
||||
return;
|
||||
}
|
||||
return result.val;
|
||||
};
|
||||
open = async (): Promise<DocumentData> => {
|
||||
await this.observer.subscribe({
|
||||
didReceiveUpdate: this.updated,
|
||||
@ -114,6 +107,26 @@ export class DocumentController {
|
||||
};
|
||||
};
|
||||
|
||||
canUndo = async () => {
|
||||
const result = await this.backendService.canUndoRedo();
|
||||
return result.ok && result.val.can_undo;
|
||||
};
|
||||
|
||||
canRedo = async () => {
|
||||
const result = await this.backendService.canUndoRedo();
|
||||
return result.ok && result.val.can_redo;
|
||||
};
|
||||
|
||||
undo = async () => {
|
||||
const result = await this.backendService.undo();
|
||||
return result.ok && result.val.is_success;
|
||||
};
|
||||
|
||||
redo = async () => {
|
||||
const result = await this.backendService.redo();
|
||||
return result.ok && result.val.is_success;
|
||||
};
|
||||
|
||||
dispose = async () => {
|
||||
this.onDocChange = undefined;
|
||||
await this.backendService.close();
|
||||
|
2
frontend/rust-lib/Cargo.lock
generated
2
frontend/rust-lib/Cargo.lock
generated
@ -1710,6 +1710,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber 0.3.16",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1867,6 +1868,7 @@ dependencies = [
|
||||
"dotenv",
|
||||
"flowy-core",
|
||||
"flowy-database2",
|
||||
"flowy-document2",
|
||||
"flowy-folder2",
|
||||
"flowy-net",
|
||||
"flowy-notification",
|
||||
|
@ -143,7 +143,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
|
||||
let manager = self.0.clone();
|
||||
let view_id = view_id.to_string();
|
||||
FutureResult::new(async move {
|
||||
let document = manager.get_document(view_id)?;
|
||||
let document = manager.get_document_from_disk(view_id)?;
|
||||
let data: DocumentDataPB = document.lock().get_document()?.into();
|
||||
let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?;
|
||||
Ok(data_bytes)
|
||||
|
@ -27,6 +27,7 @@ tracing = { version = "0.1", features = ["log"] }
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
indexmap = {version = "1.9.2", features = ["serde"]}
|
||||
uuid = { version = "1.3.3", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.4.0"
|
||||
|
@ -1,6 +1,9 @@
|
||||
use collab_document::blocks::{BlockAction, DocumentData};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::parse::{NotEmptyStr, NotEmptyVec};
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct OpenDocumentPayloadPB {
|
||||
@ -8,6 +11,54 @@ pub struct OpenDocumentPayloadPB {
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
pub struct OpenDocumentParams {
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
impl TryInto<OpenDocumentParams> for OpenDocumentPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<OpenDocumentParams, Self::Error> {
|
||||
let document_id =
|
||||
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||
Ok(OpenDocumentParams {
|
||||
document_id: document_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct DocumentRedoUndoPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
pub struct DocumentRedoUndoParams {
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
impl TryInto<DocumentRedoUndoParams> for DocumentRedoUndoPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<DocumentRedoUndoParams, Self::Error> {
|
||||
let document_id =
|
||||
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||
Ok(DocumentRedoUndoParams {
|
||||
document_id: document_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, ProtoBuf)]
|
||||
pub struct DocumentRedoUndoResponsePB {
|
||||
#[pb(index = 1)]
|
||||
pub can_undo: bool,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub can_redo: bool,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub is_success: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct CreateDocumentPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
@ -17,12 +68,45 @@ pub struct CreateDocumentPayloadPB {
|
||||
pub initial_data: Option<DocumentDataPB>,
|
||||
}
|
||||
|
||||
pub struct CreateDocumentParams {
|
||||
pub document_id: String,
|
||||
pub initial_data: Option<DocumentData>,
|
||||
}
|
||||
|
||||
impl TryInto<CreateDocumentParams> for CreateDocumentPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<CreateDocumentParams, Self::Error> {
|
||||
let document_id =
|
||||
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||
let initial_data = self.initial_data.map(|data| data.into());
|
||||
Ok(CreateDocumentParams {
|
||||
document_id: document_id.0,
|
||||
initial_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct CloseDocumentPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
pub struct CloseDocumentParams {
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
impl TryInto<CloseDocumentParams> for CloseDocumentPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<CloseDocumentParams, Self::Error> {
|
||||
let document_id =
|
||||
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||
Ok(CloseDocumentParams {
|
||||
document_id: document_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf, Debug)]
|
||||
pub struct ApplyActionPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
@ -32,11 +116,23 @@ pub struct ApplyActionPayloadPB {
|
||||
pub actions: Vec<BlockActionPB>,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct GetDocumentDataPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub struct ApplyActionParams {
|
||||
pub document_id: String,
|
||||
// Support customize initial data
|
||||
pub actions: Vec<BlockAction>,
|
||||
}
|
||||
|
||||
impl TryInto<ApplyActionParams> for ApplyActionPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<ApplyActionParams, Self::Error> {
|
||||
let document_id =
|
||||
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||
let actions = NotEmptyVec::parse(self.actions).map_err(|_| ErrorCode::ApplyActionsIsEmpty)?;
|
||||
let actions = actions.0.into_iter().map(BlockAction::from).collect();
|
||||
Ok(ApplyActionParams {
|
||||
document_id: document_id.0,
|
||||
actions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, ProtoBuf)]
|
||||
@ -226,3 +322,17 @@ pub struct ConvertDataPayloadPB {
|
||||
#[pb(index = 2)]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct ConvertDataParams {
|
||||
pub convert_type: ConvertType,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TryInto<ConvertDataParams> for ConvertDataPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<ConvertDataParams, Self::Error> {
|
||||
let convert_type = self.convert_type;
|
||||
let data = self.data;
|
||||
Ok(ConvertDataParams { convert_type, data })
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,16 @@ use collab_document::blocks::{
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||
|
||||
use crate::entities::{
|
||||
ApplyActionParams, CloseDocumentParams, ConvertDataParams, CreateDocumentParams,
|
||||
DocumentRedoUndoParams, OpenDocumentParams,
|
||||
};
|
||||
use crate::{
|
||||
entities::{
|
||||
ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
||||
BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, ConvertDataPayloadPB, ConvertType,
|
||||
CreateDocumentPayloadPB, DeltaTypePB, DocEventPB, DocumentDataPB, OpenDocumentPayloadPB,
|
||||
CreateDocumentPayloadPB, DeltaTypePB, DocEventPB, DocumentDataPB, DocumentRedoUndoPayloadPB,
|
||||
DocumentRedoUndoResponsePB, OpenDocumentPayloadPB,
|
||||
},
|
||||
manager::DocumentManager,
|
||||
parser::json::parser::JsonToDocumentParser,
|
||||
@ -29,9 +34,8 @@ pub(crate) async fn create_document_handler(
|
||||
data: AFPluginData<CreateDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let data = data.into_inner();
|
||||
let initial_data = data.initial_data.map(|data| data.into());
|
||||
manager.create_document(data.document_id, initial_data)?;
|
||||
let params: CreateDocumentParams = data.into_inner().try_into()?;
|
||||
manager.create_document(params.document_id, params.initial_data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -40,8 +44,9 @@ pub(crate) async fn open_document_handler(
|
||||
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||
let context = data.into_inner();
|
||||
let document = manager.open_document(context.document_id)?;
|
||||
let params: OpenDocumentParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
let document = manager.get_or_open_document(doc_id)?;
|
||||
let document_data = document.lock().get_document()?;
|
||||
data_result_ok(DocumentDataPB::from(document_data))
|
||||
}
|
||||
@ -50,8 +55,9 @@ pub(crate) async fn close_document_handler(
|
||||
data: AFPluginData<CloseDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let context = data.into_inner();
|
||||
manager.close_document(&context.document_id)?;
|
||||
let params: CloseDocumentParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
manager.close_document(&doc_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -61,8 +67,9 @@ pub(crate) async fn get_document_data_handler(
|
||||
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||
let context = data.into_inner();
|
||||
let document = manager.get_document(context.document_id)?;
|
||||
let params: OpenDocumentParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
let document = manager.get_document_from_disk(doc_id)?;
|
||||
let document_data = document.lock().get_document()?;
|
||||
data_result_ok(DocumentDataPB::from(document_data))
|
||||
}
|
||||
@ -72,10 +79,10 @@ pub(crate) async fn apply_action_handler(
|
||||
data: AFPluginData<ApplyActionPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let context = data.into_inner();
|
||||
let doc_id = context.document_id;
|
||||
let document = manager.open_document(doc_id)?;
|
||||
let actions = context.actions.into_iter().map(BlockAction::from).collect();
|
||||
let params: ApplyActionParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
let document = manager.get_or_open_document(doc_id)?;
|
||||
let actions = params.actions;
|
||||
document.lock().apply_action(actions);
|
||||
Ok(())
|
||||
}
|
||||
@ -92,8 +99,9 @@ pub(crate) async fn convert_data_to_document(
|
||||
pub fn convert_data_to_document_internal(
|
||||
payload: ConvertDataPayloadPB,
|
||||
) -> Result<DocumentDataPB, FlowyError> {
|
||||
let convert_type = payload.convert_type;
|
||||
let data = payload.data;
|
||||
let params: ConvertDataParams = payload.try_into()?;
|
||||
let convert_type = params.convert_type;
|
||||
let data = params.data;
|
||||
match convert_type {
|
||||
ConvertType::Json => {
|
||||
let json_str = String::from_utf8(data).map_err(|_| FlowyError::invalid_data())?;
|
||||
@ -103,6 +111,60 @@ pub fn convert_data_to_document_internal(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn redo_handler(
|
||||
data: AFPluginData<DocumentRedoUndoPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentRedoUndoResponsePB, FlowyError> {
|
||||
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
let document = manager.get_or_open_document(doc_id)?;
|
||||
let document = document.lock();
|
||||
let redo = document.redo();
|
||||
let can_redo = document.can_redo();
|
||||
let can_undo = document.can_undo();
|
||||
data_result_ok(DocumentRedoUndoResponsePB {
|
||||
can_redo,
|
||||
can_undo,
|
||||
is_success: redo,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn undo_handler(
|
||||
data: AFPluginData<DocumentRedoUndoPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentRedoUndoResponsePB, FlowyError> {
|
||||
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
let document = manager.get_or_open_document(doc_id)?;
|
||||
let document = document.lock();
|
||||
let undo = document.undo();
|
||||
let can_redo = document.can_redo();
|
||||
let can_undo = document.can_undo();
|
||||
data_result_ok(DocumentRedoUndoResponsePB {
|
||||
can_redo,
|
||||
can_undo,
|
||||
is_success: undo,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn can_undo_redo_handler(
|
||||
data: AFPluginData<DocumentRedoUndoPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentRedoUndoResponsePB, FlowyError> {
|
||||
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
||||
let doc_id = params.document_id;
|
||||
let document = manager.get_or_open_document(doc_id)?;
|
||||
let document = document.lock();
|
||||
let can_redo = document.can_redo();
|
||||
let can_undo = document.can_undo();
|
||||
drop(document);
|
||||
data_result_ok(DocumentRedoUndoResponsePB {
|
||||
can_redo,
|
||||
can_undo,
|
||||
is_success: true,
|
||||
})
|
||||
}
|
||||
|
||||
impl From<BlockActionPB> for BlockAction {
|
||||
fn from(pb: BlockActionPB) -> Self {
|
||||
Self {
|
||||
|
@ -6,8 +6,9 @@ use lib_dispatch::prelude::AFPlugin;
|
||||
|
||||
use crate::{
|
||||
event_handler::{
|
||||
apply_action_handler, close_document_handler, convert_data_to_document,
|
||||
create_document_handler, get_document_data_handler, open_document_handler,
|
||||
apply_action_handler, can_undo_redo_handler, close_document_handler, convert_data_to_document,
|
||||
create_document_handler, get_document_data_handler, open_document_handler, redo_handler,
|
||||
undo_handler,
|
||||
},
|
||||
manager::DocumentManager,
|
||||
};
|
||||
@ -26,6 +27,9 @@ pub fn init(document_manager: Arc<DocumentManager>) -> AFPlugin {
|
||||
DocumentEvent::ConvertDataToDocument,
|
||||
convert_data_to_document,
|
||||
);
|
||||
plugin = plugin.event(DocumentEvent::Redo, redo_handler);
|
||||
plugin = plugin.event(DocumentEvent::Undo, undo_handler);
|
||||
plugin = plugin.event(DocumentEvent::CanUndoRedo, can_undo_redo_handler);
|
||||
|
||||
plugin
|
||||
}
|
||||
@ -45,9 +49,27 @@ pub enum DocumentEvent {
|
||||
#[event(input = "ApplyActionPayloadPB")]
|
||||
ApplyAction = 3,
|
||||
|
||||
#[event(input = "GetDocumentDataPayloadPB")]
|
||||
#[event(input = "OpenDocumentPayloadPB")]
|
||||
GetDocumentData = 4,
|
||||
|
||||
#[event(input = "ConvertDataPayloadPB", output = "DocumentDataPB")]
|
||||
ConvertDataToDocument = 5,
|
||||
|
||||
#[event(
|
||||
input = "DocumentRedoUndoPayloadPB",
|
||||
output = "DocumentRedoUndoResponsePB"
|
||||
)]
|
||||
Redo = 6,
|
||||
|
||||
#[event(
|
||||
input = "DocumentRedoUndoPayloadPB",
|
||||
output = "DocumentRedoUndoResponsePB"
|
||||
)]
|
||||
Undo = 7,
|
||||
|
||||
#[event(
|
||||
input = "DocumentRedoUndoPayloadPB",
|
||||
output = "DocumentRedoUndoResponsePB"
|
||||
)]
|
||||
CanUndoRedo = 8,
|
||||
}
|
||||
|
@ -9,3 +9,4 @@ pub mod parser;
|
||||
pub mod protobuf;
|
||||
|
||||
mod notification;
|
||||
mod parse;
|
||||
|
@ -54,16 +54,15 @@ impl DocumentManager {
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
pub fn open_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
|
||||
/// get document
|
||||
/// read the existing document from the map if it exists, otherwise read it from the disk and write it to the map.
|
||||
pub fn get_or_open_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
|
||||
if let Some(doc) = self.documents.read().get(&doc_id) {
|
||||
return Ok(doc.clone());
|
||||
}
|
||||
tracing::debug!("open_document: {:?}", &doc_id);
|
||||
let uid = self.user.user_id()?;
|
||||
let db = self.user.collab_db()?;
|
||||
let collab = self.collab_builder.build(uid, &doc_id, "document", db);
|
||||
// read the existing document from the disk.
|
||||
let document = Arc::new(Document::new(collab)?);
|
||||
let document = self.get_document_from_disk(doc_id.clone())?;
|
||||
// save the document to the memory and read it from the memory if we open the same document again.
|
||||
// and we don't want to subscribe to the document changes if we open the same document again.
|
||||
self
|
||||
@ -87,7 +86,9 @@ impl DocumentManager {
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
pub fn get_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
|
||||
/// get document
|
||||
/// read the existing document from the disk.
|
||||
pub fn get_document_from_disk(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
|
||||
let uid = self.user.user_id()?;
|
||||
let db = self.user.collab_db()?;
|
||||
let collab = self.collab_builder.build(uid, &doc_id, "document", db);
|
||||
|
23
frontend/rust-lib/flowy-document2/src/parse.rs
Normal file
23
frontend/rust-lib/flowy-document2/src/parse.rs
Normal file
@ -0,0 +1,23 @@
|
||||
#[derive(Debug)]
|
||||
pub struct NotEmptyStr(pub String);
|
||||
|
||||
impl NotEmptyStr {
|
||||
pub fn parse(s: String) -> Result<Self, String> {
|
||||
if s.trim().is_empty() {
|
||||
return Err("Input string is empty".to_owned());
|
||||
}
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotEmptyVec<T>(pub Vec<T>);
|
||||
|
||||
impl<T> NotEmptyVec<T> {
|
||||
pub fn parse(v: Vec<T>) -> Result<Self, String> {
|
||||
if v.is_empty() {
|
||||
return Err("Input vector is empty".to_owned());
|
||||
}
|
||||
Ok(Self(v))
|
||||
}
|
||||
}
|
@ -1,25 +1,21 @@
|
||||
use std::{collections::HashMap, sync::Arc, vec};
|
||||
use std::{collections::HashMap, vec};
|
||||
|
||||
use crate::document::util::default_collab_builder;
|
||||
use crate::document::util;
|
||||
use crate::document::util::gen_id;
|
||||
use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
|
||||
use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
|
||||
use flowy_document2::document_data::default_document_data;
|
||||
use flowy_document2::{document::Document, manager::DocumentManager};
|
||||
use nanoid::nanoid;
|
||||
|
||||
use super::util::FakeUser;
|
||||
|
||||
#[test]
|
||||
fn document_apply_insert_block_with_empty_parent_id() {
|
||||
let (_, document, page_id) = create_and_open_empty_document();
|
||||
let (_, document, page_id) = util::create_and_open_empty_document();
|
||||
|
||||
// create a text block with no parent
|
||||
let text_block_id = nanoid!(10);
|
||||
let text_block_id = gen_id();
|
||||
let text_block = Block {
|
||||
id: text_block_id.clone(),
|
||||
ty: PARAGRAPH_BLOCK_TYPE.to_string(),
|
||||
parent: "".to_string(),
|
||||
children: nanoid!(10),
|
||||
children: gen_id(),
|
||||
external_id: None,
|
||||
external_type: None,
|
||||
data: HashMap::new(),
|
||||
@ -38,20 +34,3 @@ fn document_apply_insert_block_with_empty_parent_id() {
|
||||
let block = document.lock().get_block(&text_block_id).unwrap();
|
||||
assert_eq!(block.parent, page_id);
|
||||
}
|
||||
|
||||
fn create_and_open_empty_document() -> (DocumentManager, Arc<Document>, String) {
|
||||
let user = FakeUser::new();
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
let doc_id: String = nanoid!(10);
|
||||
let data = default_document_data();
|
||||
|
||||
// create a document
|
||||
_ = manager
|
||||
.create_document(doc_id.clone(), Some(data.clone()))
|
||||
.unwrap();
|
||||
|
||||
let document = manager.open_document(doc_id).unwrap();
|
||||
|
||||
(manager, document, data.page_id)
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser};
|
||||
use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
|
||||
use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
|
||||
use flowy_document2::document_data::default_document_data;
|
||||
use flowy_document2::manager::DocumentManager;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_redo_test() {
|
||||
let user = FakeUser::new();
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
let doc_id: String = gen_document_id();
|
||||
let data = default_document_data();
|
||||
|
||||
// create a document
|
||||
_ = manager.create_document(doc_id.clone(), Some(data.clone()));
|
||||
|
||||
// open a document
|
||||
let document = manager.get_or_open_document(doc_id.clone()).unwrap();
|
||||
let document = document.lock();
|
||||
let page_block = document.get_block(&data.page_id).unwrap();
|
||||
let page_id = page_block.id;
|
||||
let text_block_id = gen_id();
|
||||
|
||||
// insert a text block
|
||||
let text_block = Block {
|
||||
id: text_block_id.clone(),
|
||||
ty: PARAGRAPH_BLOCK_TYPE.to_string(),
|
||||
parent: page_id.clone(),
|
||||
children: gen_id(),
|
||||
external_id: None,
|
||||
external_type: None,
|
||||
data: HashMap::new(),
|
||||
};
|
||||
let insert_text_action = BlockAction {
|
||||
action: BlockActionType::Insert,
|
||||
payload: BlockActionPayload {
|
||||
block: text_block,
|
||||
parent_id: Some(page_id.clone()),
|
||||
prev_id: None,
|
||||
},
|
||||
};
|
||||
document.apply_action(vec![insert_text_action]);
|
||||
|
||||
let can_undo = document.can_undo();
|
||||
assert_eq!(can_undo, true);
|
||||
// undo the insert
|
||||
let undo = document.undo();
|
||||
assert_eq!(undo, true);
|
||||
assert_eq!(document.get_block(&text_block_id), None);
|
||||
|
||||
let can_redo = document.can_redo();
|
||||
assert!(can_redo);
|
||||
// redo the insert
|
||||
let redo = document.redo();
|
||||
assert_eq!(redo, true);
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
use std::{collections::HashMap, sync::Arc, vec};
|
||||
|
||||
use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
|
||||
use nanoid::nanoid;
|
||||
use serde_json::{json, to_value, Value};
|
||||
|
||||
use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
|
||||
use flowy_document2::document_data::default_document_data;
|
||||
use flowy_document2::manager::DocumentManager;
|
||||
|
||||
use crate::document::util::default_collab_builder;
|
||||
use crate::document::util::{default_collab_builder, gen_document_id, gen_id};
|
||||
|
||||
use super::util::FakeUser;
|
||||
|
||||
@ -18,7 +17,7 @@ fn restore_document() {
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
// create a document
|
||||
let doc_id: String = nanoid!(10);
|
||||
let doc_id: String = gen_document_id();
|
||||
let data = default_document_data();
|
||||
let document_a = manager
|
||||
.create_document(doc_id.clone(), Some(data.clone()))
|
||||
@ -28,7 +27,7 @@ fn restore_document() {
|
||||
|
||||
// open a document
|
||||
let data_b = manager
|
||||
.open_document(doc_id.clone())
|
||||
.get_or_open_document(doc_id.clone())
|
||||
.unwrap()
|
||||
.lock()
|
||||
.get_document()
|
||||
@ -41,7 +40,7 @@ fn restore_document() {
|
||||
_ = manager.create_document(doc_id.clone(), Some(data.clone()));
|
||||
// open a document
|
||||
let data_b = manager
|
||||
.open_document(doc_id.clone())
|
||||
.get_or_open_document(doc_id.clone())
|
||||
.unwrap()
|
||||
.lock()
|
||||
.get_document()
|
||||
@ -57,22 +56,22 @@ fn document_apply_insert_action() {
|
||||
let user = FakeUser::new();
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
let doc_id: String = nanoid!(10);
|
||||
let doc_id: String = gen_document_id();
|
||||
let data = default_document_data();
|
||||
|
||||
// create a document
|
||||
_ = manager.create_document(doc_id.clone(), Some(data.clone()));
|
||||
|
||||
// open a document
|
||||
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||
let document = manager.get_or_open_document(doc_id.clone()).unwrap();
|
||||
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
||||
|
||||
// insert a text block
|
||||
let text_block = Block {
|
||||
id: nanoid!(10),
|
||||
id: gen_id(),
|
||||
ty: PARAGRAPH_BLOCK_TYPE.to_string(),
|
||||
parent: page_block.id,
|
||||
children: nanoid!(10),
|
||||
children: gen_id(),
|
||||
external_id: None,
|
||||
external_type: None,
|
||||
data: HashMap::new(),
|
||||
@ -92,7 +91,7 @@ fn document_apply_insert_action() {
|
||||
|
||||
// re-open the document
|
||||
let data_b = manager
|
||||
.open_document(doc_id.clone())
|
||||
.get_or_open_document(doc_id.clone())
|
||||
.unwrap()
|
||||
.lock()
|
||||
.get_document()
|
||||
@ -108,14 +107,14 @@ fn document_apply_update_page_action() {
|
||||
let user = FakeUser::new();
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
let doc_id: String = nanoid!(10);
|
||||
let doc_id: String = gen_document_id();
|
||||
let data = default_document_data();
|
||||
|
||||
// create a document
|
||||
_ = manager.create_document(doc_id.clone(), Some(data.clone()));
|
||||
|
||||
// open a document
|
||||
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||
let document = manager.get_or_open_document(doc_id.clone()).unwrap();
|
||||
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
||||
|
||||
let mut page_block_clone = page_block;
|
||||
@ -139,7 +138,7 @@ fn document_apply_update_page_action() {
|
||||
_ = manager.close_document(&doc_id);
|
||||
|
||||
// re-open the document
|
||||
let document = manager.open_document(doc_id).unwrap();
|
||||
let document = manager.get_or_open_document(doc_id).unwrap();
|
||||
let page_block_new = document.lock().get_block(&data.page_id).unwrap();
|
||||
assert_eq!(page_block_old, page_block_new);
|
||||
assert!(page_block_new.data.contains_key("delta"));
|
||||
@ -150,23 +149,23 @@ fn document_apply_update_action() {
|
||||
let user = FakeUser::new();
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
let doc_id: String = nanoid!(10);
|
||||
let doc_id: String = gen_document_id();
|
||||
let data = default_document_data();
|
||||
|
||||
// create a document
|
||||
_ = manager.create_document(doc_id.clone(), Some(data.clone()));
|
||||
|
||||
// open a document
|
||||
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||
let document = manager.get_or_open_document(doc_id.clone()).unwrap();
|
||||
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
||||
|
||||
// insert a text block
|
||||
let text_block_id = nanoid!(10);
|
||||
let text_block_id = gen_id();
|
||||
let text_block = Block {
|
||||
id: text_block_id.clone(),
|
||||
ty: PARAGRAPH_BLOCK_TYPE.to_string(),
|
||||
parent: page_block.id,
|
||||
children: nanoid!(10),
|
||||
children: gen_id(),
|
||||
external_id: None,
|
||||
external_type: None,
|
||||
data: HashMap::new(),
|
||||
@ -207,7 +206,7 @@ fn document_apply_update_action() {
|
||||
_ = manager.close_document(&doc_id);
|
||||
|
||||
// re-open the document
|
||||
let document = manager.open_document(doc_id.clone()).unwrap();
|
||||
let document = manager.get_or_open_document(doc_id.clone()).unwrap();
|
||||
let block = document.lock().get_block(&text_block_id).unwrap();
|
||||
assert_eq!(block.data, updated_text_block_data);
|
||||
// close a document
|
||||
|
@ -1,4 +1,5 @@
|
||||
mod document_insert_test;
|
||||
mod document_redo_undo_test;
|
||||
mod document_test;
|
||||
mod event_handler_test;
|
||||
mod util;
|
||||
|
@ -3,11 +3,14 @@ use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CloudStorageType
|
||||
use std::sync::Arc;
|
||||
|
||||
use appflowy_integrate::RocksCollabDB;
|
||||
use flowy_document2::document::Document;
|
||||
use parking_lot::Once;
|
||||
use tempfile::TempDir;
|
||||
use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
use flowy_document2::manager::DocumentUser;
|
||||
use flowy_document2::document_data::default_document_data;
|
||||
use flowy_document2::manager::{DocumentManager, DocumentUser};
|
||||
use nanoid::nanoid;
|
||||
|
||||
pub struct FakeUser {
|
||||
kv: Arc<RocksCollabDB>,
|
||||
@ -53,3 +56,29 @@ pub fn default_collab_builder() -> Arc<AppFlowyCollabBuilder> {
|
||||
let builder = AppFlowyCollabBuilder::new(CloudStorageType::Local, None);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn create_and_open_empty_document() -> (DocumentManager, Arc<Document>, String) {
|
||||
let user = FakeUser::new();
|
||||
let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
|
||||
|
||||
let doc_id: String = gen_document_id();
|
||||
let data = default_document_data();
|
||||
|
||||
// create a document
|
||||
_ = manager
|
||||
.create_document(doc_id.clone(), Some(data.clone()))
|
||||
.unwrap();
|
||||
|
||||
let document = manager.get_or_open_document(doc_id).unwrap();
|
||||
|
||||
(manager, document, data.page_id)
|
||||
}
|
||||
|
||||
pub fn gen_document_id() -> String {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
uuid.to_string()
|
||||
}
|
||||
|
||||
pub fn gen_id() -> String {
|
||||
nanoid!(10)
|
||||
}
|
||||
|
@ -202,6 +202,12 @@ pub enum ErrorCode {
|
||||
|
||||
#[error("Only one application can access the database")]
|
||||
MultipleDBInstance = 66,
|
||||
|
||||
#[error("Document id is empty")]
|
||||
DocumentIdIsEmpty = 67,
|
||||
|
||||
#[error("Apply actions is empty")]
|
||||
ApplyActionsIsEmpty = 68,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
|
@ -11,6 +11,7 @@ flowy-user = { path = "../flowy-user"}
|
||||
flowy-net = { path = "../flowy-net"}
|
||||
flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] }
|
||||
flowy-database2 = { path = "../flowy-database2" }
|
||||
flowy-document2 = { path = "../flowy-document2" }
|
||||
lib-dispatch = { path = "../lib-dispatch" }
|
||||
lib-ot = { path = "../../../shared-lib/lib-ot" }
|
||||
lib-infra = { path = "../../../shared-lib/lib-infra" }
|
||||
|
107
frontend/rust-lib/flowy-test/src/document_event.rs
Normal file
107
frontend/rust-lib/flowy-test/src/document_event.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use crate::event_builder::EventBuilder;
|
||||
use crate::FlowyCoreTest;
|
||||
use flowy_document2::entities::*;
|
||||
use flowy_document2::event_map::DocumentEvent;
|
||||
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
||||
use flowy_folder2::event_map::FolderEvent;
|
||||
|
||||
pub struct DocumentEventTest {
|
||||
inner: FlowyCoreTest,
|
||||
}
|
||||
|
||||
pub struct OpenDocumentData {
|
||||
pub id: String,
|
||||
pub data: DocumentDataPB,
|
||||
}
|
||||
|
||||
impl DocumentEventTest {
|
||||
pub async fn new() -> Self {
|
||||
let sdk = FlowyCoreTest::new_with_user().await;
|
||||
Self { inner: sdk }
|
||||
}
|
||||
|
||||
pub async fn create_document(&self) -> ViewPB {
|
||||
let core = &self.inner;
|
||||
let current_workspace = core.get_current_workspace().await.workspace;
|
||||
let parent_id = current_workspace.id.clone();
|
||||
|
||||
let payload = CreateViewPayloadPB {
|
||||
parent_view_id: parent_id.to_string(),
|
||||
name: "document".to_string(),
|
||||
desc: "".to_string(),
|
||||
thumbnail: None,
|
||||
layout: ViewLayoutPB::Document,
|
||||
initial_data: vec![],
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
};
|
||||
EventBuilder::new(core.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<ViewPB>()
|
||||
}
|
||||
|
||||
pub async fn open_document(&self, doc_id: String) -> OpenDocumentData {
|
||||
let core = &self.inner;
|
||||
let payload = OpenDocumentPayloadPB {
|
||||
document_id: doc_id.clone(),
|
||||
};
|
||||
let data = EventBuilder::new(core.clone())
|
||||
.event(DocumentEvent::OpenDocument)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<DocumentDataPB>();
|
||||
OpenDocumentData { id: doc_id, data }
|
||||
}
|
||||
|
||||
pub async fn apply_actions(&self, payload: ApplyActionPayloadPB) {
|
||||
let core = &self.inner;
|
||||
EventBuilder::new(core.clone())
|
||||
.event(DocumentEvent::ApplyAction)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn undo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
|
||||
let core = &self.inner;
|
||||
let payload = DocumentRedoUndoPayloadPB {
|
||||
document_id: doc_id.clone(),
|
||||
};
|
||||
EventBuilder::new(core.clone())
|
||||
.event(DocumentEvent::Undo)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<DocumentRedoUndoResponsePB>()
|
||||
}
|
||||
|
||||
pub async fn redo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
|
||||
let core = &self.inner;
|
||||
let payload = DocumentRedoUndoPayloadPB {
|
||||
document_id: doc_id.clone(),
|
||||
};
|
||||
EventBuilder::new(core.clone())
|
||||
.event(DocumentEvent::Redo)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<DocumentRedoUndoResponsePB>()
|
||||
}
|
||||
|
||||
pub async fn can_undo_redo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
|
||||
let core = &self.inner;
|
||||
let payload = DocumentRedoUndoPayloadPB {
|
||||
document_id: doc_id.clone(),
|
||||
};
|
||||
EventBuilder::new(core.clone())
|
||||
.event(DocumentEvent::CanUndoRedo)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<DocumentRedoUndoResponsePB>()
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ use flowy_user::errors::FlowyError;
|
||||
use crate::event_builder::EventBuilder;
|
||||
use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
|
||||
|
||||
pub mod document_event;
|
||||
pub mod event_builder;
|
||||
pub mod folder_event;
|
||||
pub mod user_event;
|
||||
|
2
frontend/rust-lib/flowy-test/tests/document/mod.rs
Normal file
2
frontend/rust-lib/flowy-test/tests/document/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod test;
|
||||
mod utils;
|
62
frontend/rust-lib/flowy-test/tests/document/test.rs
Normal file
62
frontend/rust-lib/flowy-test/tests/document/test.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use crate::document::utils::*;
|
||||
use flowy_document2::entities::*;
|
||||
use flowy_test::document_event::DocumentEventTest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_document_event_test() {
|
||||
let test = DocumentEventTest::new().await;
|
||||
let view = test.create_document().await;
|
||||
let document = test.open_document(view.id).await;
|
||||
let document_data = document.data;
|
||||
assert!(!document_data.page_id.is_empty());
|
||||
assert!(document_data.blocks.len() > 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_document_event_test() {
|
||||
let test = DocumentEventTest::new().await;
|
||||
let view = test.create_document().await;
|
||||
let doc_id = view.id.clone();
|
||||
let document = test.open_document(doc_id.clone()).await;
|
||||
let block_count = document.data.blocks.len();
|
||||
let insert_action = gen_insert_block_action(document);
|
||||
let payload = ApplyActionPayloadPB {
|
||||
document_id: doc_id.clone(),
|
||||
actions: vec![insert_action],
|
||||
};
|
||||
test.apply_actions(payload).await;
|
||||
let document = test.open_document(doc_id).await;
|
||||
let document_data = document.data;
|
||||
let block_count_after = document_data.blocks.len();
|
||||
assert_eq!(block_count_after, block_count + 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_redo_event_test() {
|
||||
let test = DocumentEventTest::new().await;
|
||||
let view = test.create_document().await;
|
||||
let doc_id = view.id.clone();
|
||||
|
||||
let document = test.open_document(doc_id.clone()).await;
|
||||
let insert_action = gen_insert_block_action(document);
|
||||
let payload = ApplyActionPayloadPB {
|
||||
document_id: doc_id.clone(),
|
||||
actions: vec![insert_action],
|
||||
};
|
||||
test.apply_actions(payload).await;
|
||||
let block_count_after_insert = test.open_document(doc_id.clone()).await.data.blocks.len();
|
||||
|
||||
// undo insert action
|
||||
let can_undo = test.can_undo_redo(doc_id.clone()).await.can_undo;
|
||||
assert!(can_undo);
|
||||
test.undo(doc_id.clone()).await;
|
||||
let block_count_after_undo = test.open_document(doc_id.clone()).await.data.blocks.len();
|
||||
assert_eq!(block_count_after_undo, block_count_after_insert - 1);
|
||||
|
||||
// redo insert action
|
||||
let can_redo = test.can_undo_redo(doc_id.clone()).await.can_redo;
|
||||
assert!(can_redo);
|
||||
test.redo(doc_id.clone()).await;
|
||||
let block_count_after_redo = test.open_document(doc_id.clone()).await.data.blocks.len();
|
||||
assert_eq!(block_count_after_redo, block_count_after_insert);
|
||||
}
|
58
frontend/rust-lib/flowy-test/tests/document/utils.rs
Normal file
58
frontend/rust-lib/flowy-test/tests/document/utils.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use flowy_document2::entities::*;
|
||||
use flowy_test::document_event::OpenDocumentData;
|
||||
use nanoid::nanoid;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn gen_id() -> String {
|
||||
nanoid!(10)
|
||||
}
|
||||
|
||||
pub struct ParseDocumentData {
|
||||
pub doc_id: String,
|
||||
pub page_id: String,
|
||||
pub blocks: HashMap<String, BlockPB>,
|
||||
pub children_map: HashMap<String, ChildrenPB>,
|
||||
pub first_block_id: String,
|
||||
}
|
||||
pub fn parse_document_data(document: OpenDocumentData) -> ParseDocumentData {
|
||||
let doc_id = document.id.clone();
|
||||
let data = document.data;
|
||||
let page_id = data.page_id;
|
||||
let blocks = data.blocks;
|
||||
let children_map = data.meta.children_map;
|
||||
let page_block = blocks.get(&page_id).unwrap();
|
||||
let children_id = page_block.children_id.clone();
|
||||
let children = children_map.get(&children_id).unwrap();
|
||||
let block_id = children.children.get(0).unwrap().to_string();
|
||||
ParseDocumentData {
|
||||
doc_id,
|
||||
page_id,
|
||||
blocks,
|
||||
children_map,
|
||||
first_block_id: block_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_insert_block_action(document: OpenDocumentData) -> BlockActionPB {
|
||||
let parse_data = parse_document_data(document);
|
||||
let first_block_id = parse_data.first_block_id;
|
||||
let block = parse_data.blocks.get(&first_block_id).unwrap();
|
||||
let page_id = parse_data.page_id;
|
||||
let data = block.data.clone();
|
||||
let new_block_id = gen_id();
|
||||
let new_block = BlockPB {
|
||||
id: new_block_id.clone(),
|
||||
ty: block.ty.clone(),
|
||||
data,
|
||||
parent_id: page_id.clone(),
|
||||
children_id: gen_id(),
|
||||
};
|
||||
BlockActionPB {
|
||||
action: BlockActionTypePB::Insert,
|
||||
payload: BlockActionPayloadPB {
|
||||
block: new_block,
|
||||
prev_id: Some(first_block_id),
|
||||
parent_id: Some(page_id),
|
||||
},
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod database;
|
||||
mod document;
|
||||
mod folder;
|
||||
mod user;
|
||||
|
Loading…
Reference in New Issue
Block a user