mirror of
https://github.com/outline/outline.git
synced 2025-12-29 23:09:55 -06:00
fix: Allow dragging shared documents to starred section (#7506)
* fix: Allow dragging shared documents to starred section * fix: Allow read-only collection drag and drop fix: Full screen delete modal from drag and drop
This commit is contained in:
@@ -5,7 +5,6 @@ import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
@@ -39,9 +38,6 @@ const CollectionLink: React.FC<Props> = ({
|
||||
onDisclosureClick,
|
||||
isDraggingAnyCollection,
|
||||
}: Props) => {
|
||||
const itemRef = React.useRef<
|
||||
NavigationNode & { depth: number; active: boolean; collectionId: string }
|
||||
>();
|
||||
const { dialogs, documents, collections } = useStores();
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
@@ -86,8 +82,6 @@ const CollectionLink: React.FC<Props> = ({
|
||||
prevCollection.permission !== collection.permission &&
|
||||
!document?.isDraft
|
||||
) {
|
||||
itemRef.current = item;
|
||||
|
||||
dialogs.openModal({
|
||||
title: t("Move document"),
|
||||
content: (
|
||||
|
||||
@@ -2,11 +2,9 @@ import { Location } from "history";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { getEmptyImage } from "react-dnd-html5-backend";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import styled from "styled-components";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||
@@ -29,6 +27,7 @@ import Folder from "./Folder";
|
||||
import Relative from "./Relative";
|
||||
import { SidebarContextType, useSidebarContext } from "./SidebarContext";
|
||||
import SidebarLink, { DragObject } from "./SidebarLink";
|
||||
import { useDragDocument, useDropToReorderDocument } from "./useDragAndDrop";
|
||||
|
||||
type Props = {
|
||||
node: NavigationNode;
|
||||
@@ -142,30 +141,12 @@ function InnerDocumentLink(
|
||||
);
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const isMoving = documents.movingDocumentId === node.id;
|
||||
const manualSort = collection?.sort.field === "index";
|
||||
const can = policies.abilities(node.id);
|
||||
const icon = document?.icon || node.icon || node.emoji;
|
||||
const color = document?.color || node.color;
|
||||
|
||||
// Draggable
|
||||
const [{ isDragging }, drag, preview] = useDrag({
|
||||
type: "document",
|
||||
item: () => ({
|
||||
...node,
|
||||
depth,
|
||||
icon: icon ? <Icon value={icon} color={color} /> : undefined,
|
||||
active: isActiveDocument,
|
||||
collectionId: collection?.id || "",
|
||||
}),
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => can.move || can.archive || can.delete,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, [preview]);
|
||||
const [{ isDragging }, drag] = useDragDocument(node, depth, document);
|
||||
|
||||
const hoverExpanding = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
@@ -196,10 +177,11 @@ function InnerDocumentLink(
|
||||
setExpanded(true);
|
||||
},
|
||||
canDrop: (item, monitor) =>
|
||||
!isDraft &&
|
||||
!!pathToNode &&
|
||||
!pathToNode.includes(monitor.getItem<DragObject>().id) &&
|
||||
item.id !== node.id,
|
||||
item.id !== node.id &&
|
||||
policies.abilities(node.id).update &&
|
||||
policies.abilities(item.id).move,
|
||||
hover: (_item, monitor) => {
|
||||
// Enables expansion of document children when hovering over the document
|
||||
// for more than half a second.
|
||||
@@ -234,47 +216,26 @@ function InnerDocumentLink(
|
||||
});
|
||||
|
||||
// Drop to reorder
|
||||
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
|
||||
accept: "document",
|
||||
drop: (item: DragObject) => {
|
||||
if (!manualSort) {
|
||||
toast.message(
|
||||
t(
|
||||
"You can't reorder documents in an alphabetically sorted collection"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] =
|
||||
useDropToReorderDocument(node, collection, (item) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
if (item.id === node.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
void documents.move({
|
||||
return {
|
||||
documentId: item.id,
|
||||
collectionId: collection.id,
|
||||
parentDocumentId: node.id,
|
||||
index: 0,
|
||||
});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
void documents.move({
|
||||
return {
|
||||
documentId: item.id,
|
||||
collectionId: collection.id,
|
||||
parentDocumentId: parentId,
|
||||
index: index + 1,
|
||||
});
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: monitor.isOver(),
|
||||
isDraggingAnyDocument: monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const nodeChildren = React.useMemo(() => {
|
||||
const insertDraftDocument =
|
||||
@@ -407,7 +368,7 @@ function InnerDocumentLink(
|
||||
</DropToImport>
|
||||
</div>
|
||||
</Draggable>
|
||||
{isDraggingAnyDocument && manualSort && (
|
||||
{isDraggingAnyDocument && collection?.isManualSort && (
|
||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||
)}
|
||||
</Relative>
|
||||
|
||||
@@ -7,7 +7,6 @@ import styled from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { useLocationState } from "../hooks/useLocationState";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
@@ -32,12 +31,11 @@ function DraggableCollectionLink({
|
||||
}: Props) {
|
||||
const locationSidebarContext = useLocationState();
|
||||
const sidebarContext = useSidebarContext();
|
||||
const { ui, collections } = useStores();
|
||||
const { ui, policies, collections } = useStores();
|
||||
const [expanded, setExpanded] = React.useState(
|
||||
collection.id === ui.activeCollectionId &&
|
||||
sidebarContext === locationSidebarContext
|
||||
);
|
||||
const can = usePolicy(collection);
|
||||
const belowCollectionIndex = belowCollection ? belowCollection.index : null;
|
||||
|
||||
// Drop to reorder collection
|
||||
@@ -54,7 +52,8 @@ function DraggableCollectionLink({
|
||||
},
|
||||
canDrop: (item) =>
|
||||
collection.id !== item.id &&
|
||||
(!belowCollection || item.id !== belowCollection.id),
|
||||
(!belowCollection || item.id !== belowCollection.id) &&
|
||||
policies.abilities(item.id)?.move,
|
||||
collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({
|
||||
isCollectionDropping: monitor.isOver(),
|
||||
isDraggingAnyCollection: monitor.canDrop(),
|
||||
@@ -72,7 +71,6 @@ function DraggableCollectionLink({
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => can.move,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -34,8 +34,8 @@ function SharedWithMe() {
|
||||
});
|
||||
|
||||
// Drop to reorder document
|
||||
const [reorderMonitor, dropToReorderRef] = useDropToReorderUserMembership(
|
||||
() => fractionalIndex(null, user.documentMemberships[0].index)
|
||||
const [reorderProps, dropToReorderRef] = useDropToReorderUserMembership(() =>
|
||||
fractionalIndex(null, user.documentMemberships[0].index)
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -59,9 +59,9 @@ function SharedWithMe() {
|
||||
<GroupLink key={group.id} group={group} />
|
||||
))}
|
||||
<Relative>
|
||||
{reorderMonitor.isDragging && (
|
||||
{reorderProps.isDragging && (
|
||||
<DropCursor
|
||||
isActiveDrop={reorderMonitor.isOverCursor}
|
||||
isActiveDrop={reorderProps.isOverCursor}
|
||||
innerRef={dropToReorderRef}
|
||||
position="top"
|
||||
/>
|
||||
|
||||
@@ -88,7 +88,7 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) {
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const [reorderMonitor, dropToReorderRef] =
|
||||
const [reorderProps, dropToReorderRef] =
|
||||
useDropToReorderUserMembership(getIndex);
|
||||
|
||||
const displayChildDocuments = expanded && !isDragging;
|
||||
@@ -168,9 +168,9 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) {
|
||||
/>
|
||||
))}
|
||||
</Folder>
|
||||
{reorderMonitor.isDragging && (
|
||||
{reorderProps.isDragging && (
|
||||
<DropCursor
|
||||
isActiveDrop={reorderMonitor.isOverCursor}
|
||||
isActiveDrop={reorderProps.isOverCursor}
|
||||
innerRef={dropToReorderRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,6 @@ import NavLink, { Props as NavLinkProps } from "./NavLink";
|
||||
|
||||
export type DragObject = NavigationNode & {
|
||||
depth: number;
|
||||
active: boolean;
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ function Starred() {
|
||||
const { loading, next, end, error, page } = usePaginatedRequest<Star>(
|
||||
stars.fetchPage
|
||||
);
|
||||
const [reorderStarMonitor, dropToReorder] = useDropToReorderStar();
|
||||
const [createStarMonitor, dropToStarRef] = useDropToCreateStar();
|
||||
const [reorderStarProps, dropToReorder] = useDropToReorderStar();
|
||||
const [createStarProps, dropToStarRef] = useDropToCreateStar();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (error) {
|
||||
@@ -43,16 +43,16 @@ function Starred() {
|
||||
<Flex column>
|
||||
<Header id="starred" title={t("Starred")}>
|
||||
<Relative>
|
||||
{reorderStarMonitor.isDragging && (
|
||||
{reorderStarProps.isDragging && (
|
||||
<DropCursor
|
||||
isActiveDrop={reorderStarMonitor.isOverCursor}
|
||||
isActiveDrop={reorderStarProps.isOverCursor}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
{createStarMonitor.isDragging && (
|
||||
{createStarProps.isDragging && (
|
||||
<DropCursor
|
||||
isActiveDrop={createStarMonitor.isOverCursor}
|
||||
isActiveDrop={createStarProps.isOverCursor}
|
||||
innerRef={dropToStarRef}
|
||||
position="top"
|
||||
/>
|
||||
|
||||
@@ -84,22 +84,22 @@ function StarredLink({ star }: Props) {
|
||||
<StarredIcon color={theme.yellow} />
|
||||
);
|
||||
const [{ isDragging }, draggableRef] = useDragStar(star);
|
||||
const [reorderStarMonitor, dropToReorderRef] = useDropToReorderStar(getIndex);
|
||||
const [createStarMonitor, dropToStarRef] = useDropToCreateStar(getIndex);
|
||||
const [reorderStarProps, dropToReorderRef] = useDropToReorderStar(getIndex);
|
||||
const [createStarProps, dropToStarRef] = useDropToCreateStar(getIndex);
|
||||
|
||||
const displayChildDocuments = expanded && !isDragging;
|
||||
|
||||
const cursor = (
|
||||
<>
|
||||
{reorderStarMonitor.isDragging && (
|
||||
{reorderStarProps.isDragging && (
|
||||
<DropCursor
|
||||
isActiveDrop={reorderStarMonitor.isOverCursor}
|
||||
isActiveDrop={reorderStarProps.isOverCursor}
|
||||
innerRef={dropToReorderRef}
|
||||
/>
|
||||
)}
|
||||
{createStarMonitor.isDragging && (
|
||||
{createStarProps.isDragging && (
|
||||
<DropCursor
|
||||
isActiveDrop={createStarMonitor.isOverCursor}
|
||||
isActiveDrop={createStarProps.isOverCursor}
|
||||
innerRef={dropToStarRef}
|
||||
/>
|
||||
)}
|
||||
@@ -183,7 +183,7 @@ function StarredLink({ star }: Props) {
|
||||
expanded={isDragging ? undefined : displayChildDocuments}
|
||||
activeDocument={documents.active}
|
||||
onDisclosureClick={handleDisclosureClick}
|
||||
isDraggingAnyCollection={reorderStarMonitor.isDragging}
|
||||
isDraggingAnyCollection={reorderStarProps.isDragging}
|
||||
/>
|
||||
</Draggable>
|
||||
<SidebarContext.Provider value={collection.id}>
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentDelete from "~/scenes/DocumentDelete";
|
||||
import Modal from "~/components/Modal";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { trashPath } from "~/utils/routeHelpers";
|
||||
import SidebarLink, { DragObject } from "./SidebarLink";
|
||||
|
||||
function TrashLink() {
|
||||
const { policies, documents } = useStores();
|
||||
const { policies, dialogs, documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const [document, setDocument] = useState<Document>();
|
||||
|
||||
const [{ isDocumentDropping }, dropToTrashDocument] = useDrop({
|
||||
const [{ isDocumentDropping }, dropToTrashRef] = useDrop({
|
||||
accept: "document",
|
||||
drop: (item: DragObject) => {
|
||||
const doc = documents.get(item.id);
|
||||
drop: async (item: DragObject) => {
|
||||
const document = documents.get(item.id);
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
// without setTimeout it was not working in firefox v89.0.2-ubuntu
|
||||
// on dropping mouseup is considered as clicking outside the modal, and it immediately closes
|
||||
setTimeout(() => doc && setDocument(doc), 1);
|
||||
dialogs.openModal({
|
||||
title: t("Delete {{ documentName }}", {
|
||||
documentName: document?.noun,
|
||||
}),
|
||||
content: (
|
||||
<DocumentDelete
|
||||
document={document}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
canDrop: (item) => policies.abilities(item.id).delete,
|
||||
collect: (monitor) => ({
|
||||
@@ -32,32 +39,16 @@ function TrashLink() {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={dropToTrashDocument}>
|
||||
<SidebarLink
|
||||
to={trashPath()}
|
||||
icon={<TrashIcon open={isDocumentDropping} />}
|
||||
exact={false}
|
||||
label={t("Trash")}
|
||||
active={documents.active?.isDeleted}
|
||||
isActiveDrop={isDocumentDropping}
|
||||
/>
|
||||
</div>
|
||||
{document && (
|
||||
<Modal
|
||||
title={t("Delete {{ documentName }}", {
|
||||
documentName: document.noun,
|
||||
})}
|
||||
onRequestClose={() => setDocument(undefined)}
|
||||
isOpen
|
||||
>
|
||||
<DocumentDelete
|
||||
document={document}
|
||||
onSubmit={() => setDocument(undefined)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
<div ref={dropToTrashRef}>
|
||||
<SidebarLink
|
||||
to={trashPath()}
|
||||
icon={<TrashIcon open={isDocumentDropping} />}
|
||||
exact={false}
|
||||
label={t("Trash")}
|
||||
active={documents.active?.isDeleted}
|
||||
isActiveDrop={isDocumentDropping}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@ import { StarredIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { ConnectDragSource, useDrag, useDrop } from "react-dnd";
|
||||
import { getEmptyImage } from "react-dnd-html5-backend";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useTheme } from "styled-components";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import GroupMembership from "~/models/GroupMembership";
|
||||
import Star from "~/models/Star";
|
||||
import UserMembership from "~/models/UserMembership";
|
||||
import Icon from "~/components/Icon";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { DragObject } from "./SidebarLink";
|
||||
@@ -32,7 +38,6 @@ export function useDragStar(
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => true,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -48,21 +53,41 @@ export function useDragStar(
|
||||
* @param getIndex A function to get the index of the current item where the star should be inserted.
|
||||
*/
|
||||
export function useDropToCreateStar(getIndex?: () => string) {
|
||||
const { documents, stars, collections } = useStores();
|
||||
const accept = [
|
||||
"document",
|
||||
"collection",
|
||||
"userMembership",
|
||||
"groupMembership",
|
||||
];
|
||||
const { documents, stars, collections, userMemberships, groupMemberships } =
|
||||
useStores();
|
||||
|
||||
return useDrop({
|
||||
accept: ["document", "collection"],
|
||||
drop: async (item: DragObject) => {
|
||||
const model = documents.get(item.id) ?? collections?.get(item.id);
|
||||
return useDrop<
|
||||
DragObject,
|
||||
Promise<void>,
|
||||
{ isOverCursor: boolean; isDragging: boolean }
|
||||
>({
|
||||
accept,
|
||||
drop: async (item, monitor) => {
|
||||
const type = monitor.getItemType();
|
||||
let model;
|
||||
|
||||
if (type === "collection") {
|
||||
model = collections.get(item.id);
|
||||
} else if (type === "userMembership") {
|
||||
model = userMemberships.get(item.id)?.document;
|
||||
} else if (type === "groupMembership") {
|
||||
model = groupMemberships.get(item.id)?.document;
|
||||
} else {
|
||||
model = documents.get(item.id);
|
||||
}
|
||||
await model?.star(
|
||||
getIndex?.() ?? fractionalIndex(null, stars.orderedData[0].index)
|
||||
);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverCursor: !!monitor.isOver(),
|
||||
isDragging: ["document", "collection"].includes(
|
||||
String(monitor.getItemType())
|
||||
),
|
||||
isDragging: accept.includes(String(monitor.getItemType())),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -75,9 +100,13 @@ export function useDropToCreateStar(getIndex?: () => string) {
|
||||
export function useDropToReorderStar(getIndex?: () => string) {
|
||||
const { stars } = useStores();
|
||||
|
||||
return useDrop({
|
||||
return useDrop<
|
||||
DragObject,
|
||||
Promise<void>,
|
||||
{ isOverCursor: boolean; isDragging: boolean }
|
||||
>({
|
||||
accept: "star",
|
||||
drop: async (item: DragObject) => {
|
||||
drop: async (item) => {
|
||||
const star = stars.get(item.id);
|
||||
void star?.save({
|
||||
index:
|
||||
@@ -92,34 +121,138 @@ export function useDropToReorderStar(getIndex?: () => string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for shared logic that allows dragging user memberships to reorder
|
||||
* Hook for shared logic that allows dragging documents.
|
||||
*
|
||||
* @param membership The UserMembership or GroupMembership model to drag.
|
||||
* @param node The NavigationNode model to drag.
|
||||
* @param depth The depth of the node in the sidebar.
|
||||
* @param document The related Document model.
|
||||
*/
|
||||
export function useDragMembership(
|
||||
membership: UserMembership | GroupMembership
|
||||
): [{ isDragging: boolean }, ConnectDragSource] {
|
||||
const id = membership.id;
|
||||
const { label: title, icon } = useSidebarLabelAndIcon(membership);
|
||||
export function useDragDocument(
|
||||
node: NavigationNode,
|
||||
depth: number,
|
||||
document?: Document
|
||||
) {
|
||||
const icon = document?.icon || node.icon || node.emoji;
|
||||
const color = document?.color || node.color;
|
||||
|
||||
const [{ isDragging }, draggableRef, preview] = useDrag({
|
||||
type: "userMembership",
|
||||
item: () => ({
|
||||
id,
|
||||
title,
|
||||
icon,
|
||||
}),
|
||||
const [{ isDragging }, draggableRef, preview] = useDrag<
|
||||
DragObject,
|
||||
Promise<void>,
|
||||
{ isDragging: boolean }
|
||||
>({
|
||||
type: "document",
|
||||
item: () =>
|
||||
({
|
||||
...node,
|
||||
depth,
|
||||
icon: icon ? <Icon value={icon} color={color} /> : undefined,
|
||||
collectionId: document?.collectionId || "",
|
||||
} as DragObject),
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => membership instanceof UserMembership,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, [preview]);
|
||||
|
||||
return [{ isDragging }, draggableRef];
|
||||
return [{ isDragging }, draggableRef] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for shared logic that allows dropping documents to reorder
|
||||
*
|
||||
* @param node The NavigationNode model to drop.
|
||||
* @param collection The related Collection model, if published
|
||||
* @param getMoveParams A function to get the move parameters for the document.
|
||||
*/
|
||||
export function useDropToReorderDocument(
|
||||
node: NavigationNode,
|
||||
collection: Collection | undefined,
|
||||
getMoveParams: (item: DragObject) =>
|
||||
| undefined
|
||||
| {
|
||||
documentId: string;
|
||||
collectionId: string;
|
||||
parentDocumentId: string | undefined;
|
||||
index: number;
|
||||
}
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { documents, policies } = useStores();
|
||||
|
||||
return useDrop<
|
||||
DragObject,
|
||||
Promise<void>,
|
||||
{ isOverReorder: boolean; isDraggingAnyDocument: boolean }
|
||||
>({
|
||||
accept: "document",
|
||||
canDrop: (item: DragObject) => {
|
||||
if (item.id === node.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return policies.abilities(item.id)?.move;
|
||||
},
|
||||
drop: async (item) => {
|
||||
if (!collection?.isManualSort && item.collectionId === collection?.id) {
|
||||
toast.message(
|
||||
t(
|
||||
"You can't reorder documents in an alphabetically sorted collection"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = getMoveParams(item);
|
||||
if (params) {
|
||||
void documents.move(params);
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: monitor.isOver(),
|
||||
isDraggingAnyDocument: monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for shared logic that allows dragging user memberships.
|
||||
*
|
||||
* @param membership The UserMembership or GroupMembership model to drag.
|
||||
*/
|
||||
export function useDragMembership(
|
||||
membership: UserMembership | GroupMembership
|
||||
) {
|
||||
const id = membership.id;
|
||||
const { label: title, icon } = useSidebarLabelAndIcon(membership);
|
||||
|
||||
const [{ isDragging }, draggableRef, preview] = useDrag<
|
||||
DragObject,
|
||||
Promise<void>,
|
||||
{ isDragging: boolean }
|
||||
>({
|
||||
type:
|
||||
membership instanceof UserMembership
|
||||
? "userMembership"
|
||||
: "groupMembership",
|
||||
item: () =>
|
||||
({
|
||||
id,
|
||||
title,
|
||||
icon,
|
||||
} as DragObject),
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, [preview]);
|
||||
|
||||
return [{ isDragging }, draggableRef] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,9 +264,13 @@ export function useDropToReorderUserMembership(getIndex?: () => string) {
|
||||
const { userMemberships } = useStores();
|
||||
const user = useCurrentUser();
|
||||
|
||||
return useDrop({
|
||||
return useDrop<
|
||||
DragObject,
|
||||
Promise<void>,
|
||||
{ isOverCursor: boolean; isDragging: boolean }
|
||||
>({
|
||||
accept: "userMembership",
|
||||
drop: async (item: DragObject) => {
|
||||
drop: async (item) => {
|
||||
const userMembership = userMemberships.get(item.id);
|
||||
void userMembership?.save({
|
||||
index:
|
||||
|
||||
@@ -154,6 +154,11 @@ export default class Collection extends ParanoidModel {
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
get isManualSort(): boolean {
|
||||
return this.sort.field === "index";
|
||||
}
|
||||
|
||||
@computed
|
||||
get sortedDocuments(): NavigationNode[] | undefined {
|
||||
if (!this.documents) {
|
||||
|
||||
@@ -228,6 +228,8 @@ export type TeamPreferences = {
|
||||
export enum NavigationNodeType {
|
||||
Collection = "collection",
|
||||
Document = "document",
|
||||
UserMembership = "userMembership",
|
||||
GroupMembership = "groupMembership",
|
||||
}
|
||||
|
||||
export type NavigationNode = {
|
||||
|
||||
Reference in New Issue
Block a user