mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(documents): multi-select bulk actions on the vault timeline
Selection grows file-manager gestures — shift-click ranges (additive, anchored on the last plain toggle) and press-and-hold on touch — and a floating toolbar appears with the selected count and the bulk verbs: change type, link condition, delete, clear. All verbs drive the bulk endpoint in <=100-id chunks with merged per-id results; partial failures surface as one counted toast, and a bulk delete gets one aggregate undo that bulk-restores the succeeded rows (both sides no-op-safe by contract). Escape clears the selection unless a dialog owns the key. Refs: document-vault plan W3-T2
This commit is contained in:
1 parent
a8a733fc4b
commit
aef2003d4c
13 files changed
+586
-45
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
/**
|
||||
* The floating bulk bar, pinned via single-pass static renders: a labelled
|
||||
* toolbar carrying the selected count and the four bulk verbs. The
|
||||
* link-condition menu only renders when the account actually has episodes
|
||||
* to link — no dead affordance on an episode-free account.
|
||||
*/
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { DocumentBulkBar } from "../document-bulk-bar";
|
||||
|
||||
function render(episodes: { id: string; label: string }[]) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<DocumentBulkBar
|
||||
selectedCount={3}
|
||||
episodes={episodes}
|
||||
busy={false}
|
||||
onSetKind={() => {}}
|
||||
onLinkEpisode={() => {}}
|
||||
onDelete={() => {}}
|
||||
onClear={() => {}}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<DocumentBulkBar>", () => {
|
||||
it("renders a labelled toolbar with count and the bulk verbs", () => {
|
||||
const html = render([{ id: "ep1", label: "Knee" }]);
|
||||
expect(html).toContain('data-slot="document-bulk-bar"');
|
||||
expect(html).toContain('role="toolbar"');
|
||||
expect(html).toContain("3 selected");
|
||||
expect(html).toContain("Change type");
|
||||
expect(html).toContain("Link condition");
|
||||
expect(html).toContain("Delete");
|
||||
expect(html).toContain("Clear selection");
|
||||
});
|
||||
|
||||
it("omits the link-condition verb when the account has no episodes", () => {
|
||||
const html = render([]);
|
||||
expect(html).not.toContain("Link condition");
|
||||
expect(html).toContain("Change type");
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
classifyUploadFailure,
|
||||
countActiveFilters,
|
||||
documentDateKey,
|
||||
expandRangeSelection,
|
||||
formatBytes,
|
||||
formatMonthLabel,
|
||||
parseUploadResponse,
|
||||
@@ -252,3 +253,31 @@ describe("upload response contract (§3.2)", () => {
|
||||
expect(parseUploadResponse(500, "")).toMatchObject({ reason: "generic" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("shift-click range selection", () => {
|
||||
const order = ["a", "b", "c", "d", "e"];
|
||||
|
||||
it("selects the inclusive range between anchor and target, both ways", () => {
|
||||
expect(
|
||||
[...expandRangeSelection(order, new Set(["b"]), "b", "d")].sort(),
|
||||
).toEqual(["b", "c", "d"]);
|
||||
expect(
|
||||
[...expandRangeSelection(order, new Set(["d"]), "d", "b")].sort(),
|
||||
).toEqual(["b", "c", "d"]);
|
||||
});
|
||||
|
||||
it("is additive — a range gesture never deselects", () => {
|
||||
const next = expandRangeSelection(order, new Set(["a", "c"]), "c", "e");
|
||||
expect([...next].sort()).toEqual(["a", "c", "d", "e"]);
|
||||
});
|
||||
|
||||
it("falls back to a plain toggle without a live anchor", () => {
|
||||
expect([...expandRangeSelection(order, new Set(), null, "c")]).toEqual([
|
||||
"c",
|
||||
]);
|
||||
// Anchor filtered out of the current order → plain toggle (deselect).
|
||||
expect([
|
||||
...expandRangeSelection(order, new Set(["c"]), "gone", "c"),
|
||||
]).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user