fix: nest legacy bills and stamp builds with commit
This commit is contained in:
1 parent
85a21c58eb
commit
f02c452d6c
17 files changed
+214
-27
No files matched your search
@@ -6,7 +6,7 @@ import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { createSeedDataset } from "../../domain/seed.ts";
|
||||
import { addBudgetFolder, addRecurringCost, confirmOccurrence } from "../../store/actions.ts";
|
||||
import { addBudgetFolder, addRecurringCost, confirmOccurrence, placeRecurringUnderPost } from "../../store/actions.ts";
|
||||
import { monthView } from "../../domain/views.ts";
|
||||
import { store } from "../../store/index.ts";
|
||||
import { PostTableLegend } from "./PostTable.tsx";
|
||||
@@ -57,3 +57,27 @@ test("legend scopes base currency to post figures and identifies bill currencies
|
||||
expect(legend.textContent).toContain("Projected subtracts bills not yet confirmed.");
|
||||
expect(legend.textContent).not.toContain("All figures");
|
||||
}));
|
||||
|
||||
test("bills beneath a post are hidden until its post is expanded", () => {
|
||||
const data = createSeedDataset("2026-09");
|
||||
const games = data.posts[0]!;
|
||||
const bill = addRecurringCost(data, { name: "Star Rail Pass", folderId: null, archived: false, amount: { amount: 35, currency: "DKK" }, startDate: "2026-09", recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar", splitMode: "percent", splits: [{ postId: games.id, value: 100, absorbsRemainder: true }] });
|
||||
placeRecurringUnderPost(data, bill.id, games.id);
|
||||
const original = { get: store.get, subscribe: store.subscribe };
|
||||
store.get = () => data;
|
||||
store.subscribe = () => () => {};
|
||||
const container = document.createElement("div"); document.body.append(container);
|
||||
const root = createRoot(container);
|
||||
try {
|
||||
act(() => root.render(<MemoryRouter><BudgetTree monthId="2026-09" rows={monthView(data, "2026-09").rows} /></MemoryRouter>));
|
||||
const controls = Array.from(container.querySelectorAll(`button[aria-label="Bills under ${games.name}"]`)) as HTMLButtonElement[];
|
||||
expect(controls).toHaveLength(2);
|
||||
expect(controls[0]!.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(container.querySelector(`[data-budget-item="${bill.id}"]`)).toBeNull();
|
||||
act(() => controls[0]!.click());
|
||||
expect(controls[0]!.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(container.querySelectorAll(`[data-budget-item="${bill.id}"]`)).toHaveLength(2);
|
||||
} finally {
|
||||
act(() => root.unmount()); container.remove(); Object.assign(store, original);
|
||||
}
|
||||
});
|
||||
@@ -37,7 +37,10 @@ export function BudgetTree({ monthId, rows, onChangeRule }: { monthId: MonthId;
|
||||
const render = (nodes: BudgetNode[], mobile: boolean, depth = 0): ReactNode[] => nodes.flatMap(node => {
|
||||
const indent = Math.min(depth, 4) * 16;
|
||||
const id = node.kind === "folder" ? node.folder.id : node.kind === "post" ? node.row.post.id : node.kind === "purchase" ? node.purchase.id : node.cost.id;
|
||||
if (node.kind === "post") return [mobile ? <MobilePostRow key={id} row={node.row} indent={indent} monthId={monthId} onChangeRule={onChangeRule} /> : <DesktopPostBody key={id} row={node.row} indent={indent} monthId={monthId} onChangeRule={onChangeRule} />, ...render(node.children, mobile, depth + 1)];
|
||||
if (node.kind === "post") {
|
||||
const rowProps = { row: node.row, indent, monthId, onChangeRule, ...(node.children.length ? { childrenExpanded: expanded.has(id), onToggleChildren: () => toggle(id) } : {}) };
|
||||
return [mobile ? <MobilePostRow key={id} {...rowProps} /> : <DesktopPostBody key={id} {...rowProps} />, ...(expanded.has(id) ? render(node.children, mobile, depth + 1) : [])];
|
||||
}
|
||||
const name = node.kind === "folder" ? node.folder.name : node.kind === "purchase" ? node.purchase.description : node.cost.name;
|
||||
const content = node.kind === "folder" ? folderHeading(node) : node.kind === "purchase" ? purchaseContent(node) : billContent(node);
|
||||
const style = { paddingInlineStart: `${indent}px` };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link } from "react-router";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useMoneyFormat, type MoneyFormat } from "../hooks/useMoneyFormat.ts";
|
||||
import { ruleAt } from "../../domain/allocation.ts";
|
||||
@@ -211,14 +212,20 @@ export function PostTable({ monthId, rows, onChangeRule }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
interface PostRowProps { row: MonthPostRow; monthId: MonthId; onChangeRule?: (postId: string) => void; indent?: number; }
|
||||
interface PostRowProps { row: MonthPostRow; monthId: MonthId; onChangeRule?: (postId: string) => void; indent?: number; childrenExpanded?: boolean; onToggleChildren?: () => void; }
|
||||
|
||||
export function DesktopPostBody({ row, monthId, onChangeRule, indent = 0 }: PostRowProps) {
|
||||
function PostChildrenToggle({ postName, expanded, onToggle }: { postName: string; expanded: boolean; onToggle: () => void }) {
|
||||
const Icon = expanded ? ChevronDown : ChevronRight;
|
||||
return <button type="button" aria-label={`Bills under ${postName}`} aria-expanded={expanded} onClick={onToggle} className="mr-1 inline-flex size-6 align-middle items-center justify-center rounded-sm text-budget-ink-muted hover:text-budget-accent focus-visible:outline-2 focus-visible:outline-ring"><Icon size={16} aria-hidden="true" /></button>;
|
||||
}
|
||||
|
||||
export function DesktopPostBody({ row, monthId, onChangeRule, indent = 0, childrenExpanded, onToggleChildren }: PostRowProps) {
|
||||
const cells = postRowCells(row, useMoneyFormat());
|
||||
return (
|
||||
<tbody data-budget-item={row.post.id} data-budget-name={row.post.name} className="group">
|
||||
<tr className="transition-colors group-hover:bg-accent/60">
|
||||
<td className="pb-0 pt-3" style={{ paddingInlineStart: indent }}>
|
||||
{onToggleChildren && <PostChildrenToggle postName={row.post.name} expanded={!!childrenExpanded} onToggle={onToggleChildren} />}
|
||||
<PostName row={row} monthId={monthId} />
|
||||
{onChangeRule && (
|
||||
<ChangeRuleButton
|
||||
@@ -257,12 +264,13 @@ export function DesktopPostBody({ row, monthId, onChangeRule, indent = 0 }: Post
|
||||
);
|
||||
}
|
||||
|
||||
export function MobilePostRow({ row, monthId, onChangeRule, indent = 0 }: PostRowProps) {
|
||||
export function MobilePostRow({ row, monthId, onChangeRule, indent = 0, childrenExpanded, onToggleChildren }: PostRowProps) {
|
||||
const cells = postRowCells(row, useMoneyFormat());
|
||||
return (
|
||||
<li data-budget-item={row.post.id} data-budget-name={row.post.name} className="space-y-1.5" style={{ paddingInlineStart: indent }}>
|
||||
<div className="flex items-baseline justify-between gap-3 text-sm">
|
||||
<span className="min-w-0">
|
||||
{onToggleChildren && <PostChildrenToggle postName={row.post.name} expanded={!!childrenExpanded} onToggle={onToggleChildren} />}
|
||||
<PostName row={row} monthId={monthId} />
|
||||
</span>
|
||||
<span
|
||||
|
||||
Reference in new issue
Block a user