fix(sidebar): preserve order on same-folder sorted drops

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-19 20:25:05 +08:00
1 parent 34a158765e
commit 7a707e77d8
6 files changed
+168 -16

No files matched your search

@@ -25,7 +25,11 @@ describe("sidebar filter guards", () => {
expect(source).toContain("sidebarTableSearchEnabled && !isTreeSearchFiltering.value");
expect(source).toContain("!useVirtualTree.value || isTreeSearchFiltering.value");
expect(source.match(/if \(isRootListPartial\.value\)/g)).toHaveLength(2);
expect(source.match(/:reorder-disabled="isRootListPartial \|\| isConnectionListAlphabeticallySorted"/g)).toHaveLength(2);
// Alphabetical sort only suppresses before/after position reordering (the manual
// order it would act on is invisible while sorted); moving a connection into a
// different group stays available so drag stays usable while sorted (#6669).
expect(source.match(/:reorder-disabled="isRootListPartial"/g)).toHaveLength(2);
expect(source.match(/:move-to-group-only="isConnectionListAlphabeticallySorted"/g)).toHaveLength(2);
expect(source).not.toContain("isFiltering");
});
});
+122 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { connectionIdsInGroups, deleteGroups, findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout";
import { connectionIdsInGroups, deleteGroups, findConnectionGroupPath, reorderEntries } from "@/lib/sidebar/sidebarLayout";
import type { SidebarLayout } from "@/types/database";
const layout: SidebarLayout = {
@@ -24,6 +24,34 @@ const layout: SidebarLayout = {
],
};
const dragLayout: SidebarLayout = {
groups: [
{ id: "alpha", name: "Alpha", collapsed: false },
{ id: "beta", name: "Beta", collapsed: false },
{ id: "nested", name: "Nested", collapsed: false },
],
order: [
{
type: "group",
id: "alpha",
children: [
{ type: "connection", id: "alpha-first" },
{
type: "group",
id: "nested",
children: [{ type: "connection", id: "nested-connection" }],
},
{ type: "connection", id: "alpha-second" },
],
},
{
type: "group",
id: "beta",
children: [{ type: "connection", id: "beta-connection" }],
},
],
};
describe("findConnectionGroupPath", () => {
it("returns every containing group from root to leaf", () => {
expect(findConnectionGroupPath(layout, "nested")).toEqual(["Project", "Staging"]);
@@ -82,3 +110,96 @@ describe("connection group deletion", () => {
});
});
});
describe("sorted sidebar move-only drops", () => {
const preserveSameGroupOrder = { preserveSameGroupOrder: true };
it("keeps a same-folder connection drop from rewriting hidden manual order", () => {
expect(reorderEntries(dragLayout, ["alpha-first"], "alpha", "inside", preserveSameGroupOrder)).toBe(dragLayout);
});
it("keeps a same-folder multi-selection drop from rewriting hidden manual order", () => {
expect(reorderEntries(dragLayout, ["alpha-first", "alpha-second"], "alpha", "inside", preserveSameGroupOrder)).toBe(dragLayout);
});
it("keeps a same-folder group drop from rewriting hidden manual order", () => {
expect(reorderEntries(dragLayout, ["nested"], "alpha", "inside", preserveSameGroupOrder)).toBe(dragLayout);
});
it("moves cross-folder entries while preserving selected entries already in the target folder", () => {
expect(reorderEntries(dragLayout, ["alpha-first", "beta-connection"], "alpha", "inside", preserveSameGroupOrder)).toEqual({
...dragLayout,
order: [
{
type: "group",
id: "alpha",
children: [
{ type: "connection", id: "alpha-first" },
{
type: "group",
id: "nested",
children: [{ type: "connection", id: "nested-connection" }],
},
{ type: "connection", id: "alpha-second" },
{ type: "connection", id: "beta-connection" },
],
},
{ type: "group", id: "beta", children: [] },
],
});
});
it("preserves ordinary same-folder inside reordering outside move-only mode", () => {
expect(reorderEntries(dragLayout, ["alpha-first"], "alpha", "inside")).toEqual({
...dragLayout,
order: [
{
type: "group",
id: "alpha",
children: [
{
type: "group",
id: "nested",
children: [{ type: "connection", id: "nested-connection" }],
},
{ type: "connection", id: "alpha-second" },
{ type: "connection", id: "alpha-first" },
],
},
{
type: "group",
id: "beta",
children: [{ type: "connection", id: "beta-connection" }],
},
],
});
});
it("still moves a group across folders in move-only mode", () => {
expect(reorderEntries(dragLayout, ["nested"], "beta", "inside", preserveSameGroupOrder)).toEqual({
...dragLayout,
order: [
{
type: "group",
id: "alpha",
children: [
{ type: "connection", id: "alpha-first" },
{ type: "connection", id: "alpha-second" },
],
},
{
type: "group",
id: "beta",
children: [
{ type: "connection", id: "beta-connection" },
{
type: "group",
id: "nested",
children: [{ type: "connection", id: "nested-connection" }],
},
],
},
],
});
});
});
@@ -354,6 +354,20 @@ export function moveConnectionToGroup(layout: SidebarLayout, connectionId: strin
export type DropPosition = "before" | "after" | "inside";
export interface ReorderEntriesOptions {
preserveSameGroupOrder?: boolean;
}
function findEntryParentGroupId(entries: SidebarOrderEntry[], entryId: string, parentGroupId: string | null = null): string | null | undefined {
for (const entry of entries) {
if (entry.id === entryId) return parentGroupId;
if (entry.type !== "group") continue;
const found = findEntryParentGroupId(entryChildren(entry), entryId, entry.id);
if (found !== undefined) return found;
}
return undefined;
}
export function reorderEntry(layout: SidebarLayout, draggedId: string, targetId: string, position: DropPosition): SidebarLayout {
if (draggedId === targetId) return layout;
@@ -387,6 +401,16 @@ export function reorderEntry(layout: SidebarLayout, draggedId: string, targetId:
return { ...layout, order };
}
export function reorderEntries(layout: SidebarLayout, draggedIds: string[], targetId: string, position: DropPosition, options: ReorderEntriesOptions = {}): SidebarLayout {
let nextLayout = layout;
for (const draggedId of draggedIds) {
if (draggedId === targetId) continue;
if (position === "inside" && options.preserveSameGroupOrder && findEntryParentGroupId(nextLayout.order, draggedId) === targetId) continue;
nextLayout = reorderEntry(nextLayout, draggedId, targetId, position);
}
return nextLayout;
}
export function createGroup(layout: SidebarLayout, name: string, parentGroupId?: string | null): { layout: SidebarLayout; groupId: string } {
const groupId = uuid();
const group: ConnectionGroup = { id: groupId, name, collapsed: false };