feat(insights): add inline edit mode to reorder and hide overview cards

This commit is contained in:
Marc-André Bombeck committed 2026-06-07 16:55:28 +02:00
1 parent e3539d7c94
commit 9b13b41f1a
10 files changed
+915 -11

No files matched your search

@@ -0,0 +1,68 @@
import { describe, it, expect } from "vitest";
import { reorderById } from "@/lib/insights-layout-reorder";
describe("reorderById", () => {
const rows = [
{ id: "a", order: 0, visible: true },
{ id: "b", order: 1, visible: false },
{ id: "c", order: 2, visible: true },
];
it("moves a row forward and renumbers densely", () => {
const next = reorderById(rows, "a", "c");
expect(next.map((r) => r.id)).toEqual(["b", "c", "a"]);
expect(next.map((r) => r.order)).toEqual([0, 1, 2]);
});
it("moves a row backward and renumbers densely", () => {
const next = reorderById(rows, "c", "a");
expect(next.map((r) => r.id)).toEqual(["c", "a", "b"]);
expect(next.map((r) => r.order)).toEqual([0, 1, 2]);
});
it("preserves every non-order field on each row", () => {
const next = reorderById(rows, "a", "b");
const moved = next.find((r) => r.id === "a");
expect(moved?.visible).toBe(true);
const hidden = next.find((r) => r.id === "b");
expect(hidden?.visible).toBe(false);
});
it("is a no-op (but still densely renumbered) when ids match", () => {
const next = reorderById(rows, "b", "b");
expect(next.map((r) => r.id)).toEqual(["a", "b", "c"]);
expect(next.map((r) => r.order)).toEqual([0, 1, 2]);
});
it("returns a densely renumbered copy when an id is missing", () => {
const sparse = [
{ id: "a", order: 5 },
{ id: "b", order: 9 },
];
const next = reorderById(sparse, "a", "zzz");
expect(next).toEqual([
{ id: "a", order: 0 },
{ id: "b", order: 1 },
]);
});
it("does not mutate the input array or its rows", () => {
const input = [
{ id: "a", order: 0 },
{ id: "b", order: 1 },
];
const snapshot = JSON.parse(JSON.stringify(input));
reorderById(input, "a", "b");
expect(input).toEqual(snapshot);
});
it("sorts an unsorted input by order before reordering", () => {
const unsorted = [
{ id: "c", order: 2 },
{ id: "a", order: 0 },
{ id: "b", order: 1 },
];
const next = reorderById(unsorted, "a", "c");
expect(next.map((r) => r.id)).toEqual(["b", "c", "a"]);
});
});
+43
View File
@@ -0,0 +1,43 @@
/**
* v1.15.11 W3 — pure reorder helper for the inline Insights edit mode.
*
* Mirrors the `reorderWidgets` contract in
* `src/components/settings/dashboard-layout-section.tsx` (move an item from
* `fromId` next to `toId`, renumber the whole list to a dense 0-based
* `order`) but is generic over any `{ id, order }` row so the section list
* AND the Vitals tile sub-list can both reorder through one tested helper.
*
* Kept as a standalone module (not imported from the dashboard component) so
* the inline-edit surface does not depend on a Settings component, and so the
* helper is unit-testable without spinning up a `DndContext`. The dashboard's
* own `reorderWidgets` stays untouched — extracting it there would have
* touched the stable dashboard surface for no behavioural gain.
*/
export interface OrderableRow {
id: string;
order: number;
}
/**
* Move the row identified by `fromId` to the position of `toId`, then
* renumber every row to a dense 0-based `order`. Returns a NEW array of NEW
* row objects (every input field is preserved via the spread); the input is
* never mutated. A no-op move (same id, or either id missing) still returns a
* densely-renumbered copy so the caller can treat the output as canonical.
*/
export function reorderById<T extends OrderableRow>(
rows: readonly T[],
fromId: string,
toId: string,
): T[] {
const sorted = [...rows].sort((a, b) => a.order - b.order);
const fromIdx = sorted.findIndex((r) => r.id === fromId);
const toIdx = sorted.findIndex((r) => r.id === toId);
if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) {
return sorted.map((r, i) => ({ ...r, order: i }));
}
const next = [...sorted];
const [removed] = next.splice(fromIdx, 1);
next.splice(toIdx, 0, removed);
return next.map((r, i) => ({ ...r, order: i }));
}