Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop/src/app/utils/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const REVEAL_NOTE_IN_TREE_EVENT = "neverwrite:reveal-note-in-tree";
export const CLEAR_FILE_TREE_SELECTION_EVENT =
"neverwrite:clear-file-tree-selection";

export function revealNoteInTree(noteId: string) {
window.dispatchEvent(
Expand All @@ -7,3 +9,7 @@ export function revealNoteInTree(noteId: string) {
}),
);
}

export function clearFileTreeSelection() {
window.dispatchEvent(new CustomEvent(CLEAR_FILE_TREE_SELECTION_EVENT));
}
28 changes: 28 additions & 0 deletions apps/desktop/src/features/editor/MultiPaneWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "../../test/test-utils";
import { publishWindowTabDropZone } from "../../app/detachedWindows";
import { useEditorStore } from "../../app/store/editorStore";
import { CLEAR_FILE_TREE_SELECTION_EVENT } from "../../app/utils/navigation";
import {
createInitialLayout,
splitPane,
Expand Down Expand Up @@ -223,6 +224,33 @@ describe("MultiPaneWorkspace", () => {
expect(useEditorStore.getState().focusedPaneId).toBe("secondary");
});

it("requests file tree selection cleanup when a pane is clicked", () => {
const events: Event[] = [];
const handleClearSelection = (event: Event) => events.push(event);
window.addEventListener(
CLEAR_FILE_TREE_SELECTION_EVENT,
handleClearSelection,
);

try {
renderComponent(<MultiPaneWorkspace />);

const targetPane = screen
.getByTestId("pane-content-secondary")
.closest('[data-editor-pane-id="secondary"]');
expect(targetPane).not.toBeNull();

fireEvent.pointerDown(targetPane!, { pointerId: 1, button: 0 });

expect(events).toHaveLength(1);
} finally {
window.removeEventListener(
CLEAR_FILE_TREE_SELECTION_EVENT,
handleClearSelection,
);
}
});

it("opens a dragged vault file in the pane under the pointer", async () => {
setVaultEntries([
{
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/features/editor/MultiPaneWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getCurrentWindowLabel,
publishWindowTabDropZone,
} from "../../app/detachedWindows";
import { clearFileTreeSelection } from "../../app/utils/navigation";
import {
selectFocusedPaneId,
selectLeafPaneIds,
Expand Down Expand Up @@ -382,6 +383,10 @@ export function MultiPaneWorkspace() {
[focusPane],
);

const handlePanePointerDown = useCallback(() => {
clearFileTreeSelection();
}, []);

const handleResizeSplit = useCallback(
(splitId: string, sizes: readonly number[]) => {
resizePaneSplit(splitId, sizes);
Expand Down Expand Up @@ -578,6 +583,7 @@ export function MultiPaneWorkspace() {
node={layoutTree}
focusedPaneId={focusedPaneId}
externalFileDropPaneId={externalFileDropPaneId}
onPanePointerDown={handlePanePointerDown}
onPaneFocus={handlePaneFocus}
onResizeSplit={handleResizeSplit}
/>
Expand Down
11 changes: 10 additions & 1 deletion apps/desktop/src/features/editor/WorkspaceSplitContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface WorkspaceSplitContainerProps {
node: WorkspaceLayoutNode;
focusedPaneId: string | null;
externalFileDropPaneId: string | null;
onPanePointerDown: () => void;
onPaneFocus: (paneId: string) => void;
onResizeSplit: (splitId: string, sizes: readonly number[]) => void;
}
Expand Down Expand Up @@ -94,11 +95,13 @@ const WorkspacePane = memo(function WorkspacePane({
paneId,
isFocused,
isExternalFileDropActive,
onPanePointerDown,
onPaneFocus,
}: {
paneId: string;
isFocused: boolean;
isExternalFileDropActive: boolean;
onPanePointerDown: () => void;
onPaneFocus: (paneId: string) => void;
}) {
return (
Expand All @@ -116,7 +119,10 @@ const WorkspacePane = memo(function WorkspacePane({
? "inset 0 1px 0 color-mix(in srgb, var(--accent) 16%, transparent)"
: "none",
}}
onPointerDownCapture={() => onPaneFocus(paneId)}
onPointerDownCapture={() => {
onPanePointerDown();
onPaneFocus(paneId);
}}
onFocusCapture={() => onPaneFocus(paneId)}
data-editor-pane-id={paneId}
data-editor-pane-focused={isFocused || undefined}
Expand Down Expand Up @@ -149,6 +155,7 @@ export function WorkspaceSplitContainer({
node,
focusedPaneId,
externalFileDropPaneId,
onPanePointerDown,
onPaneFocus,
onResizeSplit,
}: WorkspaceSplitContainerProps) {
Expand Down Expand Up @@ -281,6 +288,7 @@ export function WorkspaceSplitContainer({
isExternalFileDropActive={
node.paneId === externalFileDropPaneId
}
onPanePointerDown={onPanePointerDown}
onPaneFocus={onPaneFocus}
/>
);
Expand Down Expand Up @@ -320,6 +328,7 @@ export function WorkspaceSplitContainer({
node={child}
focusedPaneId={focusedPaneId}
externalFileDropPaneId={externalFileDropPaneId}
onPanePointerDown={onPanePointerDown}
onPaneFocus={onPaneFocus}
onResizeSplit={onResizeSplit}
/>
Expand Down
149 changes: 149 additions & 0 deletions apps/desktop/src/features/vault/FileTree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { useBookmarkStore } from "../../app/store/bookmarkStore";
import { useSettingsStore } from "../../app/store/settingsStore";
import { useVaultStore } from "../../app/store/vaultStore";
import { clearFileTreeSelection } from "../../app/utils/navigation";
import { safeStorageSetItem } from "../../app/utils/safeStorage";
import {
buildVaultFileEntry as buildFileEntry,
Expand Down Expand Up @@ -729,6 +730,154 @@ describe("FileTree", () => {
).toBeInTheDocument();
});

it("clears explicit multi-selection when another pane requests it", async () => {
const user = userEvent.setup();
setVaultNotes([
{
id: "notes/alpha",
path: "/vault/notes/alpha.md",
title: "Alpha",
modified_at: 1,
created_at: 1,
},
{
id: "notes/beta",
path: "/vault/notes/beta.md",
title: "Beta",
modified_at: 1,
created_at: 1,
},
]);

renderComponent(<FileTree />);
await expandFolder(user, "notes");

fireEvent.click(getNoteRow("Alpha"), { metaKey: true });
fireEvent.click(getNoteRow("Beta"), { metaKey: true });
expect(getNoteRow("Alpha")).toHaveAttribute("data-selected", "true");
expect(getNoteRow("Beta")).toHaveAttribute("data-selected", "true");

act(() => clearFileTreeSelection());

expect(getNoteRow("Alpha")).toHaveAttribute("data-selected", "false");
expect(getNoteRow("Beta")).toHaveAttribute("data-selected", "false");
});

it("clears the active tree cursor highlight when another pane requests it", async () => {
const user = userEvent.setup();
setVaultNotes([
{
id: "notes/alpha",
path: "/vault/notes/alpha.md",
title: "Alpha",
modified_at: 1,
created_at: 1,
},
{
id: "notes/beta",
path: "/vault/notes/beta.md",
title: "Beta",
modified_at: 1,
created_at: 1,
},
]);
setEditorTabs(
[
{
id: "tab-alpha",
noteId: "notes/alpha",
title: "Alpha",
content: "Alpha",
},
],
"tab-alpha",
);

renderComponent(<FileTree />);
await expandFolder(user, "notes");

await waitFor(() => {
expect(getNoteRow("Alpha")).toHaveAttribute(
"data-keyboard-focus",
"true",
);
});

act(() => clearFileTreeSelection());

expect(getNoteRow("Alpha")).toHaveAttribute(
"data-keyboard-focus",
"false",
);

await user.click(getNoteRow("Beta"));

expect(getNoteRow("Beta")).toHaveAttribute(
"data-keyboard-focus",
"true",
);
});

it("uses the active tree cursor as the shift-click range anchor", async () => {
const user = userEvent.setup();
setVaultNotes([
{
id: "notes/alpha",
path: "/vault/notes/alpha.md",
title: "Alpha",
modified_at: 1,
created_at: 1,
},
{
id: "notes/beta",
path: "/vault/notes/beta.md",
title: "Beta",
modified_at: 1,
created_at: 1,
},
{
id: "notes/gamma",
path: "/vault/notes/gamma.md",
title: "Gamma",
modified_at: 1,
created_at: 1,
},
]);
setEditorTabs(
[
{
id: "tab-alpha",
noteId: "notes/alpha",
title: "Alpha",
content: "Alpha",
},
],
"tab-alpha",
);

renderComponent(<FileTree />);
await expandFolder(user, "notes");

await waitFor(() => {
expect(getNoteRow("Alpha")).toHaveAttribute(
"data-keyboard-focus",
"true",
);
});

fireEvent.click(getNoteRow("Gamma"), { shiftKey: true });

expect(getNoteRow("Alpha")).toHaveAttribute("data-selected", "true");
expect(getNoteRow("Beta")).toHaveAttribute("data-selected", "true");
expect(getNoteRow("Gamma")).toHaveAttribute("data-selected", "true");

fireEvent.click(getNoteRow("Beta"), { shiftKey: true });

expect(getNoteRow("Alpha")).toHaveAttribute("data-selected", "true");
expect(getNoteRow("Beta")).toHaveAttribute("data-selected", "true");
expect(getNoteRow("Gamma")).toHaveAttribute("data-selected", "false");
});

it("deletes all selected notes from the context menu", async () => {
const user = userEvent.setup();
const deleteNote = vi.fn().mockResolvedValue(undefined);
Expand Down
Loading
Loading