2018-05-06 05:45:10 +00:00
|
|
|
// @flow
|
2020-08-09 05:53:59 +00:00
|
|
|
import { observer } from "mobx-react";
|
|
|
|
import * as React from "react";
|
2020-12-16 03:07:29 +00:00
|
|
|
import { useDrag, useDrop } from "react-dnd";
|
2020-12-02 05:59:18 +00:00
|
|
|
import { useTranslation } from "react-i18next";
|
2020-06-20 20:59:15 +00:00
|
|
|
import styled from "styled-components";
|
2020-08-09 05:53:59 +00:00
|
|
|
import Collection from "models/Collection";
|
2020-06-20 20:59:15 +00:00
|
|
|
import Document from "models/Document";
|
|
|
|
import Fade from "components/Fade";
|
2021-08-23 07:37:28 +00:00
|
|
|
import Disclosure from "./Disclosure";
|
2020-12-31 20:51:12 +00:00
|
|
|
import DropCursor from "./DropCursor";
|
2021-04-06 02:05:27 +00:00
|
|
|
import DropToImport from "./DropToImport";
|
2020-09-16 01:01:40 +00:00
|
|
|
import EditableTitle from "./EditableTitle";
|
2020-08-09 05:53:59 +00:00
|
|
|
import SidebarLink from "./SidebarLink";
|
2021-07-15 19:27:03 +00:00
|
|
|
import useBoolean from "hooks/useBoolean";
|
2020-12-02 05:59:18 +00:00
|
|
|
import useStores from "hooks/useStores";
|
2020-08-09 05:53:59 +00:00
|
|
|
import DocumentMenu from "menus/DocumentMenu";
|
2020-06-20 20:59:15 +00:00
|
|
|
import { type NavigationNode } from "types";
|
2018-05-06 05:45:10 +00:00
|
|
|
|
2020-09-16 01:01:40 +00:00
|
|
|
type Props = {|
|
2019-07-13 17:15:38 +00:00
|
|
|
node: NavigationNode,
|
2020-09-16 01:01:40 +00:00
|
|
|
canUpdate: boolean,
|
2019-04-18 02:11:23 +00:00
|
|
|
collection?: Collection,
|
2018-05-06 05:45:10 +00:00
|
|
|
activeDocument: ?Document,
|
|
|
|
prefetchDocument: (documentId: string) => Promise<void>,
|
|
|
|
depth: number,
|
2020-12-31 20:51:12 +00:00
|
|
|
index: number,
|
|
|
|
parentId?: string,
|
2020-09-16 01:01:40 +00:00
|
|
|
|};
|
2018-05-06 05:45:10 +00:00
|
|
|
|
2021-07-01 22:01:30 +00:00
|
|
|
function DocumentLink(
|
|
|
|
{
|
|
|
|
node,
|
|
|
|
canUpdate,
|
|
|
|
collection,
|
|
|
|
activeDocument,
|
|
|
|
prefetchDocument,
|
|
|
|
depth,
|
|
|
|
index,
|
|
|
|
parentId,
|
|
|
|
}: Props,
|
|
|
|
ref
|
|
|
|
) {
|
2020-12-16 03:07:29 +00:00
|
|
|
const { documents, policies } = useStores();
|
2020-12-02 05:59:18 +00:00
|
|
|
const { t } = useTranslation();
|
2019-07-13 17:15:38 +00:00
|
|
|
|
2020-12-02 05:59:18 +00:00
|
|
|
const isActiveDocument = activeDocument && activeDocument.id === node.id;
|
|
|
|
const hasChildDocuments = !!node.children.length;
|
|
|
|
|
|
|
|
const document = documents.get(node.id);
|
|
|
|
const { fetchChildDocuments } = documents;
|
2019-12-23 01:06:39 +00:00
|
|
|
|
2020-12-02 05:59:18 +00:00
|
|
|
React.useEffect(() => {
|
|
|
|
if (isActiveDocument && hasChildDocuments) {
|
|
|
|
fetchChildDocuments(node.id);
|
2019-12-23 01:06:39 +00:00
|
|
|
}
|
2020-12-02 05:59:18 +00:00
|
|
|
}, [fetchChildDocuments, node, hasChildDocuments, isActiveDocument]);
|
|
|
|
|
2020-12-16 03:07:29 +00:00
|
|
|
const pathToNode = React.useMemo(
|
|
|
|
() =>
|
|
|
|
collection && collection.pathToDocument(node.id).map((entry) => entry.id),
|
|
|
|
[collection, node]
|
|
|
|
);
|
|
|
|
|
2020-12-02 05:59:18 +00:00
|
|
|
const showChildren = React.useMemo(() => {
|
|
|
|
return !!(
|
|
|
|
hasChildDocuments &&
|
2018-05-06 05:45:10 +00:00
|
|
|
activeDocument &&
|
2019-04-18 02:11:23 +00:00
|
|
|
collection &&
|
|
|
|
(collection
|
2020-12-16 03:07:29 +00:00
|
|
|
.pathToDocument(activeDocument.id)
|
2020-08-09 01:53:11 +00:00
|
|
|
.map((entry) => entry.id)
|
2019-07-13 17:15:38 +00:00
|
|
|
.includes(node.id) ||
|
2020-12-02 05:59:18 +00:00
|
|
|
isActiveDocument)
|
2018-05-06 05:45:10 +00:00
|
|
|
);
|
2020-12-02 05:59:18 +00:00
|
|
|
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
|
|
|
|
|
|
|
|
const [expanded, setExpanded] = React.useState(showChildren);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (showChildren) {
|
|
|
|
setExpanded(showChildren);
|
|
|
|
}
|
|
|
|
}, [showChildren]);
|
|
|
|
|
2020-12-31 20:51:12 +00:00
|
|
|
// when the last child document is removed,
|
|
|
|
// also close the local folder state to closed
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (expanded && !hasChildDocuments) {
|
|
|
|
setExpanded(false);
|
|
|
|
}
|
|
|
|
}, [expanded, hasChildDocuments]);
|
|
|
|
|
2020-12-02 05:59:18 +00:00
|
|
|
const handleDisclosureClick = React.useCallback(
|
|
|
|
(ev: SyntheticEvent<>) => {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
setExpanded(!expanded);
|
|
|
|
},
|
|
|
|
[expanded]
|
|
|
|
);
|
|
|
|
|
|
|
|
const handleMouseEnter = React.useCallback(
|
|
|
|
(ev: SyntheticEvent<>) => {
|
|
|
|
prefetchDocument(node.id);
|
|
|
|
},
|
|
|
|
[prefetchDocument, node]
|
|
|
|
);
|
|
|
|
|
|
|
|
const handleTitleChange = React.useCallback(
|
|
|
|
async (title: string) => {
|
|
|
|
if (!document) return;
|
|
|
|
|
|
|
|
await documents.update({
|
|
|
|
id: document.id,
|
|
|
|
lastRevision: document.revision,
|
|
|
|
text: document.text,
|
|
|
|
title,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[documents, document]
|
|
|
|
);
|
|
|
|
|
2021-07-15 19:27:03 +00:00
|
|
|
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
2020-12-16 03:07:29 +00:00
|
|
|
const isMoving = documents.movingDocumentId === node.id;
|
2020-12-31 20:51:12 +00:00
|
|
|
const manualSort = collection?.sort.field === "index";
|
2020-12-16 03:07:29 +00:00
|
|
|
|
|
|
|
// Draggable
|
|
|
|
const [{ isDragging }, drag] = useDrag({
|
2021-03-10 02:41:30 +00:00
|
|
|
type: "document",
|
|
|
|
item: () => ({ ...node, depth, active: isActiveDocument }),
|
2020-12-16 03:07:29 +00:00
|
|
|
collect: (monitor) => ({
|
|
|
|
isDragging: !!monitor.isDragging(),
|
|
|
|
}),
|
|
|
|
canDrag: (monitor) => {
|
2021-07-20 19:19:41 +00:00
|
|
|
return (
|
|
|
|
policies.abilities(node.id).move ||
|
|
|
|
policies.abilities(node.id).archive ||
|
|
|
|
policies.abilities(node.id).delete
|
|
|
|
);
|
2020-12-16 03:07:29 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2021-01-03 01:20:13 +00:00
|
|
|
const hoverExpanding = React.useRef(null);
|
|
|
|
|
|
|
|
// We set a timeout when the user first starts hovering over the document link,
|
|
|
|
// to trigger expansion of children. Clear this timeout when they stop hovering.
|
|
|
|
const resetHoverExpanding = React.useCallback(() => {
|
|
|
|
if (hoverExpanding.current) {
|
|
|
|
clearTimeout(hoverExpanding.current);
|
|
|
|
hoverExpanding.current = null;
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
2020-12-31 20:51:12 +00:00
|
|
|
// Drop to re-parent
|
|
|
|
const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({
|
2020-12-16 03:07:29 +00:00
|
|
|
accept: "document",
|
2021-03-10 02:41:30 +00:00
|
|
|
drop: (item, monitor) => {
|
2020-12-31 20:51:12 +00:00
|
|
|
if (monitor.didDrop()) return;
|
2020-12-16 03:07:29 +00:00
|
|
|
if (!collection) return;
|
|
|
|
documents.move(item.id, collection.id, node.id);
|
|
|
|
},
|
2021-01-03 01:20:13 +00:00
|
|
|
|
2020-12-16 03:07:29 +00:00
|
|
|
canDrop: (item, monitor) =>
|
|
|
|
pathToNode && !pathToNode.includes(monitor.getItem().id),
|
2021-01-03 01:20:13 +00:00
|
|
|
|
|
|
|
hover: (item, monitor) => {
|
|
|
|
// Enables expansion of document children when hovering over the document
|
|
|
|
// for more than half a second.
|
|
|
|
if (
|
|
|
|
hasChildDocuments &&
|
|
|
|
monitor.canDrop() &&
|
|
|
|
monitor.isOver({ shallow: true })
|
|
|
|
) {
|
|
|
|
if (!hoverExpanding.current) {
|
|
|
|
hoverExpanding.current = setTimeout(() => {
|
|
|
|
hoverExpanding.current = null;
|
|
|
|
if (monitor.isOver({ shallow: true })) {
|
|
|
|
setExpanded(true);
|
|
|
|
}
|
|
|
|
}, 500);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-12-16 03:07:29 +00:00
|
|
|
collect: (monitor) => ({
|
2020-12-31 20:51:12 +00:00
|
|
|
isOverReparent: !!monitor.isOver({ shallow: true }),
|
|
|
|
canDropToReparent: monitor.canDrop(),
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
// Drop to reorder
|
|
|
|
const [{ isOverReorder }, dropToReorder] = useDrop({
|
|
|
|
accept: "document",
|
2021-03-10 02:41:30 +00:00
|
|
|
drop: (item, monitor) => {
|
2020-12-31 20:51:12 +00:00
|
|
|
if (!collection) return;
|
|
|
|
if (item.id === node.id) return;
|
|
|
|
|
|
|
|
if (expanded) {
|
|
|
|
documents.move(item.id, collection.id, node.id, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
documents.move(item.id, collection.id, parentId, index + 1);
|
|
|
|
},
|
|
|
|
collect: (monitor) => ({
|
|
|
|
isOverReorder: !!monitor.isOver(),
|
2020-12-16 03:07:29 +00:00
|
|
|
}),
|
|
|
|
});
|
2020-12-02 05:59:18 +00:00
|
|
|
|
|
|
|
return (
|
2020-12-16 03:07:29 +00:00
|
|
|
<>
|
2021-08-23 07:37:28 +00:00
|
|
|
<Relative onDragLeave={resetHoverExpanding}>
|
2020-12-31 20:51:12 +00:00
|
|
|
<Draggable
|
|
|
|
key={node.id}
|
|
|
|
ref={drag}
|
|
|
|
$isDragging={isDragging}
|
|
|
|
$isMoving={isMoving}
|
|
|
|
>
|
|
|
|
<div ref={dropToReparent}>
|
|
|
|
<DropToImport documentId={node.id} activeClassName="activeDropZone">
|
|
|
|
<SidebarLink
|
|
|
|
onMouseEnter={handleMouseEnter}
|
|
|
|
to={{
|
|
|
|
pathname: node.url,
|
|
|
|
state: { title: node.title },
|
|
|
|
}}
|
|
|
|
label={
|
|
|
|
<>
|
|
|
|
{hasChildDocuments && (
|
|
|
|
<Disclosure
|
|
|
|
expanded={expanded && !isDragging}
|
|
|
|
onClick={handleDisclosureClick}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<EditableTitle
|
|
|
|
title={node.title || t("Untitled")}
|
|
|
|
onSubmit={handleTitleChange}
|
|
|
|
canUpdate={canUpdate}
|
2020-12-16 03:07:29 +00:00
|
|
|
/>
|
2020-12-31 20:51:12 +00:00
|
|
|
</>
|
|
|
|
}
|
|
|
|
isActiveDrop={isOverReparent && canDropToReparent}
|
|
|
|
depth={depth}
|
|
|
|
exact={false}
|
2021-01-03 03:11:13 +00:00
|
|
|
showActions={menuOpen}
|
2021-08-23 07:37:28 +00:00
|
|
|
scrollIntoViewIfNeeded={!document?.isStarred}
|
2021-07-01 22:01:30 +00:00
|
|
|
ref={ref}
|
2020-12-31 20:51:12 +00:00
|
|
|
menu={
|
|
|
|
document && !isMoving ? (
|
|
|
|
<Fade>
|
|
|
|
<DocumentMenu
|
|
|
|
document={document}
|
2021-07-15 19:27:03 +00:00
|
|
|
onOpen={handleMenuOpen}
|
|
|
|
onClose={handleMenuClose}
|
2020-12-31 20:51:12 +00:00
|
|
|
/>
|
|
|
|
</Fade>
|
|
|
|
) : undefined
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</DropToImport>
|
|
|
|
</div>
|
|
|
|
</Draggable>
|
|
|
|
{manualSort && (
|
|
|
|
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
|
|
|
)}
|
2021-08-23 07:37:28 +00:00
|
|
|
</Relative>
|
2020-12-16 03:07:29 +00:00
|
|
|
{expanded && !isDragging && (
|
2020-12-02 05:59:18 +00:00
|
|
|
<>
|
2020-12-31 20:51:12 +00:00
|
|
|
{node.children.map((childNode, index) => (
|
2020-12-02 05:59:18 +00:00
|
|
|
<ObservedDocumentLink
|
|
|
|
key={childNode.id}
|
|
|
|
collection={collection}
|
|
|
|
node={childNode}
|
|
|
|
activeDocument={activeDocument}
|
|
|
|
prefetchDocument={prefetchDocument}
|
|
|
|
depth={depth + 1}
|
|
|
|
canUpdate={canUpdate}
|
2020-12-31 20:51:12 +00:00
|
|
|
index={index}
|
|
|
|
parentId={node.id}
|
2020-12-02 05:59:18 +00:00
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
)}
|
2020-12-16 03:07:29 +00:00
|
|
|
</>
|
2020-12-02 05:59:18 +00:00
|
|
|
);
|
2018-05-06 05:45:10 +00:00
|
|
|
}
|
|
|
|
|
2021-08-23 07:37:28 +00:00
|
|
|
const Relative = styled.div`
|
|
|
|
position: relative;
|
2020-12-16 03:07:29 +00:00
|
|
|
`;
|
|
|
|
|
2021-08-23 07:37:28 +00:00
|
|
|
const Draggable = styled.div`
|
|
|
|
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
|
|
|
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
2020-12-02 05:59:18 +00:00
|
|
|
`;
|
2018-05-06 05:45:10 +00:00
|
|
|
|
2021-07-01 22:01:30 +00:00
|
|
|
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
|
|
|
|
|
2020-12-02 05:59:18 +00:00
|
|
|
export default ObservedDocumentLink;
|