feat: Allow moving draft documents (#4652)

* feat: Allow moving draft documents

* Allow drag-n-drop move of draft documents

* fix: Allow moving draft without a collection

* fix: Allow moving draft without a collection
This commit is contained in:
Tom Moor
2023-01-06 19:31:06 -08:00
committed by GitHub
parent 9f825b9adf
commit e67ac1215a
8 changed files with 61 additions and 63 deletions

View File

@@ -77,8 +77,9 @@ const DocumentBreadcrumb: React.FC<Props> = ({
} }
const path = React.useMemo( const path = React.useMemo(
() => collection?.pathToDocument?.(document.id).slice(0, -1) || [], () => collection?.pathToDocument(document.id).slice(0, -1) || [],
[collection, document] // eslint-disable-next-line react-hooks/exhaustive-deps
[collection, document, document.collectionId, document.parentDocumentId]
); );
const items = React.useMemo(() => { const items = React.useMemo(() => {

View File

@@ -27,7 +27,7 @@ import { useStarredContext } from "./StarredContext";
type Props = { type Props = {
collection: Collection; collection: Collection;
expanded?: boolean; expanded?: boolean;
onDisclosureClick: (ev: React.MouseEvent<HTMLButtonElement>) => void; onDisclosureClick: (ev?: React.MouseEvent<HTMLButtonElement>) => void;
activeDocument: Document | undefined; activeDocument: Document | undefined;
isDraggingAnyCollection?: boolean; isDraggingAnyCollection?: boolean;
}; };
@@ -62,7 +62,7 @@ const CollectionLink: React.FC<Props> = ({
// Drop to re-parent document // Drop to re-parent document
const [{ isOver, canDrop }, drop] = useDrop({ const [{ isOver, canDrop }, drop] = useDrop({
accept: "document", accept: "document",
drop: (item: DragObject, monitor) => { drop: async (item: DragObject, monitor) => {
const { id, collectionId } = item; const { id, collectionId } = item;
if (monitor.didDrop()) { if (monitor.didDrop()) {
return; return;
@@ -81,7 +81,8 @@ const CollectionLink: React.FC<Props> = ({
if ( if (
prevCollection && prevCollection &&
prevCollection.permission === null && prevCollection.permission === null &&
prevCollection.permission !== collection.permission prevCollection.permission !== collection.permission &&
!document?.isDraft
) { ) {
itemRef.current = item; itemRef.current = item;
@@ -97,7 +98,11 @@ const CollectionLink: React.FC<Props> = ({
), ),
}); });
} else { } else {
documents.move(id, collection.id); await documents.move(id, collection.id);
if (!expanded) {
onDisclosureClick();
}
} }
}, },
canDrop: () => canUpdate, canDrop: () => canUpdate,

View File

@@ -105,8 +105,7 @@ function InnerDocumentLink(
const handleDisclosureClick = React.useCallback( const handleDisclosureClick = React.useCallback(
(ev) => { (ev) => {
ev.preventDefault(); ev?.preventDefault();
ev.stopPropagation();
setExpanded(!expanded); setExpanded(!expanded);
}, },
[expanded] [expanded]
@@ -150,14 +149,10 @@ function InnerDocumentLink(
collect: (monitor) => ({ collect: (monitor) => ({
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),
}), }),
canDrag: () => { canDrag: () =>
return ( policies.abilities(node.id).move ||
!isDraft && policies.abilities(node.id).archive ||
(policies.abilities(node.id).move || policies.abilities(node.id).delete,
policies.abilities(node.id).archive ||
policies.abilities(node.id).delete)
);
},
}); });
const hoverExpanding = React.useRef<ReturnType<typeof setTimeout>>(); const hoverExpanding = React.useRef<ReturnType<typeof setTimeout>>();
@@ -174,19 +169,18 @@ function InnerDocumentLink(
// Drop to re-parent // Drop to re-parent
const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({ const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({
accept: "document", accept: "document",
drop: (item: DragObject, monitor) => { drop: async (item: DragObject, monitor) => {
if (monitor.didDrop()) { if (monitor.didDrop()) {
return; return;
} }
if (!collection) { if (!collection) {
return; return;
} }
documents.move(item.id, collection.id, node.id); await documents.move(item.id, collection.id, node.id);
setExpanded(true);
}, },
canDrop: (_item, monitor) => canDrop: (_item, monitor) =>
!isDraft && !!pathToNode && !pathToNode.includes(monitor.getItem<DragObject>().id),
!!pathToNode &&
!pathToNode.includes(monitor.getItem<DragObject>().id),
hover: (_item, monitor) => { hover: (_item, monitor) => {
// Enables expansion of document children when hovering over the document // Enables expansion of document children when hovering over the document
// for more than half a second. // for more than half a second.

View File

@@ -91,7 +91,7 @@ function DraggableCollectionLink({
}, [collection.id, ui.activeCollectionId, locationStateStarred]); }, [collection.id, ui.activeCollectionId, locationStateStarred]);
const handleDisclosureClick = React.useCallback((ev) => { const handleDisclosureClick = React.useCallback((ev) => {
ev.preventDefault(); ev?.preventDefault();
setExpanded((e) => !e); setExpanded((e) => !e);
}, []); }, []);

View File

@@ -171,8 +171,11 @@ export default class Collection extends ParanoidModel {
} }
pathToDocument(documentId: string) { pathToDocument(documentId: string) {
let path: NavigationNode[] | undefined; let path: NavigationNode[] | undefined = [];
const document = this.store.rootStore.documents.get(documentId); const document = this.store.rootStore.documents.get(documentId);
if (!document) {
return path;
}
const travelNodes = ( const travelNodes = (
nodes: NavigationNode[], nodes: NavigationNode[],
@@ -187,8 +190,8 @@ export default class Collection extends ParanoidModel {
} }
if ( if (
document?.parentDocumentId && document.parentDocumentId &&
node?.id === document?.parentDocumentId node.id === document.parentDocumentId
) { ) {
path = [...newPath, document.asNavigationNode]; path = [...newPath, document.asNavigationNode];
return; return;
@@ -199,10 +202,10 @@ export default class Collection extends ParanoidModel {
}; };
if (this.documents) { if (this.documents) {
travelNodes(this.documents, []); travelNodes(this.documents, path);
} }
return path || []; return path;
} }
@action @action

View File

@@ -67,43 +67,45 @@ async function documentMover({
invariant(newCollection, "collection should exist"); invariant(newCollection, "collection should exist");
// Remove the document from the current collection if (document.publishedAt) {
const response = await collection?.removeDocumentInStructure(document, { // Remove the document from the current collection
transaction, const response = await collection?.removeDocumentInStructure(document, {
}); transaction,
});
const documentJson = response?.[0]; const documentJson = response?.[0];
const fromIndex = response?.[1] || 0; const fromIndex = response?.[1] || 0;
if (!documentJson) { if (!documentJson) {
throw ValidationError("The document was not found in the collection"); throw ValidationError("The document was not found in the collection");
}
// if we're reordering from within the same parent
// the original and destination collection are the same,
// so when the initial item is removed above, the list will reduce by 1.
// We need to compensate for this when reordering
const toIndex =
index !== undefined &&
document.parentDocumentId === parentDocumentId &&
document.collectionId === collectionId &&
fromIndex < index
? index - 1
: index;
// Add the document and it's tree to the new collection
await newCollection.addDocumentToStructure(document, toIndex, {
documentJson,
transaction,
});
} }
// if we're reordering from within the same parent
// the original and destination collection are the same,
// so when the initial item is removed above, the list will reduce by 1.
// We need to compensate for this when reordering
const toIndex =
index !== undefined &&
document.parentDocumentId === parentDocumentId &&
document.collectionId === collectionId &&
fromIndex < index
? index - 1
: index;
// Update the properties on the document record // Update the properties on the document record
document.collectionId = collectionId; document.collectionId = collectionId;
document.parentDocumentId = parentDocumentId; document.parentDocumentId = parentDocumentId;
document.lastModifiedById = user.id; document.lastModifiedById = user.id;
document.updatedBy = user; document.updatedBy = user;
// Add the document and it's tree to the new collection if (collection && document.publishedAt) {
await newCollection.addDocumentToStructure(document, toIndex, {
documentJson,
transaction,
});
if (collection) {
result.collections.push(collection); result.collections.push(collection);
} }

View File

@@ -128,7 +128,7 @@ describe("no collection", () => {
expect(abilities.createChildDocument).toEqual(false); expect(abilities.createChildDocument).toEqual(false);
expect(abilities.delete).toEqual(true); expect(abilities.delete).toEqual(true);
expect(abilities.download).toEqual(true); expect(abilities.download).toEqual(true);
expect(abilities.move).toEqual(false); expect(abilities.move).toEqual(true);
expect(abilities.permanentDelete).toEqual(false); expect(abilities.permanentDelete).toEqual(false);
expect(abilities.pin).toEqual(false); expect(abilities.pin).toEqual(false);
expect(abilities.pinToHome).toEqual(false); expect(abilities.pinToHome).toEqual(false);

View File

@@ -174,14 +174,7 @@ allow(User, "move", Document, (user, document) => {
if (document.deletedAt) { if (document.deletedAt) {
return false; return false;
} }
if (!document.publishedAt) { if (document.collection && cannot(user, "update", document.collection)) {
return false;
}
invariant(
document.collection,
"collection is missing, did you forget to include in the query scope?"
);
if (cannot(user, "update", document.collection)) {
return false; return false;
} }
return user.teamId === document.teamId; return user.teamId === document.teamId;