瀏覽代碼

fix: implement the interface of move nested views (#3042)

* fix: implement the interface of move nested views

* fix: update rust ci ubuntu version

* fix: update rust ci version
Kilu.He 1 年之前
父節點
當前提交
915ce02157
共有 27 個文件被更改,包括 385 次插入112 次删除
  1. 0 14
      .github/workflows/rust_ci.yaml
  2. 10 10
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  3. 12 12
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  4. 4 1
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDragDropContext.tsx
  5. 1 4
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDraggable.hooks.ts
  6. 24 18
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/index.tsx
  7. 4 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx
  8. 11 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx
  9. 9 7
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
  10. 1 6
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx
  11. 9 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/NestedPage.hooks.ts
  12. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/index.tsx
  13. 13 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
  14. 5 8
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_bd_svc.ts
  15. 13 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts
  16. 5 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_observer.ts
  17. 6 0
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts
  18. 2 1
      frontend/resources/translations/en.json
  19. 10 10
      frontend/rust-lib/Cargo.lock
  20. 10 10
      frontend/rust-lib/Cargo.toml
  21. 41 0
      frontend/rust-lib/flowy-folder2/src/entities/view.rs
  22. 11 0
      frontend/rust-lib/flowy-folder2/src/event_handler.rs
  23. 10 0
      frontend/rust-lib/flowy-folder2/src/event_map.rs
  24. 36 0
      frontend/rust-lib/flowy-folder2/src/manager.rs
  25. 44 0
      frontend/rust-lib/flowy-folder2/tests/workspace/folder_test.rs
  26. 29 0
      frontend/rust-lib/flowy-folder2/tests/workspace/script.rs
  27. 64 0
      frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs

+ 0 - 14
.github/workflows/rust_ci.yaml

@@ -22,7 +22,6 @@ on:
 env:
   CARGO_TERM_COLOR: always
   RUST_TOOLCHAIN: "1.70"
-  FLUTTER_VERSION: "3.10.1"
 
 jobs:
   test-on-ubuntu:
@@ -40,14 +39,6 @@ jobs:
           components: rustfmt, clippy
           profile: minimal
 
-      - name: Install flutter
-        id: flutter
-        uses: subosito/flutter-action@v2
-        with:
-          channel: "stable"
-          flutter-version: ${{ env.FLUTTER_VERSION }}
-          cache: true
-
       - name: Install prerequisites
         working-directory: frontend
         run: |
@@ -60,11 +51,6 @@ jobs:
           workspaces: |
             frontend/rust-lib 
 
-      - name: Build FlowySDK
-        working-directory: frontend
-        run: |
-          cargo make --profile development-linux-x86_64 appflowy-core-dev
-
       - name: Run rust-lib tests
         working-directory: frontend/rust-lib
         run: RUST_LOG=info RUST_BACKTRACE=1 cargo test --no-default-features --features="rev-sqlite"

+ 10 - 10
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "collab",
@@ -1030,7 +1030,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1048,7 +1048,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1066,7 +1066,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1093,7 +1093,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1105,7 +1105,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "collab",
@@ -1124,7 +1124,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "chrono",
@@ -1144,7 +1144,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "bincode",
  "chrono",
@@ -1164,7 +1164,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1198,7 +1198,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "bytes",
  "collab",

+ 12 - 12
frontend/appflowy_tauri/src-tauri/Cargo.toml

@@ -34,18 +34,18 @@ default = ["custom-protocol"]
 custom-protocol = ["tauri/custom-protocol"]
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-
-#collab = { path = "../../AppFlowy-Collab/collab" }
-#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
-#collab-document = { path = "../../AppFlowy-Collab/collab-document" }
-#collab-database = { path = "../../AppFlowy-Collab/collab-database" }
-#appflowy-integrate = { path = "../../AppFlowy-Collab/appflowy-integrate" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+
+#collab = { path = "../../../../AppFlowy-Collab/collab" }
+#collab-folder = { path = "../../../../AppFlowy-Collab/collab-folder" }
+#collab-document = { path = "../../../../AppFlowy-Collab/collab-document" }
+#collab-database = { path = "../../../../AppFlowy-Collab/collab-database" }
+#appflowy-integrate = { path = "../../../../AppFlowy-Collab/appflowy-integrate" }
 
 
 

+ 4 - 1
frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDragDropContext.tsx

@@ -94,8 +94,12 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) {
     const draggingNode = document.querySelector(`[data-draggable-id="${draggingId}"]`);
 
     if (!draggingNode) return;
+    const nodeWidth = draggingNode.clientWidth;
+    const nodeHeight = draggingNode.clientHeight;
     const clone = draggingNode.cloneNode(true);
 
+    shadow.style.width = `${nodeWidth}px`;
+    shadow.style.height = `${nodeHeight}px`;
     shadow.appendChild(clone);
   }, [dragShadowVisible, draggingId]);
 
@@ -111,7 +115,6 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) {
           pointerEvents: 'none',
           opacity: dragShadowVisible ? 1 : 0,
           zIndex: 1000,
-          width: '100%',
         }}
       />
     </>

+ 1 - 4
frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDraggable.hooks.ts

@@ -1,10 +1,9 @@
-import React, { useCallback, useMemo, useRef } from 'react';
+import React, { useCallback, useMemo } from 'react';
 import { useAppDispatch, useAppSelector } from '$app/stores/store';
 import { blockDraggableActions, BlockDraggableType, DragInsertType } from '$app_reducers/block-draggable/slice';
 import { getDragDropContext } from '$app/utils/draggable';
 
 export function useDraggableState(id: string, type: BlockDraggableType) {
-  const ref = useRef<HTMLDivElement>(null);
   const dispatch = useAppDispatch();
   const { dropState, isDragging } = useAppSelector((state) => {
     const draggableState = state.blockDraggable;
@@ -28,7 +27,6 @@ export function useDraggableState(id: string, type: BlockDraggableType) {
 
   const onDragStart = useCallback(
     (event: React.MouseEvent | MouseEvent) => {
-      if (!ref.current) return;
       if (event.button !== 0) return;
 
       event.preventDefault();
@@ -73,7 +71,6 @@ export function useDraggableState(id: string, type: BlockDraggableType) {
 
   return {
     onDragStart,
-    ref,
     beforeDropping,
     afterDropping,
     childDropping,

+ 24 - 18
frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/index.tsx

@@ -1,19 +1,24 @@
-import React, { useEffect, useState } from 'react';
+import React, { HTMLAttributes, useEffect } from 'react';
 import { useDraggableState } from '$app/components/_shared/BlockDraggable/BlockDraggable.hooks';
 import { BlockDraggableType } from '$app_reducers/block-draggable/slice';
 
-function BlockDraggable({
-  id,
-  type,
-  children,
-  getAnchorEl,
-}: {
-  id: string;
-  type: BlockDraggableType;
-  children: React.ReactNode;
-  getAnchorEl?: () => HTMLElement | null;
-}) {
-  const { onDragStart, ref, beforeDropping, afterDropping, childDropping, isDragging } = useDraggableState(id, type);
+function BlockDraggable(
+  {
+    id,
+    type,
+    children,
+    getAnchorEl,
+    className,
+    ...props
+  }: {
+    id: string;
+    type: BlockDraggableType;
+    children: React.ReactNode;
+    getAnchorEl?: () => HTMLElement | null;
+  } & HTMLAttributes<HTMLDivElement>,
+  ref: React.Ref<HTMLDivElement>
+) {
+  const { onDragStart, beforeDropping, afterDropping, childDropping, isDragging } = useDraggableState(id, type);
 
   const commonCls = 'pointer-events-none absolute z-10 w-[100%] bg-fill-hover transition-all duration-200';
 
@@ -34,15 +39,16 @@ function BlockDraggable({
         data-draggable-id={id}
         data-draggable-type={type}
         onMouseDown={getAnchorEl ? undefined : onDragStart}
-        className={'relative'}
+        className={`relative ${className || ''}`}
         style={{
           opacity: isDragging ? 0.7 : 1,
         }}
+        {...props}
       >
         {
           <div
             style={{
-              display: beforeDropping ? 'block' : 'none',
+              visibility: beforeDropping ? 'visible' : 'hidden',
             }}
             className={`${commonCls} left-0 top-[-2px] h-[4px]`}
           />
@@ -52,7 +58,7 @@ function BlockDraggable({
         {
           <div
             style={{
-              display: childDropping ? 'block' : 'none',
+              visibility: childDropping ? 'visible' : 'hidden',
             }}
             className={`${commonCls} left-0 top-0 h-[100%] opacity-[0.3]`}
           />
@@ -60,7 +66,7 @@ function BlockDraggable({
         {
           <div
             style={{
-              display: afterDropping ? 'block' : 'none',
+              visibility: afterDropping ? 'visible' : 'hidden',
             }}
             className={`${commonCls} bottom-[-2px] left-0 h-[4px]`}
           />
@@ -70,4 +76,4 @@ function BlockDraggable({
   );
 }
 
-export default React.memo(BlockDraggable);
+export default React.memo(React.forwardRef(BlockDraggable));

+ 4 - 4
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx

@@ -8,10 +8,10 @@ import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
 import { getNode } from '$app/utils/document/node';
 import { get } from '$app/utils/tool';
 
-const headingBlockTopOffset: Record<number, number> = {
-  1: 6,
-  2: 4,
-  3: 3,
+const headingBlockTopOffset: Record<number, string> = {
+  1: '1.65rem',
+  2: '1.3rem',
+  3: '0.25rem',
 };
 
 export function useBlockSideToolbar(id: string) {

+ 11 - 2
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx

@@ -29,7 +29,7 @@ export default function BlockSideToolbar({ id }: { id: string }) {
           opacity: show ? 1 : 0,
           top: topOffset,
         }}
-        className='absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-100'
+        className='absolute left-[-50px] inline-flex transition-opacity duration-100'
       >
         {/** Add Block below */}
         <Tooltip disableInteractive={true} title={t('blockActions.addBelowTooltip')} placement={'top-start'}>
@@ -59,7 +59,16 @@ export default function BlockSideToolbar({ id }: { id: string }) {
         </Tooltip>
 
         {/** Open menu or drag */}
-        <Tooltip disableInteractive={true} title={t('blockActions.dragAndOpenTooltip')} placement={'top-start'}>
+        <Tooltip
+          disableInteractive={true}
+          title={
+            <div className={'flex flex-col items-center justify-center'}>
+              <div>{t('blockActions.dragTooltip')}</div>
+              <div>{t('blockActions.openMenuTooltip')}</div>
+            </div>
+          }
+          placement={'top-start'}
+        >
           <IconButton
             style={{
               pointerEvents: show ? 'auto' : 'none',

+ 9 - 7
frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx

@@ -87,14 +87,16 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
         getAnchorEl={() => {
           return ref.current?.querySelector(`[data-draggable-anchor="${id}"]`) || null;
         }}
+        {...props}
+        ref={ref}
+        data-block-id={node.id}
+        className={`pt-[0.5px] ${className}`}
       >
-        <div {...props} ref={ref} data-block-id={node.id} className={`relative ${className}`}>
-          {renderBlock()}
-          <BlockOverlay id={id} />
-          {isSelected ? (
-            <div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-content-blue-100' />
-          ) : null}
-        </div>
+        {renderBlock()}
+        <BlockOverlay id={id} />
+        {isSelected ? (
+          <div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-content-blue-100' />
+        ) : null}
       </BlockDraggable>
     </NodeIdContext.Provider>
   );

+ 1 - 6
frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx

@@ -49,12 +49,7 @@ export default function VirtualizedList({
                 const id = childIds[virtualRow.index];
 
                 return (
-                  <div
-                    className='mt-[-0.5px] pt-[0.5px]'
-                    key={id}
-                    data-index={virtualRow.index}
-                    ref={virtualize.measureElement}
-                  >
+                  <div key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
                     {virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
                     {renderNode(id)}
                   </div>

+ 9 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/NestedPage.hooks.ts

@@ -36,6 +36,13 @@ export function useLoadChildPages(pageId: string) {
     [dispatch, pageId]
   );
 
+  const onPageChanged = useCallback(
+    (page: Page) => {
+      dispatch(pagesActions.onPageChanged(page));
+    },
+    [dispatch]
+  );
+
   const onPageCollapsed = useCallback(async () => {
     dispatch(pagesActions.removeChildPages(pageId));
     await controller.unsubscribe();
@@ -52,8 +59,9 @@ export function useLoadChildPages(pageId: string) {
     );
     await controller.subscribe({
       onChildPagesChanged,
+      onPageChanged,
     });
-  }, [controller, dispatch, onChildPagesChanged, pageId]);
+  }, [controller, dispatch, onChildPagesChanged, onPageChanged, pageId]);
 
   useEffect(() => {
     if (collapsed) {

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/index.tsx

@@ -11,7 +11,7 @@ function NestedPage({ pageId }: { pageId: string }) {
   const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
 
   return (
-    <BlockDraggable id={pageId} type={BlockDraggableType.PAGE}>
+    <BlockDraggable id={pageId} type={BlockDraggableType.PAGE} data-page-id={pageId}>
       <NestedPageTitle
         onClick={() => {
           onPageClick();

+ 13 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts

@@ -17,6 +17,7 @@ import {
   MoveGroupRowPayloadPB,
   MoveRowPayloadPB,
   RowIdPB,
+  DatabaseEventUpdateDatabaseSetting,
 } from '@/services/backend/events/flowy-database2';
 import {
   GetFieldPayloadPB,
@@ -41,12 +42,14 @@ export class DatabaseBackendService {
     const payload = DatabaseViewIdPB.fromObject({
       value: this.viewId,
     });
+
     return DatabaseEventGetDatabase(payload);
   };
 
   /// Close a database
   closeDatabase = async () => {
     const payload = ViewIdPB.fromObject({ value: this.viewId });
+
     return FolderEventCloseView(payload);
   };
 
@@ -57,6 +60,7 @@ export class DatabaseBackendService {
   /// only support in kanban board.
   createRow = async (params?: { rowId?: string; groupId?: string }) => {
     const payload = CreateRowPayloadPB.fromObject({ view_id: this.viewId });
+
     if (params?.rowId !== undefined) {
       payload.start_row_id = params.rowId;
     }
@@ -64,16 +68,19 @@ export class DatabaseBackendService {
     if (params?.groupId !== undefined) {
       payload.group_id = params.groupId;
     }
+
     return DatabaseEventCreateRow(payload);
   };
 
   duplicateRow = async (rowId: string) => {
     const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
+
     return DatabaseEventDuplicateRow(payload);
   };
 
   deleteRow = async (rowId: string) => {
     const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
+
     return DatabaseEventDeleteRow(payload);
   };
 
@@ -85,6 +92,7 @@ export class DatabaseBackendService {
       from_row_id: fromRowId,
       to_group_id: toGroupId,
     });
+
     if (toRowId !== undefined) {
       payload.to_row_id = toRowId;
     }
@@ -98,6 +106,7 @@ export class DatabaseBackendService {
       from_group_id: fromGroupId,
       to_group_id: toGroupId,
     });
+
     return DatabaseEventMoveGroup(payload);
   };
 
@@ -115,6 +124,7 @@ export class DatabaseBackendService {
   /// Get a group by id
   getGroup = (groupId: string) => {
     const payload = DatabaseGroupIdPB.fromObject({ view_id: this.viewId, group_id: groupId });
+
     return DatabaseEventGetGroup(payload);
   };
 
@@ -125,6 +135,7 @@ export class DatabaseBackendService {
       from_index: params.fromIndex,
       to_index: params.toIndex,
     });
+
     return DatabaseEventMoveField(payload);
   };
 
@@ -132,11 +143,13 @@ export class DatabaseBackendService {
   /// It should only call once after the board open
   loadGroups = () => {
     const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
+
     return DatabaseEventGetGroups(payload);
   };
 
   getSettings = () => {
     const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
+
     return DatabaseEventGetDatabaseSetting(payload);
   };
 }

+ 5 - 8
frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_bd_svc.ts

@@ -6,14 +6,14 @@ import {
   FolderEventDuplicateView,
   FolderEventCloseView,
   FolderEventImportData,
-  FolderEventMoveView,
   ViewIdPB,
   CreateViewPayloadPB,
   UpdateViewPayloadPB,
   RepeatedViewIdPB,
   ViewPB,
   ImportPB,
-  MoveViewPayloadPB,
+  MoveNestedViewPayloadPB,
+  FolderEventMoveNestedView,
 } from '@/services/backend/events/flowy-folder2';
 import { Page } from '$app_reducers/pages/slice';
 
@@ -31,16 +31,13 @@ export class PageBackendService {
   };
 
   movePage = async (params: { viewId: string; parentId: string; prevId?: string }) => {
-    console.log('movePage', params);
-    const payload = new MoveViewPayloadPB({
+    const payload = new MoveNestedViewPayloadPB({
       view_id: params.viewId,
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-ignore
-      parent_view_id: params.parentId,
+      new_parent_id: params.parentId,
       prev_view_id: params.prevId,
     });
 
-    return FolderEventMoveView(payload);
+    return FolderEventMoveNestedView(payload);
   };
 
   createPage = async (params: ReturnType<typeof CreateViewPayloadPB.prototype.toObject>) => {

+ 13 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts

@@ -72,16 +72,22 @@ export class PageController {
     return this.getPage(parentPageId);
   };
 
-  subscribe = async (callbacks: { onChildPagesChanged?: (childPages: Page[]) => void }) => {
-    const onChildPagesChanged = async () => {
+  subscribe = async (callbacks: {
+    onChildPagesChanged?: (childPages: Page[]) => void;
+    onPageChanged?: (page: Page) => void;
+  }) => {
+    const onChanged = async () => {
+      const page = await this.getPage();
       const childPages = await this.getChildPages();
 
+      callbacks.onPageChanged?.(page);
       callbacks.onChildPagesChanged?.(childPages);
     };
 
-    this.onChangeQueue = new AsyncQueue(onChildPagesChanged);
+    this.onChangeQueue = new AsyncQueue(onChanged);
     await this.observer.subscribeView(this.id, {
       didUpdateChildViews: this.didUpdateChildPages,
+      didUpdateView: this.didUpdateView,
     });
   };
 
@@ -123,4 +129,8 @@ export class PageController {
   private didUpdateChildPages = (payload: Uint8Array) => {
     this.onChangeQueue?.enqueue(Math.random());
   };
+
+  private didUpdateView = (payload: Uint8Array) => {
+    this.onChangeQueue?.enqueue(Math.random());
+  };
 }

+ 5 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_observer.ts

@@ -59,6 +59,7 @@ export class WorkspaceObserver {
     viewId: string,
     callbacks: {
       didUpdateChildViews: (payload: Uint8Array) => void;
+      didUpdateView: (payload: Uint8Array) => void;
     }
   ) => {
     this.listener = new WorkspaceNotificationObserver({
@@ -69,6 +70,10 @@ export class WorkspaceObserver {
             if (!result.ok) break;
             callbacks.didUpdateChildViews(result.val);
             break;
+          case FolderNotification.DidUpdateView:
+            if (!result.ok) break;
+            callbacks.didUpdateView(result.val);
+            break;
           default:
             break;
         }

+ 6 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts

@@ -61,6 +61,12 @@ export const pagesSlice = createSlice({
       state.relationMap[id] = children;
     },
 
+    onPageChanged(state, action: PayloadAction<Page>) {
+      const page = action.payload;
+
+      state.pageMap[page.id] = page;
+    },
+
     removeChildPages(state, action: PayloadAction<string>) {
       const parentId = action.payload;
 

+ 2 - 1
frontend/resources/translations/en.json

@@ -13,7 +13,8 @@
     "addAboveCmd": "Alt+click",
     "addAboveMacCmd": "Option+click",
     "addAboveTooltip": "to add above",
-    "dragAndOpenTooltip": "Drag to reorder, click to open"
+    "dragTooltip": "Drag to move",
+    "openMenuTooltip": "Click to open menu"
   },
   "signUp": {
     "buttonText": "Sign Up",

+ 10 - 10
frontend/rust-lib/Cargo.lock

@@ -85,7 +85,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "collab",
@@ -897,7 +897,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "bytes",
@@ -915,7 +915,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -933,7 +933,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -960,7 +960,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -972,7 +972,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "collab",
@@ -991,7 +991,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "chrono",
@@ -1011,7 +1011,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "bincode",
  "chrono",
@@ -1031,7 +1031,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1065,7 +1065,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f420738#f4207385738961a9aa4ea871731de204dfee8455"
 dependencies = [
  "bytes",
  "collab",

+ 10 - 10
frontend/rust-lib/Cargo.toml

@@ -34,15 +34,15 @@ opt-level = 3
 incremental = false
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f420738" }
 
-#collab = { path = "../AppFlowy-Collab/collab" }
-#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
-#collab-database= { path = "../AppFlowy-Collab/collab-database" }
-#collab-document = { path = "../AppFlowy-Collab/collab-document" }
-#appflowy-integrate = { path = "../AppFlowy-Collab/appflowy-integrate" }
+#collab = { path = "../../../AppFlowy-Collab/collab" }
+#collab-folder = { path = "../../../AppFlowy-Collab/collab-folder" }
+#collab-database= { path = "../../../AppFlowy-Collab/collab-database" }
+#collab-document = { path = "../../../AppFlowy-Collab/collab-document" }
+#appflowy-integrate = { path = "../../../AppFlowy-Collab/appflowy-integrate" }
 

+ 41 - 0
frontend/rust-lib/flowy-folder2/src/entities/view.rs

@@ -369,6 +369,26 @@ pub struct MoveViewPayloadPB {
   pub to: i32,
 }
 
+/// * `view_id` - A string slice that holds the id of the view to be moved.
+/// * `new_parent_id` - A string slice that holds the id of the new parent view.
+/// * `prev_view_id` - An `Option<String>` that holds the id of the view after which the `view_id` should be positioned.
+///
+/// If `prev_view_id` is provided, the moved view will be placed right after
+/// the view corresponding to `prev_view_id` under the `new_parent_id`.
+///
+/// If `prev_view_id` is `None`, the moved view will become the first child of the new parent.
+#[derive(Default, ProtoBuf)]
+pub struct MoveNestedViewPayloadPB {
+  #[pb(index = 1)]
+  pub view_id: String,
+
+  #[pb(index = 2)]
+  pub new_parent_id: String,
+
+  #[pb(index = 3, one_of)]
+  pub prev_view_id: Option<String>,
+}
+
 pub struct MoveViewParams {
   pub view_id: String,
   pub from: usize,
@@ -388,6 +408,27 @@ impl TryInto<MoveViewParams> for MoveViewPayloadPB {
   }
 }
 
+pub struct MoveNestedViewParams {
+  pub view_id: String,
+  pub new_parent_id: String,
+  pub prev_view_id: Option<String>,
+}
+
+impl TryInto<MoveNestedViewParams> for MoveNestedViewPayloadPB {
+  type Error = ErrorCode;
+
+  fn try_into(self) -> Result<MoveNestedViewParams, Self::Error> {
+    let view_id = ViewIdentify::parse(self.view_id)?.0;
+    let new_parent_id = ViewIdentify::parse(self.new_parent_id)?.0;
+    let prev_view_id = self.prev_view_id;
+    Ok(MoveNestedViewParams {
+      view_id,
+      new_parent_id,
+      prev_view_id,
+    })
+  }
+}
+
 // impl<'de> Deserialize<'de> for ViewDataType {
 //     fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
 //     where

+ 11 - 0
frontend/rust-lib/flowy-folder2/src/event_handler.rs

@@ -163,6 +163,17 @@ pub(crate) async fn move_view_handler(
   Ok(())
 }
 
+pub(crate) async fn move_nested_view_handler(
+  data: AFPluginData<MoveNestedViewPayloadPB>,
+  folder: AFPluginState<Arc<FolderManager>>,
+) -> Result<(), FlowyError> {
+  let params: MoveNestedViewParams = data.into_inner().try_into()?;
+  folder
+    .move_nested_view(params.view_id, params.new_parent_id, params.prev_view_id)
+    .await?;
+  Ok(())
+}
+
 #[tracing::instrument(level = "debug", skip(data, folder), err)]
 pub(crate) async fn duplicate_view_handler(
   data: AFPluginData<ViewPB>,

+ 10 - 0
frontend/rust-lib/flowy-folder2/src/event_map.rs

@@ -29,6 +29,7 @@ pub fn init(folder: Arc<FolderManager>) -> AFPlugin {
     .event(FolderEvent::SetLatestView, set_latest_view_handler)
     .event(FolderEvent::CloseView, close_view_handler)
     .event(FolderEvent::MoveView, move_view_handler)
+    .event(FolderEvent::MoveNestedView, move_nested_view_handler)
     // Trash
     .event(FolderEvent::ReadTrash, read_trash_handler)
     .event(FolderEvent::PutbackTrash, putback_trash_handler)
@@ -132,4 +133,13 @@ pub enum FolderEvent {
 
   #[event()]
   GetFolderSnapshots = 31,
+
+  /// Moves a nested view to a new location in the hierarchy.
+  ///
+  /// This function takes the `view_id` of the view to be moved,
+  /// `new_parent_id` of the view under which the `view_id` should be moved,
+  /// and an optional `prev_view_id` to position the `view_id` right after
+  /// this specific view.
+  #[event(input = "MoveNestedViewPayloadPB")]
+  MoveNestedView = 32,
 }

+ 36 - 0
frontend/rust-lib/flowy-folder2/src/manager.rs

@@ -450,6 +450,42 @@ impl FolderManager {
     Ok(())
   }
 
+  /// Moves a nested view to a new location in the hierarchy.
+  ///
+  /// This function takes the `view_id` of the view to be moved,
+  /// `new_parent_id` of the view under which the `view_id` should be moved,
+  /// and an optional `prev_view_id` to position the `view_id` right after
+  /// this specific view.
+  ///
+  /// If `prev_view_id` is provided, the moved view will be placed right after
+  /// the view corresponding to `prev_view_id` under the `new_parent_id`.
+  /// If `prev_view_id` is `None`, the moved view will become the first child of the new parent.
+  ///
+  /// # Arguments
+  ///
+  /// * `view_id` - A string slice that holds the id of the view to be moved.
+  /// * `new_parent_id` - A string slice that holds the id of the new parent view.
+  /// * `prev_view_id` - An `Option<String>` that holds the id of the view after which the `view_id` should be positioned.
+  ///
+  #[tracing::instrument(level = "trace", skip(self), err)]
+  pub async fn move_nested_view(
+    &self,
+    view_id: String,
+    new_parent_id: String,
+    prev_view_id: Option<String>,
+  ) -> FlowyResult<()> {
+    let view = self.get_view(&view_id).await?;
+    let old_parent_id = view.parent_view_id;
+    self.with_folder((), |folder| {
+      folder.move_nested_view(&view_id, &new_parent_id, prev_view_id);
+    });
+    notify_parent_view_did_change(
+      self.mutex_folder.clone(),
+      vec![new_parent_id, old_parent_id],
+    );
+    Ok(())
+  }
+
   /// Move the view with given id from one position to another position.
   /// The view will be moved to the new position in the same parent view.
   /// The passed in index is the index of the view that displayed in the UI.

+ 44 - 0
frontend/rust-lib/flowy-folder2/tests/workspace/folder_test.rs

@@ -248,3 +248,47 @@ async fn view_delete_all_permanent() {
   assert_eq!(test.parent_view.child_views.len(), 0);
   assert_eq!(test.trash.len(), 0);
 }
+
+#[tokio::test]
+async fn move_view_event_test() {
+  let mut test = FolderTest::new().await;
+  let parent_view = test.parent_view.clone();
+  test
+    .run_scripts(vec![
+      CreateView {
+        name: "View A".to_owned(),
+        desc: "View A description".to_owned(),
+        layout: ViewLayout::Document,
+      },
+      ReloadParentView(parent_view.id.clone()),
+    ])
+    .await;
+  let view_ids = test
+    .parent_view
+    .child_views
+    .iter()
+    .map(|view| view.id.clone())
+    .collect::<Vec<String>>();
+  let move_view_id = view_ids[0].clone();
+  let new_prev_view_id = view_ids[1].clone();
+  let new_parent_view_id = parent_view.id.clone();
+  test
+    .run_scripts(vec![
+      MoveView {
+        view_id: move_view_id.clone(),
+        new_parent_id: new_parent_view_id.clone(),
+        prev_view_id: Some(new_prev_view_id.clone()),
+      },
+      ReloadParentView(parent_view.id.clone()),
+    ])
+    .await;
+
+  let after_view_ids = test
+    .parent_view
+    .child_views
+    .iter()
+    .map(|view| view.id.clone())
+    .collect::<Vec<String>>();
+  assert_eq!(after_view_ids[0], view_ids[1]);
+  assert_eq!(after_view_ids[1], view_ids[0]);
+}

+ 29 - 0
frontend/rust-lib/flowy-folder2/tests/workspace/script.rs

@@ -42,6 +42,11 @@ pub enum FolderScript {
   },
   DeleteView,
   DeleteViews(Vec<String>),
+  MoveView {
+    view_id: String,
+    new_parent_id: String,
+    prev_view_id: Option<String>,
+  },
 
   // Trash
   RestoreAppFromTrash,
@@ -128,6 +133,13 @@ impl FolderTest {
         let view = create_view(sdk, &self.parent_view.id, &name, &desc, layout).await;
         self.child_view = view;
       },
+      FolderScript::MoveView {
+        view_id,
+        new_parent_id,
+        prev_view_id,
+      } => {
+        move_view(sdk, view_id, new_parent_id, prev_view_id).await;
+      },
       FolderScript::AssertView(view) => {
         assert_eq!(self.child_view, view, "View not equal");
       },
@@ -256,6 +268,23 @@ pub async fn read_view(sdk: &FlowyCoreTest, view_id: &str) -> ViewPB {
     .parse::<ViewPB>()
 }
 
+pub async fn move_view(
+  sdk: &FlowyCoreTest,
+  view_id: String,
+  parent_id: String,
+  prev_view_id: Option<String>,
+) {
+  let request = MoveNestedViewPayloadPB {
+    view_id,
+    new_parent_id: parent_id,
+    prev_view_id,
+  };
+  EventBuilder::new(sdk.clone())
+    .event(MoveNestedView)
+    .payload(request)
+    .async_send()
+    .await;
+}
 pub async fn update_view(
   sdk: &FlowyCoreTest,
   view_id: &str,

+ 64 - 0
frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs

@@ -508,3 +508,67 @@ fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
     ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
   ]
 }
+
+#[tokio::test]
+async fn move_view_across_parent_test() {
+  let test = FlowyCoreTest::new_with_guest_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let parent_1 = test
+    .create_view(&current_workspace.id, "My view 1".to_string())
+    .await;
+  let parent_2 = test
+    .create_view(&current_workspace.id, "My view 2".to_string())
+    .await;
+
+  for j in 1..6 {
+    let _ = test
+      .create_view(&parent_1.id, format!("My 1-{} view 1", j))
+      .await;
+  }
+
+  let views = test.get_view(&parent_1.id).await.child_views;
+  // Move `My 1-1 view 1` to `My view 2`
+  let move_view_id = views[0].id.clone();
+  let new_parent_id = parent_2.id.clone();
+  let prev_id = None;
+  move_folder_nested_view(test.clone(), move_view_id, new_parent_id, prev_id).await;
+  let parent1_views = test.get_view(&parent_1.id).await.child_views;
+  let parent2_views = test.get_view(&parent_2.id).await.child_views;
+  assert_eq!(parent2_views.len(), 1);
+  assert_eq!(parent2_views[0].name, "My 1-1 view 1");
+  assert_eq!(parent1_views[0].name, "My 1-2 view 1");
+
+  // Move My 1-2 view 1 from My view 1 to the current workspace and insert it after My view 1.
+  let move_view_id = parent1_views[0].id.clone();
+  let new_parent_id = current_workspace.id.clone();
+  let prev_id = Some(parent_1.id.clone());
+  move_folder_nested_view(test.clone(), move_view_id, new_parent_id, prev_id).await;
+  let parent1_views = test.get_view(&parent_1.id).await.child_views;
+  let workspace_views = test.get_all_workspace_views().await;
+  let workspace_views_len = workspace_views.len();
+  assert_eq!(parent1_views[0].name, "My 1-3 view 1");
+  assert_eq!(workspace_views[workspace_views_len - 3].name, "My view 1");
+  assert_eq!(
+    workspace_views[workspace_views_len - 2].name,
+    "My 1-2 view 1"
+  );
+  assert_eq!(workspace_views[workspace_views_len - 1].name, "My view 2");
+}
+
+async fn move_folder_nested_view(
+  sdk: FlowyCoreTest,
+  view_id: String,
+  new_parent_id: String,
+  prev_view_id: Option<String>,
+) {
+  let payload = MoveNestedViewPayloadPB {
+    view_id,
+    new_parent_id,
+    prev_view_id,
+  };
+  EventBuilder::new(sdk)
+    .event(flowy_folder2::event_map::FolderEvent::MoveNestedView)
+    .payload(payload)
+    .async_send()
+    .await;
+}