feat: support views drag and drop (#3004)

This commit is contained in:
Kilu.He 2023-07-19 17:59:32 +08:00 committed by GitHub
parent 0fb004aee0
commit 5ab64f8835
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1085 additions and 286 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) ||
get(state, [RANGE_NAME, docId, 'isDragging'], false) ||
get(state, ['blockDraggable', 'dragging'], false)
);
});
const ref = useRef<HTMLDivElement | null>(null);
const [opacity, setOpacity] = useState(0);
if (!el || !nodeId) return; const topOffset = useMemo(() => {
void (async () => { const block = getBlock(docId, id);
const node = getBlock(docId, nodeId);
if (!node) { if (!block) return 0;
setStyle({ if (block.type === BlockType.HeadingBlock) {
opacity: '0', return headingBlockTopOffset[(block.data as HeadingBlockData).level];
pointerEvents: 'none', }
});
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) {
top = -3;
}
setStyle({
opacity: '1',
pointerEvents: 'auto',
top: `${top}px`,
});
} }
})();
}, [dispatch, docId, nodeId]);
const handleMouseMove = useCallback((e: MouseEvent) => { const target = (e.target as HTMLElement).closest('[data-block-id]');
const { clientX, clientY } = e;
const id = getNodeIdByPoint(clientX, clientY);
setHoverNodeId(id); if (!target) return;
const targetId = target.getAttribute('data-block-id');
if (targetId !== id) {
setOpacity(0);
return;
}
setOpacity(1);
},
[id, isDragging]
);
const onMouseLeave = useCallback(() => {
setOpacity(0);
}, []); }, []);
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,
}; };
} }

View File

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

View File

@ -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 style={{
ref={ref} opacity: show ? 1 : 0,
style={{ top: topOffset,
opacity: 0, }}
...style, className='absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-100'
}} >
className='absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-500' {/** Add Block below */}
onMouseDown={(e) => { <Tooltip disableInteractive={true} title={t('blockActions.addBelowTooltip')} placement={'top-start'}>
// prevent toolbar from taking focus away from editor <IconButton
e.preventDefault(); style={{
e.stopPropagation(); pointerEvents: show ? 'auto' : 'none',
}} }}
> onClick={(_: React.MouseEvent<HTMLButtonElement>) => {
{/** Add Block below */}
<ToolbarButton
tooltip={t('tooltip.addBlockBelow')}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
if (!nodeId || !controller) return;
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>
</div> </Tooltip>
</Portal> </div>
<Popover {...popoverProps}> <Popover open={open} {...popoverProps}>
<BlockMenu id={nodeId} onClose={popoverProps.onClose} /> <BlockMenu id={id} onClose={popoverProps.onClose} />
</Popover> </Popover>
</> </>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,27 +21,29 @@ function Layout({ children }: { children: ReactNode }) {
}; };
}, []); }, []);
return ( return (
<div className='flex h-screen w-[100%] text-sm text-text-title'> <BlockDragDropContext>
<SideBar /> <div className='flex h-screen w-[100%] text-sm text-text-title'>
<div <SideBar />
className='flex flex-1 flex-col bg-bg-body'
style={{
width: isCollapsed ? 'auto' : `calc(100% - ${width}px)`,
}}
>
<TopBar />
<div <div
className='flex flex-1 flex-col bg-bg-body'
style={{ style={{
height: 'calc(100vh - 64px - 48px)', width: isCollapsed ? 'auto' : `calc(100% - ${width}px)`,
}} }}
className={'overflow-y-auto overflow-x-hidden'}
> >
{children} <TopBar />
</div> <div
style={{
height: 'calc(100vh - 64px - 48px)',
}}
className={'overflow-y-auto overflow-x-hidden'}
>
{children}
</div>
<FooterPanel /> <FooterPanel />
</div>
</div> </div>
</div> </BlockDragDropContext>
); );
} }

View File

@ -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, })}
})} subtitle={t('views.deleteContentCaption', {
</div> pageType,
<div className={'m-1 text-sm text-text-caption'}> })}
{t('views.deleteContentCaption', { onOk={onOk}
pageType, onClose={onClose}
})} />
</div>
</DialogContent>
<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>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,26 +20,34 @@ export function useLoadWorkspaces() {
return new WorkspaceManagerController(); return new WorkspaceManagerController();
}, []); }, []);
const initializeWorkspaces = useCallback(async () => {
const workspaces = await controller.getWorkspaces();
const currentWorkspace = await controller.getCurrentWorkspace();
dispatch(
workspaceActions.initWorkspaces({
workspaces,
currentWorkspace,
})
);
}, [controller, dispatch]);
const subscribeToWorkspaces = useCallback(async () => {
await controller.subscribe({
onWorkspacesChanged,
});
}, [controller, onWorkspacesChanged]);
useEffect(() => { useEffect(() => {
void (async () => { void (async () => {
const workspaces = await controller.getWorkspaces(); await initializeWorkspaces();
const currentWorkspace = await controller.getCurrentWorkspace(); await subscribeToWorkspaces();
await controller.subscribe({
onWorkspacesChanged,
});
dispatch(
workspaceActions.initWorkspaces({
workspaces,
currentWorkspace,
})
);
})(); })();
return () => { return () => {
controller.dispose(); controller.dispose();
}; };
}, [controller, dispatch, onWorkspacesChanged]); }, [controller, initializeWorkspaces, subscribeToWorkspaces]);
return { return {
workspaces, workspaces,
@ -86,27 +94,35 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
[dispatch, id] [dispatch, id]
); );
const initializeWorkspace = useCallback(async () => {
const childPages = await controller.getChildPages();
dispatch(
pagesActions.addChildPages({
id,
childPages,
})
);
}, [controller, dispatch, id]);
const subscribeToWorkspace = useCallback(async () => {
await controller.subscribe({
onWorkspaceChanged,
onWorkspaceDeleted,
onChildPagesChanged,
});
}, [controller, onChildPagesChanged, onWorkspaceChanged, onWorkspaceDeleted]);
useEffect(() => { useEffect(() => {
void (async () => { void (async () => {
const childPages = await controller.getChildPages(); await initializeWorkspace();
await subscribeToWorkspace();
dispatch(
pagesActions.addChildPages({
id,
childPages,
})
);
await controller.subscribe({
onWorkspaceChanged,
onWorkspaceDeleted,
onChildPagesChanged,
});
})(); })();
return () => { return () => {
controller.dispose(); controller.dispose();
}; };
}, [controller, dispatch, id, onChildPagesChanged, onWorkspaceChanged, onWorkspaceDeleted]); }, [controller, initializeWorkspace, subscribeToWorkspace]);
return { return {
openWorkspace, openWorkspace,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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