mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support views drag and drop (#3004)
This commit is contained in:
parent
0fb004aee0
commit
5ab64f8835
24
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
24
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -105,6 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1029,6 +1030,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -1046,6 +1048,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-client-ws"
|
name = "collab-client-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync",
|
||||||
@ -1063,6 +1066,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1089,6 +1093,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1100,6 +1105,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1118,6 +1124,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1137,6 +1144,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1156,6 +1164,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1189,6 +1198,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1869,6 +1879,7 @@ dependencies = [
|
|||||||
"flowy-folder2",
|
"flowy-folder2",
|
||||||
"flowy-net",
|
"flowy-net",
|
||||||
"flowy-server",
|
"flowy-server",
|
||||||
|
"flowy-server-config",
|
||||||
"flowy-sqlite",
|
"flowy-sqlite",
|
||||||
"flowy-task",
|
"flowy-task",
|
||||||
"flowy-user",
|
"flowy-user",
|
||||||
@ -2070,6 +2081,7 @@ dependencies = [
|
|||||||
"flowy-document2",
|
"flowy-document2",
|
||||||
"flowy-error",
|
"flowy-error",
|
||||||
"flowy-folder2",
|
"flowy-folder2",
|
||||||
|
"flowy-server-config",
|
||||||
"flowy-user",
|
"flowy-user",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -2092,6 +2104,14 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flowy-server-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"flowy-error",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flowy-sqlite"
|
name = "flowy-sqlite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2128,6 +2148,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"appflowy-integrate",
|
"appflowy-integrate",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"collab",
|
||||||
|
"collab-folder",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_derives",
|
"diesel_derives",
|
||||||
"fancy-regex 0.11.0",
|
"fancy-regex 0.11.0",
|
||||||
@ -2135,6 +2157,7 @@ dependencies = [
|
|||||||
"flowy-derive",
|
"flowy-derive",
|
||||||
"flowy-error",
|
"flowy-error",
|
||||||
"flowy-notification",
|
"flowy-notification",
|
||||||
|
"flowy-server-config",
|
||||||
"flowy-sqlite",
|
"flowy-sqlite",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lib-dispatch",
|
"lib-dispatch",
|
||||||
@ -2151,6 +2174,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
"uuid",
|
||||||
"validator",
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -34,12 +34,12 @@ default = ["custom-protocol"]
|
|||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
|
|
||||||
#collab = { path = "../../AppFlowy-Collab/collab" }
|
#collab = { path = "../../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { blockDraggableActions, DraggableContext, DragInsertType } from '$app_reducers/block-draggable/slice';
|
||||||
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
|
import { collisionNode, getDragDropContext, scrollIntoViewIfNeeded } from '$app/utils/draggable';
|
||||||
|
import { onDragEndThunk } from '$app_reducers/block-draggable/async_actions';
|
||||||
|
import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
|
||||||
|
function BlockDragDropContext({ children }: { children: React.ReactNode }) {
|
||||||
|
const shadowRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { dragging, draggingId, dragShadowVisible, draggingPosition } = useAppSelector((state) => state.blockDraggable);
|
||||||
|
|
||||||
|
const registerDraggableEvents = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const onDrag = (event: MouseEvent) => {
|
||||||
|
const data = collisionNode(event, id);
|
||||||
|
|
||||||
|
let dropContext: DraggableContext | undefined;
|
||||||
|
const dropId = data?.id;
|
||||||
|
let insertType = data?.insertType;
|
||||||
|
|
||||||
|
if (dropId) {
|
||||||
|
const context = getDragDropContext(dropId);
|
||||||
|
const contextId = context?.contextId;
|
||||||
|
const container = context?.container;
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
dropContext = {
|
||||||
|
type: context.type,
|
||||||
|
contextId: context.contextId,
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollIntoViewIfNeeded(event, container as HTMLDivElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextId) {
|
||||||
|
const block = getBlock(contextId, dropId);
|
||||||
|
|
||||||
|
if (block) {
|
||||||
|
const config = blockConfig[block.type];
|
||||||
|
|
||||||
|
if (!config.canAddChild && insertType === DragInsertType.CHILD) {
|
||||||
|
insertType = DragInsertType.AFTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
blockDraggableActions.drag({
|
||||||
|
draggingPosition: {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
},
|
||||||
|
insertType,
|
||||||
|
dropId,
|
||||||
|
dropContext,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unlisten = () => {
|
||||||
|
document.removeEventListener('mousemove', onDrag);
|
||||||
|
document.removeEventListener('mouseup', onDragEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
dispatch(onDragEndThunk());
|
||||||
|
unlisten();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onDrag);
|
||||||
|
document.addEventListener('mouseup', onDragEnd);
|
||||||
|
return unlisten;
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dragging || !draggingId) return;
|
||||||
|
return registerDraggableEvents(draggingId);
|
||||||
|
}, [dragging, draggingId, registerDraggableEvents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shadowRef.current) return;
|
||||||
|
if (!dragShadowVisible) {
|
||||||
|
shadowRef.current.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadow = shadowRef.current;
|
||||||
|
|
||||||
|
const draggingNode = document.querySelector(`[data-draggable-id="${draggingId}"]`);
|
||||||
|
|
||||||
|
if (!draggingNode) return;
|
||||||
|
const clone = draggingNode.cloneNode(true);
|
||||||
|
|
||||||
|
shadow.appendChild(clone);
|
||||||
|
}, [dragShadowVisible, draggingId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<div
|
||||||
|
ref={shadowRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: draggingPosition?.y,
|
||||||
|
left: draggingPosition?.x,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
opacity: dragShadowVisible ? 1 : 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockDragDropContext;
|
@ -0,0 +1,82 @@
|
|||||||
|
import React, { useCallback, useMemo, useRef } 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;
|
||||||
|
const isDragging = draggableState.dragging && draggableState.draggingId === id;
|
||||||
|
|
||||||
|
if (draggableState.dropId === id) {
|
||||||
|
return {
|
||||||
|
dropState: {
|
||||||
|
dropId: draggableState.dropId,
|
||||||
|
insertType: draggableState.insertType,
|
||||||
|
},
|
||||||
|
isDragging,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dropState: null,
|
||||||
|
isDragging,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDragStart = useCallback(
|
||||||
|
(event: React.MouseEvent | MouseEvent) => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const { clientY: y, clientX: x } = event;
|
||||||
|
|
||||||
|
const context = getDragDropContext(id);
|
||||||
|
|
||||||
|
if (!context) return;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
blockDraggableActions.startDrag({
|
||||||
|
startDraggingPosition: {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
},
|
||||||
|
draggingId: id,
|
||||||
|
draggingContext: {
|
||||||
|
type,
|
||||||
|
contextId: context.contextId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, id, type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const beforeDropping = useMemo(() => {
|
||||||
|
if (!dropState) return false;
|
||||||
|
return dropState.insertType === DragInsertType.BEFORE;
|
||||||
|
}, [dropState]);
|
||||||
|
|
||||||
|
const afterDropping = useMemo(() => {
|
||||||
|
if (!dropState) return false;
|
||||||
|
return dropState.insertType === DragInsertType.AFTER;
|
||||||
|
}, [dropState]);
|
||||||
|
|
||||||
|
const childDropping = useMemo(() => {
|
||||||
|
if (!dropState) return false;
|
||||||
|
return dropState.insertType === DragInsertType.CHILD;
|
||||||
|
}, [dropState]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDragStart,
|
||||||
|
ref,
|
||||||
|
beforeDropping,
|
||||||
|
afterDropping,
|
||||||
|
childDropping,
|
||||||
|
isDragging,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
import React, { useEffect, useState } 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);
|
||||||
|
|
||||||
|
const commonCls = 'pointer-events-none absolute z-10 w-[100%] bg-fill-hover transition-all duration-200';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getAnchorEl) return;
|
||||||
|
const el = getAnchorEl();
|
||||||
|
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener('mousedown', onDragStart);
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('mousedown', onDragStart);
|
||||||
|
};
|
||||||
|
}, [getAnchorEl, onDragStart]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-draggable-id={id}
|
||||||
|
data-draggable-type={type}
|
||||||
|
onMouseDown={getAnchorEl ? undefined : onDragStart}
|
||||||
|
className={'relative'}
|
||||||
|
style={{
|
||||||
|
opacity: isDragging ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: beforeDropping ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
className={`${commonCls} left-0 top-[-2px] h-[4px]`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
{
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: childDropping ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
className={`${commonCls} left-0 top-0 h-[100%] opacity-[0.3]`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: afterDropping ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
className={`${commonCls} bottom-[-2px] left-0 h-[4px]`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(BlockDraggable);
|
@ -7,19 +7,19 @@ import { useTranslation } from 'react-i18next';
|
|||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
caption: string;
|
subtitle: string;
|
||||||
onOk: () => Promise<void>;
|
onOk: () => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfirmDialog({ open, title, caption, onOk, onClose }: Props) {
|
function ConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
||||||
<DialogContent className={'flex w-[540px] flex-col items-center justify-center'}>
|
<DialogContent className={'flex w-[540px] flex-col items-center justify-center'}>
|
||||||
<div className={'text-md m-2 font-bold'}>{title}</div>
|
<div className={'text-md m-2 font-bold'}>{title}</div>
|
||||||
<div className={'m-1 text-sm text-text-caption'}>{caption}</div>
|
<div className={'m-1 text-sm text-text-caption'}>{subtitle}</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant={'outlined'} onClick={onClose}>
|
<Button variant={'outlined'} onClick={onClose}>
|
@ -15,6 +15,7 @@ export const BoardSettingsPopup = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [settingsItems, setSettingsItems] = useState<IPopupItem[]>([]);
|
const [settingsItems, setSettingsItems] = useState<IPopupItem[]>([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSettingsItems([
|
setSettingsItems([
|
||||||
{
|
{
|
||||||
@ -23,7 +24,7 @@ export const BoardSettingsPopup = ({
|
|||||||
<PropertiesSvg></PropertiesSvg>
|
<PropertiesSvg></PropertiesSvg>
|
||||||
</i>
|
</i>
|
||||||
),
|
),
|
||||||
title: t('grid.settings.Properties'),
|
title: t('grid.settings.properties'),
|
||||||
onClick: onFieldsClick,
|
onClick: onFieldsClick,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -42,7 +43,7 @@ export const BoardSettingsPopup = ({
|
|||||||
<PopupSelect
|
<PopupSelect
|
||||||
onOutsideClick={() => hidePopup()}
|
onOutsideClick={() => hidePopup()}
|
||||||
items={settingsItems}
|
items={settingsItems}
|
||||||
className={'absolute top-full left-full z-10 text-xs'}
|
className={'absolute left-full top-full z-10 text-xs'}
|
||||||
></PopupSelect>
|
></PopupSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -117,8 +117,12 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
|
|||||||
|
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
const isTapToClick = isApple() && timeStampRef.current > 0 && Date.now() - timeStampRef.current < onFrameTime;
|
const isTapToClick = isApple() && timeStampRef.current > 0 && Date.now() - timeStampRef.current < onFrameTime;
|
||||||
|
|
||||||
|
const isTextBox = (e.target as HTMLElement).closest(`[role="textbox"]`);
|
||||||
|
|
||||||
|
if (!isTextBox) return;
|
||||||
// skip if the target is not a block
|
// skip if the target is not a block
|
||||||
const blockId = getBlockIdByPoint(e.target as HTMLElement);
|
const blockId = getBlockIdByPoint(e.target as HTMLElement);
|
||||||
|
|
||||||
@ -144,7 +148,6 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
|
|||||||
anchorRef.current = {
|
anchorRef.current = {
|
||||||
...anchor,
|
...anchor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// set the anchor point and focus point
|
// set the anchor point and focus point
|
||||||
dispatch(rangeActions.setAnchorPoint({ docId, anchorPoint: anchor }));
|
dispatch(rangeActions.setAnchorPoint({ docId, anchorPoint: anchor }));
|
||||||
dispatch(rangeActions.setFocusPoint({ docId, focusPoint: anchor }));
|
dispatch(rangeActions.setFocusPoint({ docId, focusPoint: anchor }));
|
||||||
|
@ -52,6 +52,7 @@ export function useBlockRectSelection({ container, getIntersectedBlockIds }: Blo
|
|||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
if (isPointInBlock(e.target as HTMLElement)) {
|
if (isPointInBlock(e.target as HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
|
import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
|
||||||
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||||
import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
|
import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
|
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> = {
|
const headingBlockTopOffset: Record<number, number> = {
|
||||||
1: 6,
|
1: 6,
|
||||||
@ -11,66 +14,76 @@ const headingBlockTopOffset: Record<number, number> = {
|
|||||||
3: 3,
|
3: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useBlockSideToolbar({ container }: { container: HTMLDivElement }) {
|
export function useBlockSideToolbar(id: string) {
|
||||||
const [nodeId, setHoverNodeId] = useState<string | null>(null);
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [style, setStyle] = useState<React.CSSProperties>({});
|
|
||||||
const { docId } = useSubscribeDocument();
|
const { docId } = useSubscribeDocument();
|
||||||
|
|
||||||
useEffect(() => {
|
const isDragging = useAppSelector((state) => {
|
||||||
const el = ref.current;
|
return (
|
||||||
|
get(state, [RECT_RANGE_NAME, docId, 'isDragging'], false) ||
|
||||||
if (!el || !nodeId) return;
|
get(state, [RANGE_NAME, docId, 'isDragging'], false) ||
|
||||||
void (async () => {
|
get(state, ['blockDraggable', 'dragging'], false)
|
||||||
const node = getBlock(docId, nodeId);
|
);
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
setStyle({
|
|
||||||
opacity: '0',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
});
|
});
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [opacity, setOpacity] = useState(0);
|
||||||
|
|
||||||
|
const topOffset = useMemo(() => {
|
||||||
|
const block = getBlock(docId, id);
|
||||||
|
|
||||||
|
if (!block) return 0;
|
||||||
|
if (block.type === BlockType.HeadingBlock) {
|
||||||
|
return headingBlockTopOffset[(block.data as HeadingBlockData).level];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.type === BlockType.DividerBlock) {
|
||||||
|
return -6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}, [docId, id]);
|
||||||
|
|
||||||
|
const onMouseMove = useCallback(
|
||||||
|
(e: Event) => {
|
||||||
|
if (isDragging) {
|
||||||
|
setOpacity(0);
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
let top = 0;
|
|
||||||
|
|
||||||
if (node.type === BlockType.HeadingBlock) {
|
|
||||||
const nodeData = node.data as HeadingBlockData;
|
|
||||||
|
|
||||||
top = headingBlockTopOffset[nodeData.level];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === BlockType.DividerBlock) {
|
const target = (e.target as HTMLElement).closest('[data-block-id]');
|
||||||
top = -3;
|
|
||||||
|
if (!target) return;
|
||||||
|
const targetId = target.getAttribute('data-block-id');
|
||||||
|
|
||||||
|
if (targetId !== id) {
|
||||||
|
setOpacity(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle({
|
setOpacity(1);
|
||||||
opacity: '1',
|
},
|
||||||
pointerEvents: 'auto',
|
[id, isDragging]
|
||||||
top: `${top}px`,
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [dispatch, docId, nodeId]);
|
|
||||||
|
|
||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
const onMouseLeave = useCallback(() => {
|
||||||
const { clientX, clientY } = e;
|
setOpacity(0);
|
||||||
const id = getNodeIdByPoint(clientX, clientY);
|
|
||||||
|
|
||||||
setHoverNodeId(id);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
container.addEventListener('mousemove', handleMouseMove);
|
const node = getNode(id);
|
||||||
|
|
||||||
|
if (!node) return;
|
||||||
|
node.addEventListener('mousemove', onMouseMove);
|
||||||
|
node.addEventListener('mouseleave', onMouseLeave);
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener('mousemove', handleMouseMove);
|
node.removeEventListener('mousemove', onMouseMove);
|
||||||
|
node.removeEventListener('mouseleave', onMouseLeave);
|
||||||
};
|
};
|
||||||
}, [container, handleMouseMove]);
|
}, [id, onMouseMove, onMouseLeave]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodeId,
|
|
||||||
ref,
|
ref,
|
||||||
style,
|
opacity,
|
||||||
|
topOffset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
const sx = { height: 24, width: 24 };
|
|
||||||
import { IconButton } from '@mui/material';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
|
||||||
|
|
||||||
const ToolbarButton = ({
|
|
||||||
onClick,
|
|
||||||
children,
|
|
||||||
tooltip,
|
|
||||||
}: {
|
|
||||||
tooltip: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Tooltip title={tooltip} placement={'top-start'}>
|
|
||||||
<IconButton onClick={onClick} sx={sx}>
|
|
||||||
{children}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ToolbarButton;
|
|
@ -1,85 +1,96 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useBlockSideToolbar, usePopover } from './BlockSideToolbar.hooks';
|
import { useBlockSideToolbar, usePopover } from './BlockSideToolbar.hooks';
|
||||||
import Portal from '../BlockPortal';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||||
import AddSharpIcon from '@mui/icons-material/AddSharp';
|
import AddSharpIcon from '@mui/icons-material/AddSharp';
|
||||||
import BlockMenu from './BlockMenu';
|
import BlockMenu from './BlockMenu';
|
||||||
import ToolbarButton from './ToolbarButton';
|
|
||||||
import { addBlockBelowClickThunk } from '$app_reducers/document/async-actions/menu';
|
import { addBlockBelowClickThunk } from '$app_reducers/document/async-actions/menu';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
|
||||||
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IconButton } from '@mui/material';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
||||||
export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
|
export default function BlockSideToolbar({ id }: { id: string }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { docId, controller } = useSubscribeDocument();
|
const { docId, controller } = useSubscribeDocument();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { nodeId, style, ref } = useBlockSideToolbar({ container });
|
const { handleOpen, open, ...popoverProps } = usePopover();
|
||||||
const isDragging = useAppSelector(
|
const { opacity, topOffset } = useBlockSideToolbar(id);
|
||||||
(state) => state[RANGE_NAME][docId]?.isDragging || state[RECT_RANGE_NAME][docId]?.isDragging
|
|
||||||
);
|
|
||||||
const { handleOpen, ...popoverProps } = usePopover();
|
|
||||||
|
|
||||||
if (!nodeId || isDragging) return null;
|
const show = opacity === 1 || open;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Portal blockId={nodeId}>
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
style={{
|
style={{
|
||||||
opacity: 0,
|
opacity: show ? 1 : 0,
|
||||||
...style,
|
top: topOffset,
|
||||||
}}
|
|
||||||
className='absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-500'
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
// prevent toolbar from taking focus away from editor
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
}}
|
||||||
|
className='absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-100'
|
||||||
>
|
>
|
||||||
{/** Add Block below */}
|
{/** Add Block below */}
|
||||||
<ToolbarButton
|
<Tooltip disableInteractive={true} title={t('blockActions.addBelowTooltip')} placement={'top-start'}>
|
||||||
tooltip={t('tooltip.addBlockBelow')}
|
<IconButton
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
style={{
|
||||||
if (!nodeId || !controller) return;
|
pointerEvents: show ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
onClick={(_: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
addBlockBelowClickThunk({
|
addBlockBelowClickThunk({
|
||||||
id: nodeId,
|
id,
|
||||||
controller,
|
controller,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AddSharpIcon />
|
<AddSharpIcon />
|
||||||
</ToolbarButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/** Open menu or drag */}
|
{/** Open menu or drag */}
|
||||||
<ToolbarButton
|
<Tooltip disableInteractive={true} title={t('blockActions.dragAndOpenTooltip')} placement={'top-start'}>
|
||||||
tooltip={t('tooltip.openMenu')}
|
<IconButton
|
||||||
|
style={{
|
||||||
|
pointerEvents: show ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
data-draggable-anchor={id}
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!nodeId) return;
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setRectSelectionThunk({
|
setRectSelectionThunk({
|
||||||
docId,
|
docId,
|
||||||
selection: [nodeId],
|
selection: [id],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
handleOpen(e);
|
handleOpen(e);
|
||||||
}}
|
}}
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DragIndicatorRoundedIcon />
|
<DragIndicatorRoundedIcon />
|
||||||
</ToolbarButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
|
||||||
|
|
||||||
<Popover {...popoverProps}>
|
<Popover open={open} {...popoverProps}>
|
||||||
<BlockMenu id={nodeId} onClose={popoverProps.onClose} />
|
<BlockMenu id={id} onClose={popoverProps.onClose} />
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default function DividerBlock() {
|
export default function DividerBlock() {
|
||||||
return (
|
return (
|
||||||
<div className={`flex h-[1em] w-[100%] items-center justify-center`}>
|
<div className={`flex h-[1em] w-[100%] items-center justify-center`}>
|
||||||
<div className={'h-[1px] w-[100%] bg-line-border'} />
|
<div className={'h-[1px] w-[100%] bg-line-divider'} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.ho
|
|||||||
import EquationBlock from '$app/components/document/EquationBlock';
|
import EquationBlock from '$app/components/document/EquationBlock';
|
||||||
import ImageBlock from '$app/components/document/ImageBlock';
|
import ImageBlock from '$app/components/document/ImageBlock';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import BlockDraggable from '$app/components/_shared/BlockDraggable';
|
||||||
|
import { BlockDraggableType } from '$app_reducers/block-draggable/slice';
|
||||||
|
|
||||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const { node, childIds, isSelected, ref } = useNode(id);
|
const { node, childIds, isSelected, ref } = useNode(id);
|
||||||
@ -79,6 +81,13 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeIdContext.Provider value={id}>
|
<NodeIdContext.Provider value={id}>
|
||||||
|
<BlockDraggable
|
||||||
|
id={id}
|
||||||
|
type={BlockDraggableType.BLOCK}
|
||||||
|
getAnchorEl={() => {
|
||||||
|
return ref.current?.querySelector(`[data-draggable-anchor="${id}"]`) || null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div {...props} ref={ref} data-block-id={node.id} className={`relative ${className}`}>
|
<div {...props} ref={ref} data-block-id={node.id} className={`relative ${className}`}>
|
||||||
{renderBlock()}
|
{renderBlock()}
|
||||||
<BlockOverlay id={id} />
|
<BlockOverlay id={id} />
|
||||||
@ -86,6 +95,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
|||||||
<div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-content-blue-100' />
|
<div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-content-blue-100' />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</BlockDraggable>
|
||||||
</NodeIdContext.Provider>
|
</NodeIdContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import BlockSideToolbar from '$app/components/document/BlockSideToolbar';
|
||||||
|
|
||||||
function BlockOverlay({ id }: { id: string }) {
|
function BlockOverlay({ id }: { id: string }) {
|
||||||
return <div className='block-overlay' />;
|
return (
|
||||||
|
<div className='block-overlay'>
|
||||||
|
<BlockSideToolbar id={id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlockOverlay;
|
export default BlockOverlay;
|
||||||
|
@ -15,7 +15,6 @@ export default function Overlay({ container }: { container: HTMLDivElement }) {
|
|||||||
useUndoRedo(container);
|
useUndoRedo(container);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BlockSideToolbar container={container} />
|
|
||||||
<TextActionMenu container={container} />
|
<TextActionMenu container={container} />
|
||||||
<BlockSelection container={container} />
|
<BlockSelection container={container} />
|
||||||
<BlockSlash container={container} />
|
<BlockSlash container={container} />
|
||||||
|
@ -4,6 +4,8 @@ import DocumentTitle from '../DocumentTitle';
|
|||||||
import Overlay from '../Overlay';
|
import Overlay from '../Overlay';
|
||||||
import { Node } from '$app/interfaces/document';
|
import { Node } from '$app/interfaces/document';
|
||||||
|
|
||||||
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
|
|
||||||
export default function VirtualizedList({
|
export default function VirtualizedList({
|
||||||
childIds,
|
childIds,
|
||||||
node,
|
node,
|
||||||
@ -16,10 +18,13 @@ export default function VirtualizedList({
|
|||||||
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
|
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||||
const virtualItems = virtualize.getVirtualItems();
|
const virtualItems = virtualize.getVirtualItems();
|
||||||
|
|
||||||
|
const { docId } = useSubscribeDocument();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
|
id={`appflowy-scroller_${docId}`}
|
||||||
className={`doc-scroller-container flex h-[100%] flex-wrap justify-center overflow-auto px-20`}
|
className={`doc-scroller-container flex h-[100%] flex-wrap justify-center overflow-auto px-20`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -42,6 +47,7 @@ export default function VirtualizedList({
|
|||||||
>
|
>
|
||||||
{virtualItems.map((virtualRow) => {
|
{virtualItems.map((virtualRow) => {
|
||||||
const id = childIds[virtualRow.index];
|
const id = childIds[virtualRow.index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='mt-[-0.5px] pt-[0.5px]'
|
className='mt-[-0.5px] pt-[0.5px]'
|
||||||
|
@ -45,7 +45,7 @@ export function useSubscribeNode(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getBlock(docId: string, id: string) {
|
export function getBlock(docId: string, id: string) {
|
||||||
return store.getState().document[docId].nodes[id];
|
return store.getState().document[docId]?.nodes[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeIdContext = createContext<string>('');
|
export const NodeIdContext = createContext<string>('');
|
||||||
|
@ -3,6 +3,7 @@ import SideBar from '$app/components/layout/SideBar';
|
|||||||
import TopBar from '$app/components/layout/TopBar';
|
import TopBar from '$app/components/layout/TopBar';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { FooterPanel } from '$app/components/layout/FooterPanel';
|
import { FooterPanel } from '$app/components/layout/FooterPanel';
|
||||||
|
import BlockDragDropContext from '$app/components/_shared/BlockDraggable/BlockDragDropContext';
|
||||||
|
|
||||||
function Layout({ children }: { children: ReactNode }) {
|
function Layout({ children }: { children: ReactNode }) {
|
||||||
const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
|
const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
|
||||||
@ -20,6 +21,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
|
<BlockDragDropContext>
|
||||||
<div className='flex h-screen w-[100%] text-sm text-text-title'>
|
<div className='flex h-screen w-[100%] text-sm text-text-title'>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
<div
|
<div
|
||||||
@ -41,6 +43,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
<FooterPanel />
|
<FooterPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</BlockDragDropContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
|
||||||
import Dialog from '@mui/material/Dialog';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import { Button, DialogActions } from '@mui/material';
|
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
import ConfirmDialog from '$app/components/_shared/app-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function DeleteDialog({
|
function DeleteDialog({
|
||||||
layout,
|
layout,
|
||||||
@ -28,36 +24,17 @@ function DeleteDialog({
|
|||||||
}[layout];
|
}[layout];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
<ConfirmDialog
|
||||||
<DialogContent className={'flex w-[540px] flex-col items-center justify-center'}>
|
open={open}
|
||||||
<div className={'text-md m-2 font-bold'}>
|
title={t('views.deleteContentTitle', {
|
||||||
{t('views.deleteContentTitle', {
|
|
||||||
pageType,
|
pageType,
|
||||||
})}
|
})}
|
||||||
</div>
|
subtitle={t('views.deleteContentCaption', {
|
||||||
<div className={'m-1 text-sm text-text-caption'}>
|
|
||||||
{t('views.deleteContentCaption', {
|
|
||||||
pageType,
|
pageType,
|
||||||
})}
|
})}
|
||||||
</div>
|
onOk={onOk}
|
||||||
</DialogContent>
|
onClose={onClose}
|
||||||
<DialogActions>
|
/>
|
||||||
<Button variant={'outlined'} onClick={onClose}>
|
|
||||||
{t('button.Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={'contained'}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await onOk();
|
|
||||||
onClose();
|
|
||||||
} catch (e) {}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('button.delete')}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
export function useLoadChildPages(pageId: string) {
|
export function useLoadChildPages(pageId: string) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const childPages = useAppSelector((state) => state.pages.childPages[pageId]);
|
const childPages = useAppSelector((state) => state.pages.relationMap[pageId]);
|
||||||
|
|
||||||
const collapsed = useAppSelector((state) => !state.pages.expandedPages[pageId]);
|
const collapsed = useAppSelector((state) => !state.pages.expandedIdMap[pageId]);
|
||||||
const toggleCollapsed = useCallback(() => {
|
const toggleCollapsed = useCallback(() => {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
dispatch(pagesActions.expandPage(pageId));
|
dispatch(pagesActions.expandPage(pageId));
|
||||||
@ -77,7 +77,7 @@ export function useLoadChildPages(pageId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePageActions(pageId: string) {
|
export function usePageActions(pageId: string) {
|
||||||
const page = useAppSelector((state) => state.pages.map[pageId]);
|
const page = useAppSelector((state) => state.pages.pageMap[pageId]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -27,7 +27,7 @@ function NestedPageTitle({
|
|||||||
onRename: (newName: string) => Promise<void>;
|
onRename: (newName: string) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const page = useAppSelector((state) => {
|
const page = useAppSelector((state) => {
|
||||||
return state.pages.map[pageId];
|
return state.pages.pageMap[pageId];
|
||||||
});
|
});
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
const isSelected = useSelectedPage(pageId);
|
const isSelected = useSelectedPage(pageId);
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import { TransitionGroup } from 'react-transition-group';
|
import { TransitionGroup } from 'react-transition-group';
|
||||||
import NestedPageTitle from '$app/components/layout/NestedPage/NestedPageTitle';
|
import NestedPageTitle from '$app/components/layout/NestedPage/NestedPageTitle';
|
||||||
import { useLoadChildPages, usePageActions } from '$app/components/layout/NestedPage/NestedPage.hooks';
|
import { useLoadChildPages, usePageActions } from '$app/components/layout/NestedPage/NestedPage.hooks';
|
||||||
|
import BlockDraggable from '$app/components/_shared/BlockDraggable';
|
||||||
|
import { BlockDraggableType } from '$app_reducers/block-draggable/slice';
|
||||||
|
|
||||||
function NestedPage({ pageId }: { pageId: string }) {
|
function NestedPage({ pageId }: { pageId: string }) {
|
||||||
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
|
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
|
||||||
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
|
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<BlockDraggable id={pageId} type={BlockDraggableType.PAGE}>
|
||||||
<NestedPageTitle
|
<NestedPageTitle
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPageClick();
|
onPageClick();
|
||||||
@ -32,8 +34,8 @@ function NestedPage({ pageId }: { pageId: string }) {
|
|||||||
))}
|
))}
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BlockDraggable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NestedPage;
|
export default React.memo(NestedPage);
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import NestedPage from '$app/components/layout/NestedPage';
|
import NestedPage from '$app/components/layout/NestedPage';
|
||||||
import { List } from '@mui/material';
|
import { List } from '@mui/material';
|
||||||
|
|
||||||
function WorkspaceNestedPages({ workspaceId }: { workspaceId: string }) {
|
function WorkspaceNestedPages({ workspaceId }: { workspaceId: string }) {
|
||||||
const pageIds = useAppSelector((state) => {
|
const pageIds = useAppSelector((state) => {
|
||||||
return state.pages.childPages[workspaceId];
|
return state.pages.relationMap[workspaceId];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className={'h-[100%] overflow-y-auto overflow-x-hidden'}>
|
<List id={`appflowy-scroller_${workspaceId}`} ref={ref} className={'h-[100%] overflow-y-auto overflow-x-hidden'}>
|
||||||
{pageIds?.map((pageId) => (
|
{pageIds?.map((pageId) => (
|
||||||
<NestedPage key={pageId} pageId={pageId} />
|
<NestedPage key={pageId} pageId={pageId} />
|
||||||
))}
|
))}
|
||||||
|
@ -14,6 +14,7 @@ function TrashButton() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
data-page-id={'trash'}
|
||||||
selected={currentPathType === 'trash'}
|
selected={currentPathType === 'trash'}
|
||||||
onClick={navigateToTrash}
|
onClick={navigateToTrash}
|
||||||
style={{
|
style={{
|
||||||
|
@ -20,26 +20,34 @@ export function useLoadWorkspaces() {
|
|||||||
return new WorkspaceManagerController();
|
return new WorkspaceManagerController();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const initializeWorkspaces = useCallback(async () => {
|
||||||
void (async () => {
|
|
||||||
const workspaces = await controller.getWorkspaces();
|
const workspaces = await controller.getWorkspaces();
|
||||||
const currentWorkspace = await controller.getCurrentWorkspace();
|
const currentWorkspace = await controller.getCurrentWorkspace();
|
||||||
|
|
||||||
await controller.subscribe({
|
|
||||||
onWorkspacesChanged,
|
|
||||||
});
|
|
||||||
dispatch(
|
dispatch(
|
||||||
workspaceActions.initWorkspaces({
|
workspaceActions.initWorkspaces({
|
||||||
workspaces,
|
workspaces,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}, [controller, dispatch]);
|
||||||
|
|
||||||
|
const subscribeToWorkspaces = useCallback(async () => {
|
||||||
|
await controller.subscribe({
|
||||||
|
onWorkspacesChanged,
|
||||||
|
});
|
||||||
|
}, [controller, onWorkspacesChanged]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
await initializeWorkspaces();
|
||||||
|
await subscribeToWorkspaces();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
};
|
};
|
||||||
}, [controller, dispatch, onWorkspacesChanged]);
|
}, [controller, initializeWorkspaces, subscribeToWorkspaces]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspaces,
|
workspaces,
|
||||||
@ -86,8 +94,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
|||||||
[dispatch, id]
|
[dispatch, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const initializeWorkspace = useCallback(async () => {
|
||||||
void (async () => {
|
|
||||||
const childPages = await controller.getChildPages();
|
const childPages = await controller.getChildPages();
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -96,17 +103,26 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
|||||||
childPages,
|
childPages,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}, [controller, dispatch, id]);
|
||||||
|
|
||||||
|
const subscribeToWorkspace = useCallback(async () => {
|
||||||
await controller.subscribe({
|
await controller.subscribe({
|
||||||
onWorkspaceChanged,
|
onWorkspaceChanged,
|
||||||
onWorkspaceDeleted,
|
onWorkspaceDeleted,
|
||||||
onChildPagesChanged,
|
onChildPagesChanged,
|
||||||
});
|
});
|
||||||
|
}, [controller, onChildPagesChanged, onWorkspaceChanged, onWorkspaceDeleted]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
await initializeWorkspace();
|
||||||
|
await subscribeToWorkspace();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
};
|
};
|
||||||
}, [controller, dispatch, id, onChildPagesChanged, onWorkspaceChanged, onWorkspaceDeleted]);
|
}, [controller, initializeWorkspace, subscribeToWorkspace]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openWorkspace,
|
openWorkspace,
|
||||||
|
@ -8,10 +8,10 @@ function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: bo
|
|||||||
const { openWorkspace, deleteWorkspace } = useLoadWorkspace(workspace);
|
const { openWorkspace, deleteWorkspace } = useLoadWorkspace(workspace);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex h-[100%] flex-col'}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: opened ? 'auto' : 0,
|
height: opened ? '100%' : 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
transition: 'height 0.2s ease-in-out',
|
transition: 'height 0.2s ease-in-out',
|
||||||
}}
|
}}
|
||||||
|
@ -10,7 +10,7 @@ function WorkspaceManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-[100%] flex-col justify-between'}>
|
<div className={'flex h-[100%] flex-col justify-between'}>
|
||||||
<List className={'flex-1 overflow-y-auto overflow-x-hidden'}>
|
<List className={'flex-1 overflow-hidden'}>
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.map((workspace) => (
|
||||||
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
|
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,28 +1,37 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { TrashController } from '$app/stores/effects/workspace/trash/controller';
|
import { TrashController } from '$app/stores/effects/workspace/trash/controller';
|
||||||
import { TrashPB } from '@/services/backend';
|
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
|
import { trashActions, trashPBToTrash } from '$app_reducers/trash/slice';
|
||||||
|
|
||||||
export function useLoadTrash() {
|
export function useLoadTrash() {
|
||||||
const [trash, setTrash] = useState<TrashPB[]>([]);
|
const trash = useAppSelector((state) => state.trash.list);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const controller = useMemo(() => {
|
const controller = useMemo(() => {
|
||||||
return new TrashController();
|
return new TrashController();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const initializeTrash = useCallback(async () => {
|
||||||
void (async () => {
|
|
||||||
const trash = await controller.getTrash();
|
const trash = await controller.getTrash();
|
||||||
|
|
||||||
setTrash(trash);
|
dispatch(trashActions.initTrash(trash.map(trashPBToTrash)));
|
||||||
})();
|
}, [controller, dispatch]);
|
||||||
}, [controller]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const subscribeToTrash = useCallback(async () => {
|
||||||
controller.subscribe({
|
controller.subscribe({
|
||||||
onTrashChanged: (trash) => {
|
onTrashChanged: (trash) => {
|
||||||
setTrash(trash);
|
dispatch(trashActions.onTrashChanged(trash.map(trashPBToTrash)));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}, [controller, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
await initializeTrash();
|
||||||
|
await subscribeToTrash();
|
||||||
|
})();
|
||||||
|
}, [initializeTrash, subscribeToTrash]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
};
|
};
|
||||||
@ -55,7 +64,7 @@ export function useTrashActions() {
|
|||||||
setDeleteAllDialogOpen(true);
|
setDeleteAllDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDislog = () => {
|
const closeDialog = () => {
|
||||||
setRestoreAllDialogOpen(false);
|
setRestoreAllDialogOpen(false);
|
||||||
setDeleteAllDialogOpen(false);
|
setDeleteAllDialogOpen(false);
|
||||||
};
|
};
|
||||||
@ -77,6 +86,6 @@ export function useTrashActions() {
|
|||||||
onClickDeleteAll,
|
onClickDeleteAll,
|
||||||
restoreAllDialogOpen,
|
restoreAllDialogOpen,
|
||||||
deleteAllDialogOpen,
|
deleteAllDialogOpen,
|
||||||
closeDislog,
|
closeDialog,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { DeleteOutline, RestoreOutlined } from '@mui/icons-material';
|
|||||||
import { useLoadTrash, useTrashActions } from '$app/components/trash/Trash.hooks';
|
import { useLoadTrash, useTrashActions } from '$app/components/trash/Trash.hooks';
|
||||||
import { Divider, List } from '@mui/material';
|
import { Divider, List } from '@mui/material';
|
||||||
import TrashItem from '$app/components/trash/TrashItem';
|
import TrashItem from '$app/components/trash/TrashItem';
|
||||||
import ConfirmDialog from '$app/components/trash/ConfirmDialog';
|
import ConfirmDialog from '$app/components/_shared/app-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function Trash() {
|
function Trash() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -19,7 +19,7 @@ function Trash() {
|
|||||||
deleteAllDialogOpen,
|
deleteAllDialogOpen,
|
||||||
onRestoreAll,
|
onRestoreAll,
|
||||||
onDeleteAll,
|
onDeleteAll,
|
||||||
closeDislog,
|
closeDialog,
|
||||||
} = useTrashActions();
|
} = useTrashActions();
|
||||||
const [hoverId, setHoverId] = useState('');
|
const [hoverId, setHoverId] = useState('');
|
||||||
|
|
||||||
@ -60,16 +60,16 @@ function Trash() {
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={restoreAllDialogOpen}
|
open={restoreAllDialogOpen}
|
||||||
title={t('trash.confirmRestoreAll.title')}
|
title={t('trash.confirmRestoreAll.title')}
|
||||||
caption={t('trash.confirmRestoreAll.caption')}
|
subtitle={t('trash.confirmRestoreAll.caption')}
|
||||||
onOk={onRestoreAll}
|
onOk={onRestoreAll}
|
||||||
onClose={closeDislog}
|
onClose={closeDialog}
|
||||||
/>
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={deleteAllDialogOpen}
|
open={deleteAllDialogOpen}
|
||||||
title={t('trash.confirmDeleteAll.title')}
|
title={t('trash.confirmDeleteAll.title')}
|
||||||
caption={t('trash.confirmDeleteAll.caption')}
|
subtitle={t('trash.confirmDeleteAll.caption')}
|
||||||
onOk={onDeleteAll}
|
onOk={onDeleteAll}
|
||||||
onClose={closeDislog}
|
onClose={closeDialog}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,20 +2,19 @@ import React from 'react';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { IconButton, ListItem } from '@mui/material';
|
import { IconButton, ListItem } from '@mui/material';
|
||||||
import { DeleteOutline, RestoreOutlined } from '@mui/icons-material';
|
import { DeleteOutline, RestoreOutlined } from '@mui/icons-material';
|
||||||
import { TrashPB } from '@/services/backend';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Trash } from '$app_reducers/trash/slice';
|
||||||
|
|
||||||
function TrashItem({
|
function TrashItem({
|
||||||
item,
|
item,
|
||||||
hoverId,
|
hoverId,
|
||||||
setHoverId,
|
setHoverId,
|
||||||
|
|
||||||
onDelete,
|
onDelete,
|
||||||
onPutback,
|
onPutback,
|
||||||
}: {
|
}: {
|
||||||
setHoverId: (id: string) => void;
|
setHoverId: (id: string) => void;
|
||||||
item: TrashPB;
|
item: Trash;
|
||||||
hoverId: string;
|
hoverId: string;
|
||||||
onPutback: (id: string) => void;
|
onPutback: (id: string) => void;
|
||||||
onDelete: (ids: string[]) => void;
|
onDelete: (ids: string[]) => void;
|
||||||
@ -37,8 +36,8 @@ function TrashItem({
|
|||||||
>
|
>
|
||||||
<div className={'flex w-[100%] items-center justify-around rounded-lg px-2 py-3 hover:bg-fill-list-hover'}>
|
<div className={'flex w-[100%] items-center justify-around rounded-lg px-2 py-3 hover:bg-fill-list-hover'}>
|
||||||
<div className={'w-[40%] text-left'}>{item.name}</div>
|
<div className={'w-[40%] text-left'}>{item.name}</div>
|
||||||
<div className={'flex-1'}>{dayjs.unix(item.modified_time).format('MM/DD/YYYY hh:mm A')}</div>
|
<div className={'flex-1'}>{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}</div>
|
||||||
<div className={'flex-1'}>{dayjs.unix(item.create_time).format('MM/DD/YYYY hh:mm A')}</div>
|
<div className={'flex-1'}>{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
visibility: hoverId === item.id ? 'visible' : 'hidden',
|
visibility: hoverId === item.id ? 'visible' : 'hidden',
|
||||||
@ -46,12 +45,12 @@ function TrashItem({
|
|||||||
className={'w-[64px]'}
|
className={'w-[64px]'}
|
||||||
>
|
>
|
||||||
<Tooltip placement={'top-start'} title={t('button.putback')}>
|
<Tooltip placement={'top-start'} title={t('button.putback')}>
|
||||||
<IconButton onClick={(e) => onPutback(item.id)} className={'mr-2'}>
|
<IconButton onClick={(_) => onPutback(item.id)} className={'mr-2'}>
|
||||||
<RestoreOutlined />
|
<RestoreOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement={'top-start'} title={t('button.delete')}>
|
<Tooltip placement={'top-start'} title={t('button.delete')}>
|
||||||
<IconButton color={'error'} onClick={(e) => onDelete([item.id])}>
|
<IconButton color={'error'} onClick={(_) => onDelete([item.id])}>
|
||||||
<DeleteOutline />
|
<DeleteOutline />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -6,12 +6,14 @@ import {
|
|||||||
FolderEventDuplicateView,
|
FolderEventDuplicateView,
|
||||||
FolderEventCloseView,
|
FolderEventCloseView,
|
||||||
FolderEventImportData,
|
FolderEventImportData,
|
||||||
|
FolderEventMoveView,
|
||||||
ViewIdPB,
|
ViewIdPB,
|
||||||
CreateViewPayloadPB,
|
CreateViewPayloadPB,
|
||||||
UpdateViewPayloadPB,
|
UpdateViewPayloadPB,
|
||||||
RepeatedViewIdPB,
|
RepeatedViewIdPB,
|
||||||
ViewPB,
|
ViewPB,
|
||||||
ImportPB,
|
ImportPB,
|
||||||
|
MoveViewPayloadPB,
|
||||||
} from '@/services/backend/events/flowy-folder2';
|
} from '@/services/backend/events/flowy-folder2';
|
||||||
import { Page } from '$app_reducers/pages/slice';
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
@ -28,6 +30,19 @@ export class PageBackendService {
|
|||||||
return FolderEventReadView(payload);
|
return FolderEventReadView(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
movePage = async (params: { viewId: string; parentId: string; prevId?: string }) => {
|
||||||
|
console.log('movePage', params);
|
||||||
|
const payload = new MoveViewPayloadPB({
|
||||||
|
view_id: params.viewId,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
parent_view_id: params.parentId,
|
||||||
|
prev_view_id: params.prevId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return FolderEventMoveView(payload);
|
||||||
|
};
|
||||||
|
|
||||||
createPage = async (params: ReturnType<typeof CreateViewPayloadPB.prototype.toObject>) => {
|
createPage = async (params: ReturnType<typeof CreateViewPayloadPB.prototype.toObject>) => {
|
||||||
const payload = CreateViewPayloadPB.fromObject(params);
|
const payload = CreateViewPayloadPB.fromObject(params);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CreateViewPayloadPB, UpdateViewPayloadPB, ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
import { PageBackendService } from '$app/stores/effects/workspace/page/page_bd_svc';
|
import { PageBackendService } from '$app/stores/effects/workspace/page/page_bd_svc';
|
||||||
import { WorkspaceObserver } from '$app/stores/effects/workspace/workspace_observer';
|
import { WorkspaceObserver } from '$app/stores/effects/workspace/workspace_observer';
|
||||||
import { Page, parserViewPBToPage } from '$app_reducers/pages/slice';
|
import { Page, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||||
@ -31,6 +31,20 @@ export class PageController {
|
|||||||
return Promise.reject(result.err);
|
return Promise.reject(result.err);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
movePage = async (params: { parentId: string; prevId?: string }): Promise<void> => {
|
||||||
|
const result = await this.backendService.movePage({
|
||||||
|
viewId: this.id,
|
||||||
|
parentId: params.parentId,
|
||||||
|
prevId: params.prevId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
return result.val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(result.err);
|
||||||
|
};
|
||||||
|
|
||||||
getChildPages = async (): Promise<Page[]> => {
|
getChildPages = async (): Promise<Page[]> => {
|
||||||
const result = await this.backendService.getPage(this.id);
|
const result = await this.backendService.getPage(this.id);
|
||||||
|
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from '$app/stores/store';
|
||||||
|
import { blockDraggableActions, BlockDraggableType } from '$app_reducers/block-draggable/slice';
|
||||||
|
import { dragThunk } from '$app_reducers/document/async-actions/drag';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { movePageThunk } from '$app_reducers/pages/async_actions';
|
||||||
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
|
export const onDragEndThunk = createAsyncThunk('blockDraggable/onDragEnd', async (payload: void, thunkAPI) => {
|
||||||
|
const { getState, dispatch } = thunkAPI;
|
||||||
|
const { dragging, draggingId, dropId, insertType, draggingContext, dropContext } = (getState() as RootState)
|
||||||
|
.blockDraggable;
|
||||||
|
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
dispatch(blockDraggableActions.endDrag());
|
||||||
|
|
||||||
|
if (!draggingId || !dropId || !insertType || !draggingContext || !dropContext) return;
|
||||||
|
if (draggingContext.type !== dropContext.type) {
|
||||||
|
// TODO: will support this in the future
|
||||||
|
Log.info('Unsupported drag this block to different type of block');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropContext.type === BlockDraggableType.BLOCK) {
|
||||||
|
const docId = dropContext.contextId;
|
||||||
|
|
||||||
|
if (!docId) return;
|
||||||
|
await dispatch(
|
||||||
|
dragThunk({
|
||||||
|
draggingId,
|
||||||
|
dropId,
|
||||||
|
insertType,
|
||||||
|
controller: new DocumentController(docId),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropContext.type === BlockDraggableType.PAGE) {
|
||||||
|
const workspaceId = dropContext.contextId;
|
||||||
|
|
||||||
|
if (!workspaceId) return;
|
||||||
|
await dispatch(
|
||||||
|
movePageThunk({
|
||||||
|
sourceId: draggingId,
|
||||||
|
targetId: dropId,
|
||||||
|
insertType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,100 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const DRAG_DISTANCE_THRESHOLD = 10;
|
||||||
|
|
||||||
|
export enum BlockDraggableType {
|
||||||
|
BLOCK = 'BLOCK',
|
||||||
|
PAGE = 'PAGE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DraggableContext {
|
||||||
|
type: BlockDraggableType;
|
||||||
|
contextId?: string;
|
||||||
|
}
|
||||||
|
export interface BlockDraggableState {
|
||||||
|
dragging: boolean;
|
||||||
|
startDraggingPosition?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
draggingPosition?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
isDraggable: boolean;
|
||||||
|
dragShadowVisible: boolean;
|
||||||
|
draggingId?: string;
|
||||||
|
insertType?: DragInsertType;
|
||||||
|
dropId?: string;
|
||||||
|
dropContext?: DraggableContext;
|
||||||
|
draggingContext?: DraggableContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DragInsertType {
|
||||||
|
BEFORE = 'BEFORE',
|
||||||
|
AFTER = 'AFTER',
|
||||||
|
CHILD = 'CHILD',
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: BlockDraggableState = {
|
||||||
|
dragging: false,
|
||||||
|
isDraggable: true,
|
||||||
|
dragShadowVisible: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const blockDraggableSlice = createSlice({
|
||||||
|
name: 'blockDraggable',
|
||||||
|
initialState: initialState,
|
||||||
|
reducers: {
|
||||||
|
startDrag: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
startDraggingPosition: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
draggingId: string;
|
||||||
|
draggingContext: DraggableContext;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { draggingContext, startDraggingPosition, draggingId } = action.payload;
|
||||||
|
|
||||||
|
state.dragging = true;
|
||||||
|
state.startDraggingPosition = startDraggingPosition;
|
||||||
|
state.draggingId = draggingId;
|
||||||
|
state.draggingContext = draggingContext;
|
||||||
|
},
|
||||||
|
|
||||||
|
drag: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
draggingPosition: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
insertType?: DragInsertType;
|
||||||
|
dropId?: string;
|
||||||
|
dropContext?: DraggableContext;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { dropContext, dropId, draggingPosition, insertType } = action.payload;
|
||||||
|
|
||||||
|
state.draggingPosition = draggingPosition;
|
||||||
|
state.dropContext = dropContext;
|
||||||
|
const moveDistance = Math.sqrt(
|
||||||
|
Math.pow(draggingPosition.x - state.startDraggingPosition!.x, 2) +
|
||||||
|
Math.pow(draggingPosition.y - state.startDraggingPosition!.y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
state.dropId = dropId;
|
||||||
|
state.insertType = insertType;
|
||||||
|
state.dragShadowVisible = moveDistance > DRAG_DISTANCE_THRESHOLD;
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag: (state) => {
|
||||||
|
return initialState;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blockDraggableActions = blockDraggableSlice.actions;
|
@ -0,0 +1,54 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from '$app/stores/store';
|
||||||
|
import { DragInsertType } from '$app_reducers/block-draggable/slice';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
|
||||||
|
export const dragThunk = createAsyncThunk(
|
||||||
|
'document/drag',
|
||||||
|
async (
|
||||||
|
payload: {
|
||||||
|
draggingId: string;
|
||||||
|
dropId: string;
|
||||||
|
insertType: DragInsertType;
|
||||||
|
controller: DocumentController;
|
||||||
|
},
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { getState } = thunkAPI;
|
||||||
|
const { draggingId, dropId, insertType, controller } = payload;
|
||||||
|
const docId = controller.documentId;
|
||||||
|
const documentState = (getState() as RootState).document[docId];
|
||||||
|
const { nodes, children } = documentState;
|
||||||
|
const draggingNode = nodes[draggingId];
|
||||||
|
const targetNode = nodes[dropId];
|
||||||
|
const targetChildren = children[targetNode.children] || [];
|
||||||
|
const targetParentId = targetNode.parent;
|
||||||
|
|
||||||
|
if (!targetParentId) return;
|
||||||
|
const targetParent = nodes[targetParentId];
|
||||||
|
const targetParentChildren = children[targetParent.children] || [];
|
||||||
|
let prevId, parentId;
|
||||||
|
|
||||||
|
if (insertType === DragInsertType.BEFORE) {
|
||||||
|
const targetIndex = targetParentChildren.indexOf(dropId);
|
||||||
|
const prevIndex = targetIndex - 1;
|
||||||
|
|
||||||
|
parentId = targetParentId;
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
prevId = targetParentChildren[prevIndex];
|
||||||
|
}
|
||||||
|
} else if (insertType === DragInsertType.AFTER) {
|
||||||
|
prevId = dropId;
|
||||||
|
parentId = targetParentId;
|
||||||
|
} else {
|
||||||
|
parentId = dropId;
|
||||||
|
if (targetChildren.length > 0) {
|
||||||
|
prevId = targetChildren[targetChildren.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = [controller.getMoveAction(draggingNode, parentId, prevId || null)];
|
||||||
|
|
||||||
|
await controller.applyActions(actions);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,58 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from '$app/stores/store';
|
||||||
|
import { DragInsertType } from '$app_reducers/block-draggable/slice';
|
||||||
|
import { PageController } from '$app/stores/effects/workspace/page/page_controller';
|
||||||
|
|
||||||
|
export const movePageThunk = createAsyncThunk(
|
||||||
|
'pages/movePage',
|
||||||
|
async (
|
||||||
|
payload: {
|
||||||
|
sourceId: string;
|
||||||
|
targetId: string;
|
||||||
|
insertType: DragInsertType;
|
||||||
|
},
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { sourceId, targetId, insertType } = payload;
|
||||||
|
const { getState } = thunkAPI;
|
||||||
|
const { pageMap, relationMap } = (getState() as RootState).pages;
|
||||||
|
const sourcePage = pageMap[sourceId];
|
||||||
|
const targetPage = pageMap[targetId];
|
||||||
|
|
||||||
|
if (!sourcePage || !targetPage) return;
|
||||||
|
const sourceParentId = sourcePage.parentId;
|
||||||
|
const targetParentId = targetPage.parentId;
|
||||||
|
|
||||||
|
if (!sourceParentId || !targetParentId) return;
|
||||||
|
|
||||||
|
const targetParentChildren = relationMap[targetParentId] || [];
|
||||||
|
const targetIndex = targetParentChildren.indexOf(targetId);
|
||||||
|
|
||||||
|
if (targetIndex < 0) return;
|
||||||
|
|
||||||
|
let prevId, parentId;
|
||||||
|
|
||||||
|
if (insertType === DragInsertType.BEFORE) {
|
||||||
|
const prevIndex = targetIndex - 1;
|
||||||
|
|
||||||
|
parentId = targetParentId;
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
prevId = targetParentChildren[prevIndex];
|
||||||
|
}
|
||||||
|
} else if (insertType === DragInsertType.AFTER) {
|
||||||
|
prevId = targetId;
|
||||||
|
parentId = targetParentId;
|
||||||
|
} else {
|
||||||
|
const targetChildren = relationMap[targetId] || [];
|
||||||
|
|
||||||
|
parentId = targetId;
|
||||||
|
if (targetChildren.length > 0) {
|
||||||
|
prevId = targetChildren[targetChildren.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new PageController(sourceId);
|
||||||
|
|
||||||
|
await controller.movePage({ parentId, prevId });
|
||||||
|
}
|
||||||
|
);
|
@ -22,15 +22,15 @@ export function parserViewPBToPage(view: ViewPB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PageState {
|
export interface PageState {
|
||||||
map: Record<string, Page>;
|
pageMap: Record<string, Page>;
|
||||||
childPages: Record<string, string[]>;
|
relationMap: Record<string, string[] | undefined>;
|
||||||
expandedPages: Record<string, boolean>;
|
expandedIdMap: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState: PageState = {
|
export const initialState: PageState = {
|
||||||
map: {},
|
pageMap: {},
|
||||||
childPages: {},
|
relationMap: {},
|
||||||
expandedPages: {},
|
expandedIdMap: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pagesSlice = createSlice({
|
export const pagesSlice = createSlice({
|
||||||
@ -54,29 +54,29 @@ export const pagesSlice = createSlice({
|
|||||||
children.push(page.id);
|
children.push(page.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
state.map = {
|
state.pageMap = {
|
||||||
...state.map,
|
...state.pageMap,
|
||||||
...pageMap,
|
...pageMap,
|
||||||
};
|
};
|
||||||
state.childPages[id] = children;
|
state.relationMap[id] = children;
|
||||||
},
|
},
|
||||||
|
|
||||||
removeChildPages(state, action: PayloadAction<string>) {
|
removeChildPages(state, action: PayloadAction<string>) {
|
||||||
const parentId = action.payload;
|
const parentId = action.payload;
|
||||||
|
|
||||||
delete state.childPages[parentId];
|
delete state.relationMap[parentId];
|
||||||
},
|
},
|
||||||
|
|
||||||
expandPage(state, action: PayloadAction<string>) {
|
expandPage(state, action: PayloadAction<string>) {
|
||||||
const id = action.payload;
|
const id = action.payload;
|
||||||
|
|
||||||
state.expandedPages[id] = true;
|
state.expandedIdMap[id] = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
collapsePage(state, action: PayloadAction<string>) {
|
collapsePage(state, action: PayloadAction<string>) {
|
||||||
const id = action.payload;
|
const id = action.payload;
|
||||||
|
|
||||||
state.expandedPages[id] = false;
|
state.expandedIdMap[id] = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { TrashPB } from '@/services/backend';
|
||||||
|
|
||||||
|
export interface Trash {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
modifiedTime: number;
|
||||||
|
createTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trashPBToTrash(trash: TrashPB) {
|
||||||
|
return {
|
||||||
|
id: trash.id,
|
||||||
|
name: trash.name,
|
||||||
|
modifiedTime: trash.modified_time,
|
||||||
|
createTime: trash.create_time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrashState {
|
||||||
|
list: Trash[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TrashState = {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trashSlice = createSlice({
|
||||||
|
name: 'trash',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
initTrash: (state, action: PayloadAction<Trash[]>) => {
|
||||||
|
state.list = action.payload;
|
||||||
|
},
|
||||||
|
onTrashChanged: (state, action: PayloadAction<Trash[]>) => {
|
||||||
|
state.list = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trashActions = trashSlice.actions;
|
@ -16,6 +16,8 @@ import { documentReducers } from './reducers/document/slice';
|
|||||||
import { boardSlice } from './reducers/board/slice';
|
import { boardSlice } from './reducers/board/slice';
|
||||||
import { errorSlice } from './reducers/error/slice';
|
import { errorSlice } from './reducers/error/slice';
|
||||||
import { sidebarSlice } from '$app_reducers/sidebar/slice';
|
import { sidebarSlice } from '$app_reducers/sidebar/slice';
|
||||||
|
import { blockDraggableSlice } from '$app_reducers/block-draggable/slice';
|
||||||
|
import { trashSlice } from '$app_reducers/trash/slice';
|
||||||
|
|
||||||
const listenerMiddlewareInstance = createListenerMiddleware({
|
const listenerMiddlewareInstance = createListenerMiddleware({
|
||||||
onError: () => console.error,
|
onError: () => console.error,
|
||||||
@ -31,6 +33,8 @@ const store = configureStore({
|
|||||||
[workspaceSlice.name]: workspaceSlice.reducer,
|
[workspaceSlice.name]: workspaceSlice.reducer,
|
||||||
[errorSlice.name]: errorSlice.reducer,
|
[errorSlice.name]: errorSlice.reducer,
|
||||||
[sidebarSlice.name]: sidebarSlice.reducer,
|
[sidebarSlice.name]: sidebarSlice.reducer,
|
||||||
|
[blockDraggableSlice.name]: blockDraggableSlice.reducer,
|
||||||
|
[trashSlice.name]: trashSlice.reducer,
|
||||||
...documentReducers,
|
...documentReducers,
|
||||||
},
|
},
|
||||||
middleware: (gDM) => gDM({ serializableCheck: false }).prepend(listenerMiddlewareInstance.middleware),
|
middleware: (gDM) => gDM({ serializableCheck: false }).prepend(listenerMiddlewareInstance.middleware),
|
||||||
|
113
frontend/appflowy_tauri/src/appflowy_app/utils/draggable.ts
Normal file
113
frontend/appflowy_tauri/src/appflowy_app/utils/draggable.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { BlockDraggableType, DragInsertType } from '$app_reducers/block-draggable/slice';
|
||||||
|
import { findParent } from '$app/utils/document/node';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
|
||||||
|
export function getDraggableIdByPoint(target: HTMLElement | null) {
|
||||||
|
let node = target;
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
const id = node.getAttribute('data-draggable-id');
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = node.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDraggableNode(id: string) {
|
||||||
|
return document.querySelector(`[data-draggable-id="${id}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDragDropContext(id: string) {
|
||||||
|
const node = getDraggableNode(id);
|
||||||
|
|
||||||
|
if (!node) return;
|
||||||
|
const type = node.getAttribute('data-draggable-type') as BlockDraggableType;
|
||||||
|
const container = node.closest('[id^=appflowy-scroller]');
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
const containerId = container.id;
|
||||||
|
const contextId = containerId.split('_')[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
contextId,
|
||||||
|
container,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collisionNode(event: MouseEvent, draggingId: string) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const { clientY, target, clientX } = event;
|
||||||
|
|
||||||
|
if (!target) return;
|
||||||
|
let id = getDraggableIdByPoint(target as HTMLElement);
|
||||||
|
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
if (id === draggingId) return;
|
||||||
|
|
||||||
|
const parentIsDraggingId = (target as HTMLElement).closest(`[data-draggable-id="${draggingId}"]`);
|
||||||
|
|
||||||
|
if (parentIsDraggingId) return;
|
||||||
|
|
||||||
|
const node = getDraggableNode(id);
|
||||||
|
|
||||||
|
if (!node) return;
|
||||||
|
const { top, bottom, left } = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
let parent = node.parentElement;
|
||||||
|
let nodeLeft = left;
|
||||||
|
|
||||||
|
while (parent && clientX < nodeLeft) {
|
||||||
|
const parentNode = findParent(parent, '[data-draggable-id]');
|
||||||
|
|
||||||
|
if (!parentNode) break;
|
||||||
|
const parentId = parentNode.getAttribute('data-draggable-id');
|
||||||
|
|
||||||
|
id = parentId || id;
|
||||||
|
nodeLeft = parentNode.getBoundingClientRect().left;
|
||||||
|
parent = parentNode.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertType = DragInsertType.CHILD;
|
||||||
|
|
||||||
|
if (clientY - top < 4) {
|
||||||
|
insertType = DragInsertType.BEFORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientY > bottom - 4) {
|
||||||
|
insertType = DragInsertType.AFTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
insertType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollThreshold = 20;
|
||||||
|
|
||||||
|
export function scrollIntoViewIfNeeded(e: MouseEvent, container: HTMLDivElement) {
|
||||||
|
const { top, bottom } = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
let delta = 0;
|
||||||
|
|
||||||
|
if (e.clientY + scrollThreshold >= bottom) {
|
||||||
|
delta = e.clientY + scrollThreshold - bottom;
|
||||||
|
} else if (e.clientY - scrollThreshold <= top) {
|
||||||
|
delta = e.clientY - scrollThreshold - top;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.scrollBy(0, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDragContextId() {
|
||||||
|
return nanoid(10);
|
||||||
|
}
|
@ -6,14 +6,17 @@ export function debounce(fn: (...args: any[]) => void, delay: number) {
|
|||||||
fn.apply(undefined, args);
|
fn.apply(undefined, args);
|
||||||
}, delay);
|
}, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
debounceFn.cancel = () => {
|
debounceFn.cancel = () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
return debounceFn;
|
return debounceFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) {
|
export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) {
|
||||||
let timeout: NodeJS.Timeout | null = null;
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
if (!timeout) {
|
if (!timeout) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
@ -27,25 +30,31 @@ export function throttle(fn: (...args: any[]) => void, delay: number, immediate
|
|||||||
|
|
||||||
export function get<T = any>(obj: any, path: string[], defaultValue?: any): T {
|
export function get<T = any>(obj: any, path: string[], defaultValue?: any): T {
|
||||||
let value = obj;
|
let value = obj;
|
||||||
|
|
||||||
for (const prop of path) {
|
for (const prop of path) {
|
||||||
value = value[prop];
|
if (value === undefined || typeof value !== 'object' || value[prop] === undefined) {
|
||||||
if (value === undefined) {
|
|
||||||
return defaultValue !== undefined ? defaultValue : undefined;
|
return defaultValue !== undefined ? defaultValue : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value = value[prop];
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function set(obj: any, path: string[], value: any): void {
|
export function set(obj: any, path: string[], value: any): void {
|
||||||
let current = obj;
|
let current = obj;
|
||||||
|
|
||||||
for (let i = 0; i < path.length; i++) {
|
for (let i = 0; i < path.length; i++) {
|
||||||
const prop = path[i];
|
const prop = path[i];
|
||||||
|
|
||||||
if (i === path.length - 1) {
|
if (i === path.length - 1) {
|
||||||
current[prop] = value;
|
current[prop] = value;
|
||||||
} else {
|
} else {
|
||||||
if (!current[prop]) {
|
if (!current[prop]) {
|
||||||
current[prop] = {};
|
current[prop] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current[prop];
|
current = current[prop];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,6 +93,7 @@ export function isEqual<T>(value1: T, value2: T): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,8 +107,10 @@ export function clone<T>(value: T): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result: any = {};
|
const result: any = {};
|
||||||
|
|
||||||
for (const key in value) {
|
for (const key in value) {
|
||||||
result[key] = clone(value[key]);
|
result[key] = clone(value[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export const BoardPage = () => {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [viewId, setViewId] = useState('');
|
const [viewId, setViewId] = useState('');
|
||||||
const pagesStore = useAppSelector((state) => state.pages);
|
const pagesStore = useAppSelector((state) => state.pages);
|
||||||
const page = useAppSelector((state) => (params.id ? state.pages.map[params.id] : undefined));
|
const page = useAppSelector((state) => (params.id ? state.pages.pageMap[params.id] : undefined));
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"addBelowTooltip": "Click to add below",
|
"addBelowTooltip": "Click to add below",
|
||||||
"addAboveCmd": "Alt+click",
|
"addAboveCmd": "Alt+click",
|
||||||
"addAboveMacCmd": "Option+click",
|
"addAboveMacCmd": "Option+click",
|
||||||
"addAboveTooltip": "to add above"
|
"addAboveTooltip": "to add above",
|
||||||
|
"dragAndOpenTooltip": "Drag to reorder, click to open"
|
||||||
},
|
},
|
||||||
"signUp": {
|
"signUp": {
|
||||||
"buttonText": "Sign Up",
|
"buttonText": "Sign Up",
|
||||||
|
20
frontend/rust-lib/Cargo.lock
generated
20
frontend/rust-lib/Cargo.lock
generated
@ -85,7 +85,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -897,7 +897,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -915,7 +915,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-client-ws"
|
name = "collab-client-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync",
|
||||||
@ -933,7 +933,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -960,7 +960,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -972,7 +972,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -991,7 +991,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1011,7 +1011,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1031,7 +1031,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1065,7 +1065,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=52b550b#52b550b3dc3ff5969b92fea0c0a2b03530d73d20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
|
@ -34,11 +34,11 @@ opt-level = 3
|
|||||||
incremental = false
|
incremental = false
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" }
|
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "52b550b" }
|
||||||
|
|
||||||
#collab = { path = "../AppFlowy-Collab/collab" }
|
#collab = { path = "../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user