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
This commit is contained in:
Kilu.He 2023-07-26 16:49:50 +08:00 committed by GitHub
parent 6e9a5a16a6
commit 915ce02157
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 384 additions and 111 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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 = { 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" }
#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" }

View File

@ -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%',
}}
/>
</>

View File

@ -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,

View File

@ -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));

View File

@ -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) {

View File

@ -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',

View File

@ -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>
);

View File

@ -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>

View File

@ -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) {

View File

@ -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();

View File

@ -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);
};
}

View File

@ -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>) => {

View File

@ -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());
};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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",

View File

@ -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",

View File

@ -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" }

View File

@ -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

View File

@ -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>,

View File

@ -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,
}

View File

@ -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.

View File

@ -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]);
}

View File

@ -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,

View File

@ -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;
}