t-paliad-258. m's verdict on t-paliad-251's rule UI: "too many options"
(4 'Oral hearings' across courts, etc.). Replace the full deadline_rules
catalog dropdown + sort selector with a binary model and unify the rule
display contract across every surface that prints a rule label.
Binary Rule field on the deadline form
- Auto (default): rule_id is derived from the chosen Type. The resolved
rule renders read-only as 'Auto | <Name · Citation>' next to the
field. No catalog picker, no sort options.
- Custom: free-text input. Stored as deadlines.custom_rule_text (new
nullable column, migration 122). Mutually exclusive with rule_id at
the persistence boundary.
- Toggle link flips between modes. Re-toggling to Auto re-resolves from
the current Type — no stale state.
Schema + service (additive)
- migration 122 adds paliad.deadlines.custom_rule_text (nullable).
Existing rows: empty custom_rule_text + non-null rule_id = Auto-
equivalent. Both NULL = "keine Regel" (consistent with today).
- models.Deadline.CustomRuleText + service SELECTs include the column.
- CreateDeadlineInput accepts custom_rule_text; the service drops it
when rule_id is set (catalog wins; simple invariant at the boundary).
- UpdateDeadlineInput grows a {RuleSet, RuleID, CustomRuleText} triple.
RuleSet=true is the discriminator so absent fields don't overwrite
the row (PATCH semantics). RuleID and CustomRuleText are mutually
exclusive in one request; service rejects "both set".
- EventListItem (the /api/events union) carries CustomRuleText so list
surfaces can render it.
Frontend: deadlines-new
- Drop the rule <select>, the by_proceeding/by_court/alpha sort
dropdown, the override-warning slot, and the collapsed-by-Regel Typ
view. Strip the (Rule→Type) auto-fill machinery — direction is now
one-way (Type → Auto-resolved Rule).
- Keep Type→Rule resolution: resolveAutoRuleForType picks the canonical
rule by project's proceeding, then jurisdiction match, then first
candidate. Same logic, just re-aimed at the read-only display.
- Standardtitel preserves the chain (event type → Auto rule label →
Custom text → proceeding → fallback) so the recipe still produces a
sensible title even when Custom is used.
Frontend: deadlines-detail
- Read-only display: catalog rule → Name · Citation, else
custom_rule_text + Custom badge, else legacy rule_code, else "—".
- Edit mode: mirror the create form with the Auto/Custom toggle.
enterEdit initialises the mode from the persisted deadline; Save
PATCHes with rule_set:true + the chosen rule pointer.
Rule-label addendum (m's 14:31 follow-up)
- Canonical contract everywhere: Name primary, Citation muted secondary
("Notice of Appeal · UPC.RoP.220.1"). Custom rules render the text
with a "Custom" pill.
- New frontend/src/client/rule-label.ts exports formatRuleLabel /
formatRuleLabelHTML / formatCustomRuleLabelHTML — one helper per
shape (plain text vs muted-citation HTML).
- Wired into: deadlines-new Auto display, deadlines-detail read +
Standardtitel, events.ts ruleDisplay (REGEL column on /events),
projects-detail.ts Fristen table, views/shape-list.ts generic
rule column.
- Verfahrensablauf (views/verfahrensablauf-core.ts) already renders
name + citation chip separately and matches the canonical pattern;
no change needed. Schriftsätze table is column-shaped (name + code
in distinct columns) and out of scope per the addendum.
CSS
- New .rule-mode-auto / .rule-mode-custom / .rule-label-* family.
- Drop the dead .rule-sort-select rule and the .event-type-collapsed*
family (retired with the catalog dropdown).
i18n
- DE+EN. Remove 10 stale keys (rule.none, autofill, autofill_inline,
mismatch, override, override_warn, sort.*). Add 6 (auto_no_match,
auto_pick_type, custom_badge, custom_placeholder,
mode.toggle_to_auto, mode.toggle_to_custom).
Build hygiene
- go build + go test ./internal/... clean.
- frontend bun build clean (2803 keys, scan clean).
Out of scope (per issue)
- Promoting Custom entries back to the catalog ("save as new rule").
- Filtering/searching custom_rule_text in deadline lists.
- Touching the event-type browse modal (Part 1 of #82 — that stays).
Files
- internal/db/migrations/122_deadlines_custom_rule_text.{up,down}.sql
- internal/models/models.go
- internal/services/deadline_service.go (Create+Update+SELECT)
- internal/services/event_service.go (union projection)
- frontend/src/client/rule-label.ts (new helper)
- frontend/src/client/deadlines-new.ts (rewrite)
- frontend/src/client/deadlines-detail.ts (Auto/Custom editor + display)
- frontend/src/client/events.ts (REGEL column)
- frontend/src/client/projects-detail.ts (Fristen table cell)
- frontend/src/client/views/shape-list.ts (generic rule column)
- frontend/src/client/i18n.ts + i18n-keys.ts (DE+EN delta)
- frontend/src/deadlines-new.tsx (strip dropdown+sort, add toggle)
- frontend/src/deadlines-detail.tsx (Auto/Custom edit slots)
- frontend/src/styles/global.css (rule-mode + rule-label families)
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
// rule-label — canonical display contract for deadline rules.
|
|
//
|
|
// t-paliad-258 / m/paliad#89 addendum. Previously each surface (deadline
|
|
// form, list rows, detail header, Schriftsätze tab, browse-a-proceeding)
|
|
// invented its own pattern: sometimes citation-only, sometimes name-only,
|
|
// sometimes "code — name". m flagged this on the first submissions in a
|
|
// proceeding sequence where the inconsistency was most visible.
|
|
//
|
|
// Canonical pattern: **Name primary, Citation muted secondary**.
|
|
// Text: "Notice of Appeal · UPC.RoP.220.1"
|
|
// HTML: <span class="rule-label-name">Notice of Appeal</span>
|
|
// <span class="rule-label-sep"> · </span>
|
|
// <span class="rule-label-cite">UPC.RoP.220.1</span>
|
|
//
|
|
// Custom rules (t-paliad-258 — free-text label entered by the lawyer):
|
|
// formatCustomRuleLabel produces "<text>" with a "Custom" badge slot
|
|
// so list/detail surfaces can render both shapes uniformly.
|
|
|
|
import { getLang, t } from "./i18n";
|
|
|
|
export interface RuleLike {
|
|
name: string;
|
|
name_en?: string | null;
|
|
// The catalog carries multiple citation fields depending on which
|
|
// surface populated it. Order of preference: legal_source > rule_code
|
|
// > code. All three are accepted so callers don't have to normalise.
|
|
rule_code?: string | null;
|
|
code?: string | null;
|
|
legal_source?: string | null;
|
|
}
|
|
|
|
// formatRuleLabel returns the canonical plain-text label.
|
|
// Falls back gracefully when either side is missing.
|
|
export function formatRuleLabel(r: RuleLike): string {
|
|
const lang = getLang();
|
|
const name = (lang === "en" && r.name_en) ? r.name_en : r.name;
|
|
const cite = ruleCitation(r);
|
|
if (name && cite) return `${name} · ${cite}`;
|
|
return name || cite || "";
|
|
}
|
|
|
|
// formatRuleLabelHTML returns the canonical HTML form with muted-citation
|
|
// styling. The caller passes the HTML-escape helper so we don't pull a
|
|
// dependency on a specific esc() module — every surface already has one.
|
|
export function formatRuleLabelHTML(r: RuleLike, esc: (s: string) => string): string {
|
|
const lang = getLang();
|
|
const name = (lang === "en" && r.name_en) ? r.name_en : r.name;
|
|
const cite = ruleCitation(r);
|
|
if (name && cite) {
|
|
return (
|
|
`<span class="rule-label-name">${esc(name)}</span>` +
|
|
`<span class="rule-label-sep"> · </span>` +
|
|
`<span class="rule-label-cite">${esc(cite)}</span>`
|
|
);
|
|
}
|
|
return esc(name || cite || "");
|
|
}
|
|
|
|
// ruleCitation returns the best-available citation string for a rule.
|
|
// Exported so callers that need the bare code (e.g. CalDAV exports,
|
|
// inline data attributes) can pull it without going through the label
|
|
// formatter.
|
|
export function ruleCitation(r: RuleLike): string {
|
|
return r.legal_source || r.rule_code || r.code || "";
|
|
}
|
|
|
|
// formatCustomRuleLabelHTML — render a free-text custom rule label with
|
|
// a "Custom" badge slot. Used by surfaces that may display either a
|
|
// catalog rule (formatRuleLabelHTML) or a custom one. Returns "" when
|
|
// the text is empty so callers can fall through to "—".
|
|
export function formatCustomRuleLabelHTML(text: string | null | undefined, esc: (s: string) => string): string {
|
|
const trimmed = (text ?? "").trim();
|
|
if (!trimmed) return "";
|
|
const badge = t("deadlines.field.rule.custom_badge") || "Custom";
|
|
return (
|
|
`<span class="rule-label-name">${esc(trimmed)}</span>` +
|
|
`<span class="rule-label-badge rule-label-badge--custom">${esc(badge)}</span>`
|
|
);
|
|
}
|
|
|
|
// formatCustomRuleLabel — plain-text equivalent of the above.
|
|
export function formatCustomRuleLabel(text: string | null | undefined): string {
|
|
const trimmed = (text ?? "").trim();
|
|
if (!trimmed) return "";
|
|
const badge = t("deadlines.field.rule.custom_badge") || "Custom";
|
|
return `${trimmed} · ${badge}`;
|
|
}
|