Files
outline/app/components/DocumentExplorer/DocumentMove.tsx
T
Copilot dd2e8f258d Handle double-click submission in DocumentExplorer actions (#12417)
* Initial plan

* Support double-click submit in document explorer

* Remove test

* Fix double-click submit in document explorer

Single click now sets the selection instead of toggling it, so the two
clicks preceding a dblclick no longer flicker the selection on/off.
Submit handlers accept the node directly to avoid the stale-state race
across the click sequence, and button onClick handlers are wrapped so
the synthetic MouseEvent isn't passed in as the path argument.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* PR feedback

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:10:25 -04:00

115 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { observer } from "mobx-react";
import { useState, useMemo } from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import type { NavigationNode } from "@shared/types";
import { descendants, flattenTree } from "@shared/utils/tree";
import type Document from "~/models/Document";
import Button from "~/components/Button";
import Text from "~/components/Text";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { FlexContainer, Footer } from "./Components";
import DocumentExplorer from "./DocumentExplorer";
type Props = {
document: Document;
};
function DocumentMove({ document }: Props) {
const { dialogs, policies } = useStores();
const { t } = useTranslation();
const collectionTrees = useCollectionTrees();
const [moving, setMoving] = useState<boolean>(false);
const [selectedPath, selectPath] = useState<NavigationNode | null>(null);
const items = useMemo(() => {
// Collect the IDs of the document itself and all of its descendants so they
// can be excluded from the move targets (moving to self or a descendant
// would create a cycle; moving to the exact same location is a no-op).
const allNodes = collectionTrees.flatMap(flattenTree);
const sourceNode = allNodes.find((node) => node.id === document.id);
const excludedIds = new Set<string>([document.id]);
if (sourceNode) {
descendants(sourceNode).forEach((n) => excludedIds.add(n.id));
}
// Recursively filter out the document itself and its descendants.
// The document's current parent is intentionally kept so that siblings
// remain visible as valid move targets.
const filterSourceDocument = (node: NavigationNode): NavigationNode => ({
...node,
children: node.children
?.filter((c) => !excludedIds.has(c.id))
.map(filterSourceDocument),
});
const nodes = collectionTrees
.map(filterSourceDocument)
// Filter out collections that we don't have permission to create documents in.
.filter((node) =>
node.collectionId
? policies.get(node.collectionId)?.abilities.createDocument
: true
);
return nodes;
}, [policies, collectionTrees, document.id]);
const move = async (path = selectedPath) => {
if (!path) {
toast.message(t("Select a location to move"));
return;
}
try {
setMoving(true);
const { type, id: parentDocumentId } = path;
const collectionId = path.collectionId as string;
if (type === "document") {
await document.move({ collectionId, parentDocumentId });
} else {
await document.move({ collectionId });
}
toast.success(t("Document moved"));
dialogs.closeAllModals();
} catch (_err) {
toast.error(t("Couldnt move the document, try again?"));
} finally {
setMoving(false);
}
};
return (
<FlexContainer column>
<DocumentExplorer items={items} onSubmit={move} onSelect={selectPath} />
<Footer justify="space-between" align="center" gap={8}>
<Text ellipsis type="secondary">
{selectedPath ? (
<Trans
defaults="Move to <em>{{ location }}</em>"
values={{
location: selectedPath.title || t("Untitled"),
}}
components={{
em: <strong />,
}}
/>
) : (
t("Select a location to move")
)}
</Text>
<Button disabled={!selectedPath || moving} onClick={() => move()}>
{moving ? `${t("Moving")}` : t("Move")}
</Button>
</Footer>
</FlexContainer>
);
}
export default observer(DocumentMove);