fix(sidebar): preserve order on same-folder sorted drops
This commit is contained in:
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
@@ -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 };
|
||||
|
||||
Reference in new issue
Block a user