AcadeDoc/client/src/features/page/tree/page-tree.tsx
Philipinho 4cb7a56f65 feat: comments
* create comment
* reply to comment thread
* edit comment
* delete comment
* resolve comment
2023-11-09 16:52:34 +00:00

266 lines
6.8 KiB
TypeScript

import { NodeApi, NodeRendererProps, Tree, TreeApi } from 'react-arborist';
import {
IconArrowsLeftRight,
IconChevronDown,
IconChevronRight,
IconCornerRightUp,
IconDotsVertical,
IconEdit,
IconFileDescription,
IconLink,
IconPlus,
IconStar,
IconTrash,
} from '@tabler/icons-react';
import React, { useEffect, useRef } from 'react';
import clsx from 'clsx';
import classes from './styles/tree.module.css';
import { ActionIcon, Menu, rem } from '@mantine/core';
import { useAtom } from 'jotai';
import { FillFlexParent } from './components/fill-flex-parent';
import { TreeNode } from './types';
import { treeApiAtom } from './atoms/tree-api-atom';
import { usePersistence } from '@/features/page/tree/hooks/use-persistence';
import { getPages } from '@/features/page/services/page-service';
import useWorkspacePageOrder from '@/features/page/tree/hooks/use-workspace-page-order';
import { useNavigate, useParams } from 'react-router-dom';
import { convertToTree } from '@/features/page/tree/utils';
export default function PageTree() {
const { data, setData, controllers } = usePersistence<TreeApi<TreeNode>>();
const [tree, setTree] = useAtom<TreeApi<TreeNode>>(treeApiAtom);
const { data: pageOrderData } = useWorkspacePageOrder();
const rootElement = useRef<HTMLDivElement>();
const { pageId } = useParams();
const fetchAndSetTreeData = async () => {
if (pageOrderData?.childrenIds) {
try {
const pages = await getPages();
const treeData = convertToTree(pages, pageOrderData.childrenIds);
setData(treeData);
} catch (err) {
console.error('Error fetching tree data: ', err);
}
}
};
useEffect(() => {
fetchAndSetTreeData();
}, [pageOrderData?.childrenIds]);
useEffect(() => {
setTimeout(() => {
tree?.select(pageId);
tree?.scrollTo(pageId, 'center');
}, 200);
}, [tree, pageId]);
return (
<div ref={rootElement} className={classes.treeContainer}>
<FillFlexParent>
{(dimens) => (
<Tree
data={data}
{...controllers}
{...dimens}
// @ts-ignore
ref={(t) => setTree(t)}
openByDefault={false}
disableMultiSelection={true}
className={classes.tree}
rowClassName={classes.row}
padding={15}
rowHeight={30}
overscanCount={5}
dndRootElement={rootElement.current}
>
{Node}
</Tree>
)}
</FillFlexParent>
</div>
);
}
function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
const navigate = useNavigate();
const handleClick = () => {
navigate(`/p/${node.id}`);
};
if (node.willReceiveDrop && node.isClosed) {
setTimeout(() => {
if (node.state.willReceiveDrop) node.open();
}, 650);
}
return (
<>
<div
style={style}
className={clsx(classes.node, node.state)}
ref={dragHandle}
onClick={handleClick}
>
<PageArrow node={node} />
<IconFileDescription size="18px" style={{ marginRight: '4px' }} />
<span className={classes.text}>
{node.isEditing ? (
<Input node={node} />
) : (
node.data.name || 'untitled'
)}
</span>
<div className={classes.actions}>
<NodeMenu node={node} />
<CreateNode node={node} />
</div>
</div>
</>
);
}
function CreateNode({ node }: { node: NodeApi<TreeNode> }) {
const [tree] = useAtom(treeApiAtom);
function handleCreate() {
tree?.create({ type: 'internal', parentId: node.id, index: 0 });
}
return (
<ActionIcon variant="transparent" color="gray" onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCreate();
}}>
<IconPlus style={{ width: rem(20), height: rem(20) }} stroke={2} />
</ActionIcon>
);
}
function NodeMenu({ node }: { node: NodeApi<TreeNode> }) {
const [tree] = useAtom(treeApiAtom);
function handleDelete() {
tree?.delete(node);
}
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<ActionIcon variant="transparent" color="gray" onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}>
<IconDotsVertical
style={{ width: rem(20), height: rem(20) }}
stroke={2}
/>
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconEdit style={{ width: rem(14), height: rem(14) }} />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
node.edit();
}}
>
Rename
</Menu.Item>
<Menu.Item
leftSection={<IconStar style={{ width: rem(14), height: rem(14) }} />}
>
Favorite
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconLink style={{ width: rem(14), height: rem(14) }} />}
>
Copy link
</Menu.Item>
<Menu.Item
leftSection={
<IconCornerRightUp style={{ width: rem(14), height: rem(14) }} />
}
>
Move
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={
<IconArrowsLeftRight style={{ width: rem(14), height: rem(14) }} />
}
>
Archive
</Menu.Item>
<Menu.Item
color="red"
leftSection={
<IconTrash style={{ width: rem(14), height: rem(14) }} />
}
onClick={() => handleDelete()}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}
function PageArrow({ node }: { node: NodeApi<TreeNode> }) {
return (
<ActionIcon size={20} variant="subtle" color="gray"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
node.toggle();
}}>
{node.isInternal ? (
node.children && node.children.length > 0 ? (
node.isOpen ? (
<IconChevronDown stroke={2} size={18} />
) : (
<IconChevronRight stroke={2} size={18} />
)
) : (
<IconChevronRight size={18} style={{ visibility: 'hidden' }} />
)
) : null}
</ActionIcon>
);
}
function Input({ node }: { node: NodeApi<TreeNode> }) {
return (
<input
autoFocus
name="name"
type="text"
placeholder="untitled"
defaultValue={node.data.name}
onFocus={(e) => e.currentTarget.select()}
onBlur={() => node.reset()}
onKeyDown={(e) => {
if (e.key === 'Escape') node.reset();
if (e.key === 'Enter') node.submit(e.currentTarget.value);
}}
/>
);
}