File size: 3,982 Bytes
092334a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | // ---------------------------------------------------------------------------
// ui / blocks.ts β EXIT wave 2 (W2-6/W2-9). The block RULES, pure and
// React-free so `verify_ui.py` can run them under bare node.
//
// The two rules in this file are the two owner constants the presentation layer
// is most able to break quietly, which is exactly why they live here rather
// than inline in a component: a rule inside JSX can only be checked by looking
// at a screenshot, and both of these fail in ways a screenshot looks fine for.
//
// 1. **No silent caps.** `capDisclosure` renders from the SERVER's numbers.
// 2. **One drill grammar.** `cellDescriptor` resolves a row to a descriptor
// or to nothing β never to a half-populated one.
// ---------------------------------------------------------------------------
import { int } from "./fmt";
import { isRowDrillRule } from "./types";
import type { DecompDescriptor, DrillDescriptor, DrillRule, EntityKind, PageRow } from "./types";
export interface CapDisclosure {
text: string;
/** True when rows were actually withheld β the disclosure is louder then. */
capped: boolean;
}
/**
* "Showing N of M", or nothing at all.
*
* β THE NUMBERS ARE THE SERVER'S AND ARE NEVER DERIVED FROM `rows.length`.
* That is the whole point and it is subtle: a disclosure computed from the
* array it is describing agrees with a truncated list BY CONSTRUCTION, so it
* would read "Showing 25 of 25" over a list the server cut from 812. It cannot
* be wrong, which means it is not a check β it is decoration that looks like
* one ([[no-unverifiable-aggregates]]: a silent `[:N]` cap is a defect).
*
* Y1 rule 6 ships BOTH numbers even when they are equal, so `shown === total`
* still renders β "Showing 12 of 12" is the sentence that tells a reader
* nothing was withheld, and its absence is not the same statement.
*/
export function capDisclosure(shown?: number, total?: number): CapDisclosure | null {
if (typeof shown !== "number" || typeof total !== "number") return null;
if (!Number.isFinite(shown) || !Number.isFinite(total)) return null;
return { text: `Showing ${int(shown)} of ${int(total)}`, capped: total > shown };
}
/**
* A row + a block-level drill rule β the descriptor to open, or `undefined`.
*
* Y1 rule 5's placements (b) and (c):
* (b) an ENTITY rule names the columns holding the id and the label;
* (c) a ROW rule names the column holding a complete `DecompDescriptor`.
*
* β A DESCRIPTOR IS COMPLETE OR IT DOES NOT EXIST. A half-populated one opens a
* panel onto the wrong window β a period drill with no `date_from` would show
* the reader a number attributed to a scope nobody computed. Returning
* `undefined` costs a click; returning a partial costs the number's credibility.
*/
export function cellDescriptor(
row: PageRow,
rule: DrillRule | undefined
): DrillDescriptor | undefined {
if (!rule) return undefined;
if (isRowDrillRule(rule)) {
const d = row[rule.row_key];
if (!d || typeof d !== "object") return undefined;
const dd = d as DecompDescriptor;
if (dd.kind !== "decomp") return undefined;
// The window is what makes a decomp descriptor answerable at all.
if (!dd.date_from || !dd.date_to) return undefined;
return dd;
}
const id = row[rule.id_key];
if (id == null || id === "") return undefined;
const label = rule.label_key ? row[rule.label_key] : undefined;
return {
kind: rule.kind as EntityKind,
id: id as string | number,
label: String(label ?? id),
};
}
/** The empty-state sentence. NEVER the bare words "no data": an empty table
* that does not say what empty MEANS reads as a broken page, and sends people
* looking in the wrong place. The server supplies the meaning (`empty`); this
* is the fallback when it did not. */
export function emptyText(empty?: string): string {
const s = (empty ?? "").trim();
return s || "Nothing matched this scope.";
}
|