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>
This commit is contained in:
Copilot
2026-05-21 22:10:25 -04:00
committed by GitHub
parent 5309e8bb01
commit dd2e8f258d
7 changed files with 40 additions and 27 deletions
@@ -40,8 +40,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
return nodes;
}, [policies, collectionTrees]);
const copy = async () => {
if (!selectedPath) {
const copy = async (path = selectedPath) => {
if (!path) {
toast.message(t("Select a location to copy"));
return;
}
@@ -52,10 +52,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
publish,
recursive,
title: document.title,
collectionId: selectedPath.collectionId,
...(selectedPath.type === "document"
? { parentDocumentId: selectedPath.id }
: {}),
collectionId: path.collectionId,
...(path.type === "document" ? { parentDocumentId: path.id } : {}),
});
toast.success(t("Document copied"));
@@ -111,7 +109,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
t("Select a location to copy")
)}
</Text>
<Button disabled={!selectedPath || copying} onClick={copy}>
<Button disabled={!selectedPath || copying} onClick={() => copy()}>
{copying ? `${t("Copying")}` : t("Copy")}
</Button>
</Footer>
@@ -31,7 +31,7 @@ import useStores from "~/hooks/useStores";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
onSubmit: () => void;
onSubmit: (item: NavigationNode | null) => void;
/** A side-effect of item selection */
onSelect: (item: NavigationNode | null) => void;
/** Items to be shown in explorer */
@@ -255,6 +255,13 @@ function DocumentExplorer({
}
};
const submitNode = (node: number) => {
const selectedNode = nodes[node];
selectNode(selectedNode);
onSubmit(selectedNode);
};
const ListItem = observer(
({
index,
@@ -311,7 +318,8 @@ function DocumentExplorer({
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
}}
onPointerMove={() => setActiveNode(index)}
onClick={() => toggleSelect(index)}
onClick={() => selectNode(nodes[index])}
onDoubleClick={() => submitNode(index)}
icon={renderedIcon}
title={title}
path={path}
@@ -325,7 +333,8 @@ function DocumentExplorer({
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
}}
onPointerMove={() => setActiveNode(index)}
onClick={() => toggleSelect(index)}
onClick={() => selectNode(nodes[index])}
onDoubleClick={() => submitNode(index)}
onDisclosureClick={(ev) => {
ev.stopPropagation();
toggleCollapse(index);
@@ -387,7 +396,7 @@ function DocumentExplorer({
}
case "Enter": {
if (isModKey(ev)) {
onSubmit();
onSubmit(selectedNode);
} else {
toggleSelect(activeNode);
}
@@ -29,8 +29,10 @@ type Props = {
onDisclosureClick: (ev: React.MouseEvent) => void;
/** Fired on pointer movement over the node; used to update the active highlight. */
onPointerMove: (ev: React.MouseEvent) => void;
/** Fired when the node is clicked to toggle its selection. */
/** Fired when the node is clicked to select it. */
onClick: (ev: React.MouseEvent) => void;
/** Fired when the node is double-clicked to submit the current selection. */
onDoubleClick: (ev: React.MouseEvent) => void;
};
function DocumentExplorerNode(
@@ -46,6 +48,7 @@ function DocumentExplorerNode(
onDisclosureClick,
onPointerMove,
onClick,
onDoubleClick,
}: Props,
ref: React.RefObject<HTMLSpanElement>
) {
@@ -59,6 +62,7 @@ function DocumentExplorerNode(
selected={selected}
active={active}
onClick={onClick}
onDoubleClick={onDoubleClick}
style={style}
onPointerMove={onPointerMove}
role="option"
@@ -17,6 +17,7 @@ type Props = {
onPointerMove: (ev: React.MouseEvent) => void;
onClick: (ev: React.MouseEvent) => void;
onDoubleClick: (ev: React.MouseEvent) => void;
};
function DocumentExplorerSearchResult({
@@ -28,6 +29,7 @@ function DocumentExplorerSearchResult({
path,
onPointerMove,
onClick,
onDoubleClick,
}: Props) {
const { t } = useTranslation();
@@ -36,6 +38,7 @@ function DocumentExplorerSearchResult({
selected={selected}
active={active}
onClick={onClick}
onDoubleClick={onDoubleClick}
style={style}
onPointerMove={onPointerMove}
role="option"
@@ -56,17 +56,17 @@ function DocumentMove({ document }: Props) {
return nodes;
}, [policies, collectionTrees, document.id]);
const move = async () => {
if (!selectedPath) {
const move = async (path = selectedPath) => {
if (!path) {
toast.message(t("Select a location to move"));
return;
}
try {
setMoving(true);
const { type, id: parentDocumentId } = selectedPath;
const { type, id: parentDocumentId } = path;
const collectionId = selectedPath.collectionId as string;
const collectionId = path.collectionId as string;
if (type === "document") {
await document.move({ collectionId, parentDocumentId });
@@ -103,7 +103,7 @@ function DocumentMove({ document }: Props) {
t("Select a location to move")
)}
</Text>
<Button disabled={!selectedPath || moving} onClick={move}>
<Button disabled={!selectedPath || moving} onClick={() => move()}>
{moving ? `${t("Moving")}` : t("Move")}
</Button>
</Footer>
@@ -33,15 +33,14 @@ function TemplateMove({ template }: Props) {
[policies, collectionTrees]
);
const move = async () => {
if (!selectedPath) {
const move = async (path = selectedPath) => {
if (!path) {
toast.message(t("Select a location to move"));
return;
}
try {
const collectionId = (selectedPath.collectionId ??
selectedPath.id) as string;
const collectionId = (path.collectionId ?? path.id) as string;
await template.save({ collectionId });
toast.success(t("Template moved"));
@@ -76,7 +75,7 @@ function TemplateMove({ template }: Props) {
t("Select a location to move")
)}
</Text>
<Button disabled={!selectedPath} onClick={move}>
<Button disabled={!selectedPath} onClick={() => move()}>
{t("Move")}
</Button>
</Footer>
+5 -5
View File
@@ -33,16 +33,16 @@ function DocumentPublish({ document }: Props) {
[policies, collectionTrees]
);
const publish = async () => {
if (!selectedPath) {
const publish = async (path = selectedPath) => {
if (!path) {
toast.message(t("Select a location to publish"));
return;
}
try {
const { type, id: parentDocumentId } = selectedPath;
const { type, id: parentDocumentId } = path;
const collectionId = selectedPath.collectionId as string;
const collectionId = path.collectionId as string;
// Also move it under if selected path corresponds to another doc
if (type === "document") {
@@ -83,7 +83,7 @@ function DocumentPublish({ document }: Props) {
t("Select a location to publish")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={publish}>
<Button disabled={!selectedPath} onClick={() => publish()}>
{t("Publish")}
</Button>
</Footer>