feat(deadlines/new): collapse Regel + Typ to ONE field when rule sets type
m's 2026-05-08 22:08 dogfood: my first auto-fill landed but kept Regel
and Typ as TWO separate input fields. m wanted ONE — "these two are
connected, it's the same thing".
Now: when a Regel is selected and the rule's concept resolves to a
canonical event_type via the jurisdiction-aware junction, the Typ
chip cluster is HIDDEN and replaced by an inline summary —
Klageerwiderung (vorgegeben durch Regel) Anderen Typ wählen
Clicking "Anderen Typ wählen" sets a sticky expandedOverride flag
that forces the picker visible for the rest of the form session.
The chip stays in the picker so the user can edit / remove it.
The picker also stays visible when the rule has no canonical
event_type (fallback to free-text Typ) or when the user has picked
a different event_type from the canonical default (mismatch
warning surfaces yellow next to the picker, never blocking).
DE+EN i18n: deadlines.field.rule.{autofill_inline,override}.
New CSS: .event-type-collapsed{,-label,-source,-override} reusing
the existing lime-tint chip palette.
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
import { initI18n, t, tDyn } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
import { attachEventTypePicker, type PickerHandle } from "./event-types";
|
||||
import {
|
||||
attachEventTypePicker,
|
||||
eventTypeLabel,
|
||||
fetchEventTypes,
|
||||
type EventType,
|
||||
type PickerHandle,
|
||||
} from "./event-types";
|
||||
import { projectIndent } from "./project-indent";
|
||||
|
||||
let eventTypePicker: PickerHandle | null = null;
|
||||
let currentUserAdmin = false;
|
||||
let eventTypesByID = new Map<string, EventType>();
|
||||
// expandedOverride flips to true when the user clicks "Anderen Typ
|
||||
// wählen" on the collapsed inline summary. Sticky for the rest of the
|
||||
// form session — cleared only when the user reverts the rule to "Keine
|
||||
// Regel". When true, the picker stays visible regardless of whether
|
||||
// the chip matches the rule's canonical default.
|
||||
let expandedOverride = false;
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
@@ -100,33 +113,55 @@ async function loadRules() {
|
||||
}
|
||||
}
|
||||
|
||||
// t-paliad-165 — refresh the autofill hint + mismatch warning state. The
|
||||
// hint shows when the picker exactly matches the current rule's
|
||||
// suggestion (so the chip is "vorgegeben durch Regel"). The warning
|
||||
// shows when the user has picked event_types that don't include the
|
||||
// rule's canonical default (override is fine, just call it out).
|
||||
function refreshRuleHints(): void {
|
||||
const hint = document.getElementById("deadline-event-type-rule-hint");
|
||||
// t-paliad-165 follow-up — drive the collapsed/expanded view of the Typ
|
||||
// picker. The two modes are mutually exclusive:
|
||||
//
|
||||
// collapsed: rule selected + canonical event_type known + picker
|
||||
// contains exactly [default] + user hasn't clicked "Anderen Typ
|
||||
// wählen". Hides the chip cluster, surfaces a single inline
|
||||
// summary "Klageerwiderung (vorgegeben durch Regel)" + an
|
||||
// override link.
|
||||
//
|
||||
// expanded: every other case — no rule, no default for the rule,
|
||||
// picker has been edited, or expandedOverride is sticky after the
|
||||
// user clicked the override link. Picker visible; mismatch warning
|
||||
// surfaces yellow when the rule expected a different event_type.
|
||||
function refreshRuleView(): void {
|
||||
const collapsed = document.getElementById("deadline-event-type-collapsed");
|
||||
const collapsedLabel = document.getElementById("deadline-event-type-collapsed-label");
|
||||
const pickerHost = document.getElementById("deadline-event-types");
|
||||
const warn = document.getElementById("deadline-event-type-rule-mismatch");
|
||||
if (!hint || !warn) return;
|
||||
if (!collapsed || !collapsedLabel || !pickerHost || !warn) return;
|
||||
|
||||
const ruleID = (document.getElementById("deadline-rule") as HTMLSelectElement | null)?.value || "";
|
||||
const rule = ruleID ? rulesByID.get(ruleID) : undefined;
|
||||
const expected = rule?.concept_default_event_type_id ?? null;
|
||||
const picked = eventTypePicker?.getIDs() ?? [];
|
||||
|
||||
if (!ruleID || !expected) {
|
||||
hint.style.display = "none";
|
||||
const pickerMatchesDefault =
|
||||
expected !== null && picked.length === 1 && picked[0] === expected;
|
||||
const wantsCollapsed =
|
||||
!expandedOverride && ruleID !== "" && expected !== null && pickerMatchesDefault;
|
||||
|
||||
if (wantsCollapsed) {
|
||||
const et = eventTypesByID.get(expected!);
|
||||
collapsedLabel.textContent = et ? eventTypeLabel(et) : "";
|
||||
collapsed.style.display = "";
|
||||
pickerHost.style.display = "none";
|
||||
warn.style.display = "none";
|
||||
return;
|
||||
}
|
||||
if (picked.length === 1 && picked[0] === expected) {
|
||||
hint.style.display = "";
|
||||
|
||||
collapsed.style.display = "none";
|
||||
pickerHost.style.display = "";
|
||||
// Mismatch warning: rule expected an event_type AND the picker
|
||||
// doesn't contain it. (When the picker is empty + no override, no
|
||||
// warning — user is free to leave it blank.)
|
||||
if (expected && picked.length > 0 && !picked.includes(expected)) {
|
||||
warn.style.display = "";
|
||||
} else {
|
||||
warn.style.display = "none";
|
||||
return;
|
||||
}
|
||||
hint.style.display = "none";
|
||||
warn.style.display = picked.includes(expected) ? "none" : "";
|
||||
}
|
||||
|
||||
// applyRuleAutoFill replaces the picker silently when it still reflects
|
||||
@@ -139,6 +174,12 @@ function applyRuleAutoFill(): void {
|
||||
const expected = rule?.concept_default_event_type_id ?? null;
|
||||
const current = eventTypePicker.getIDs();
|
||||
|
||||
// Reset the override on transition to "Keine Regel" — fresh form
|
||||
// session. Otherwise expandedOverride stays sticky.
|
||||
if (ruleID === "") {
|
||||
expandedOverride = false;
|
||||
}
|
||||
|
||||
const pickerStillReflectsLastSuggestion =
|
||||
lastAutoFilledEventTypeID !== null &&
|
||||
current.length === 1 &&
|
||||
@@ -152,11 +193,11 @@ function applyRuleAutoFill(): void {
|
||||
}
|
||||
} else if (pickerStillReflectsLastSuggestion) {
|
||||
// New rule has no canonical event_type — clear the stale auto-fill
|
||||
// so the picker doesn't carry a hint from the old rule.
|
||||
// so the picker doesn't carry a chip from the old rule.
|
||||
eventTypePicker.setIDs([]);
|
||||
lastAutoFilledEventTypeID = null;
|
||||
}
|
||||
refreshRuleHints();
|
||||
refreshRuleView();
|
||||
}
|
||||
|
||||
function initBackLinks() {
|
||||
@@ -307,15 +348,36 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (pickerHost) {
|
||||
eventTypePicker = attachEventTypePicker(pickerHost, {
|
||||
currentUserAdmin,
|
||||
onChange: () => refreshRuleHints(),
|
||||
onChange: () => refreshRuleView(),
|
||||
});
|
||||
}
|
||||
// t-paliad-165 follow-up — preload event_types so the collapsed
|
||||
// summary can render the type's label inline without an extra round
|
||||
// trip when the user picks a Regel.
|
||||
fetchEventTypes()
|
||||
.then((types) => {
|
||||
eventTypesByID = new Map(types.map((et) => [et.id, et]));
|
||||
refreshRuleView();
|
||||
})
|
||||
.catch(() => {/* non-fatal — collapsed view falls back to empty label */});
|
||||
// t-paliad-165 — Regel change auto-fills the Typ chip from the rule's
|
||||
// concept's canonical event_type, when the picker hasn't been
|
||||
// manually edited away from the previous rule's suggestion.
|
||||
document.getElementById("deadline-rule")?.addEventListener("change", () => {
|
||||
applyRuleAutoFill();
|
||||
});
|
||||
// "Anderen Typ wählen" — sticky expanded mode so the picker stays
|
||||
// visible even when the chip still matches the rule's default.
|
||||
document.getElementById("deadline-event-type-override-btn")?.addEventListener("click", () => {
|
||||
expandedOverride = true;
|
||||
refreshRuleView();
|
||||
// Move focus into the picker's search box so the user can type
|
||||
// immediately without an extra click.
|
||||
const search = document.querySelector<HTMLInputElement>(
|
||||
"#deadline-event-types .event-type-search",
|
||||
);
|
||||
search?.focus();
|
||||
});
|
||||
// Wire approval-hint refresh: on first render + on project change.
|
||||
void refreshApprovalHint();
|
||||
document.getElementById("deadline-project")?.addEventListener("change", () => {
|
||||
|
||||
@@ -742,7 +742,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.field.rule": "Regel (optional)",
|
||||
"deadlines.field.rule.none": "Keine Regel",
|
||||
"deadlines.field.rule.autofill": "Typ vorgegeben durch Regel — entfernen, um zu überschreiben.",
|
||||
"deadlines.field.rule.autofill_inline": " (vorgegeben durch Regel)",
|
||||
"deadlines.field.rule.mismatch": "Hinweis: Typ widerspricht Regel — Sie haben den Typ überschrieben.",
|
||||
"deadlines.field.rule.override": "Anderen Typ wählen",
|
||||
"deadlines.field.notes": "Notizen (optional)",
|
||||
"deadlines.field.notes.placeholder": "Hinweise, Verweise, n\u00e4chste Schritte\u2026",
|
||||
"deadlines.error.required": "Akte, Titel und F\u00e4lligkeitsdatum sind Pflichtfelder.",
|
||||
@@ -2949,7 +2951,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.field.rule": "Rule (optional)",
|
||||
"deadlines.field.rule.none": "No rule",
|
||||
"deadlines.field.rule.autofill": "Type set by rule — remove to override.",
|
||||
"deadlines.field.rule.autofill_inline": " (set by rule)",
|
||||
"deadlines.field.rule.mismatch": "Note: type contradicts rule — you have overridden the type.",
|
||||
"deadlines.field.rule.override": "Choose another type",
|
||||
"deadlines.field.notes": "Notes (optional)",
|
||||
"deadlines.field.notes.placeholder": "References, hints, next steps\u2026",
|
||||
"deadlines.error.required": "Matter, title and due date are required.",
|
||||
|
||||
@@ -55,21 +55,42 @@ export function renderDeadlinesNew(): string {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<div className="form-field" id="deadline-event-type-field">
|
||||
<label data-i18n="deadlines.field.event_type">Typ (optional)</label>
|
||||
<div id="deadline-event-types" className="event-type-picker-host" />
|
||||
{/* t-paliad-165 — soft hint when the Regel auto-populated
|
||||
the chip, and a soft warning when the user picked a
|
||||
Regel + Typ combination that contradicts the rule's
|
||||
concept. Both rendered yellow, never blocking. */}
|
||||
<p
|
||||
className="form-hint"
|
||||
id="deadline-event-type-rule-hint"
|
||||
{/* t-paliad-165 follow-up — collapsed view: when a Regel
|
||||
is selected and a default event_type is known, the
|
||||
Typ chip is hidden and the type is rendered inline
|
||||
as a single read-only summary with an "Anderen Typ
|
||||
wählen" link that re-expands the picker. */}
|
||||
<div
|
||||
className="event-type-collapsed"
|
||||
id="deadline-event-type-collapsed"
|
||||
style="display:none"
|
||||
data-i18n="deadlines.field.rule.autofill"
|
||||
>
|
||||
Typ vorgegeben durch Regel — entfernen, um zu überschreiben.
|
||||
</p>
|
||||
<span
|
||||
className="event-type-collapsed-label"
|
||||
id="deadline-event-type-collapsed-label"
|
||||
/>
|
||||
<span
|
||||
className="event-type-collapsed-source"
|
||||
data-i18n="deadlines.field.rule.autofill_inline"
|
||||
>
|
||||
(vorgegeben durch Regel)
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="event-type-collapsed-override"
|
||||
id="deadline-event-type-override-btn"
|
||||
data-i18n="deadlines.field.rule.override"
|
||||
>
|
||||
Anderen Typ wählen
|
||||
</button>
|
||||
</div>
|
||||
<div id="deadline-event-types" className="event-type-picker-host" />
|
||||
{/* Soft warning when the user is in expanded mode AND
|
||||
has picked an event_type that doesn't include the
|
||||
rule's canonical default. Reuses the existing
|
||||
yellow form-hint--warning style; never blocking. */}
|
||||
<p
|
||||
className="form-hint form-hint--warning"
|
||||
id="deadline-event-type-rule-mismatch"
|
||||
|
||||
@@ -821,8 +821,10 @@ export type I18nKey =
|
||||
| "deadlines.field.notes.placeholder"
|
||||
| "deadlines.field.rule"
|
||||
| "deadlines.field.rule.autofill"
|
||||
| "deadlines.field.rule.autofill_inline"
|
||||
| "deadlines.field.rule.mismatch"
|
||||
| "deadlines.field.rule.none"
|
||||
| "deadlines.field.rule.override"
|
||||
| "deadlines.field.title"
|
||||
| "deadlines.field.title.placeholder"
|
||||
| "deadlines.filter.akte"
|
||||
|
||||
@@ -10420,6 +10420,43 @@ dialog.quick-add-sheet::backdrop {
|
||||
t-paliad-088 — Event Types: picker, multi-select filter, add modal
|
||||
============================================================================ */
|
||||
|
||||
/* t-paliad-165 follow-up — collapsed read-only view used on
|
||||
/deadlines/new when a Regel is selected and a default event_type is
|
||||
known. Replaces the picker with a single inline label + an
|
||||
"Anderen Typ wählen" override link. */
|
||||
.event-type-collapsed {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.55rem;
|
||||
background: var(--color-bg-lime-tint);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.3;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.event-type-collapsed-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.event-type-collapsed-source {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.event-type-collapsed-override {
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: var(--color-link, #1d4ed8);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.event-type-collapsed-override:hover { color: var(--color-link-hover, #1e40af); }
|
||||
|
||||
/* Picker host — chip cluster + search + suggest dropdown */
|
||||
.event-type-picker {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user