Migrate Committee interface patterns
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
i18nMessage,
|
||||
usePlatformLanguage,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
@@ -12,6 +18,11 @@ import {
|
||||
type CommitteeRecord
|
||||
} from "../../api/committee";
|
||||
import { COMMITTEE_STATES, committeeStateOptions } from "./lifecycle";
|
||||
import {
|
||||
COMMITTEE_FIELD_DOCUMENTATION,
|
||||
COMMITTEE_INTERFACE_I18N,
|
||||
committeeDisabledReason
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type Draft = {
|
||||
title: string;
|
||||
@@ -56,9 +67,14 @@ export default function CommitteeRecordDialog({
|
||||
onClose: () => void;
|
||||
onSaved: (record: CommitteeRecord) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<Draft>(() => draftFromRecord(kind, record));
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const initialDraft = useMemo(() => draftFromRecord(kind, record), [kind, record]);
|
||||
const [draft, setDraft] = useState<Draft>(initialDraft);
|
||||
const [baseline, setBaseline] = useState<Draft>(initialDraft);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [confirmLifecycleChange, setConfirmLifecycleChange] = useState(false);
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
const choices = useMemo(
|
||||
() => draft.choices.split(",").map((item) => item.trim()).filter(Boolean),
|
||||
[draft.choices]
|
||||
@@ -70,11 +86,16 @@ export default function CommitteeRecordDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setDraft(draftFromRecord(kind, record));
|
||||
setDraft(initialDraft);
|
||||
setBaseline(initialDraft);
|
||||
setError("");
|
||||
}, [kind, open, record]);
|
||||
setConfirmLifecycleChange(false);
|
||||
}, [initialDraft, open]);
|
||||
|
||||
async function save() {
|
||||
const dirty = draftKey(draft) !== draftKey(baseline);
|
||||
const incomplete = !draft.title.trim() || !draft.changeReason.trim();
|
||||
|
||||
async function save(closeAfter = true): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -91,30 +112,61 @@ export default function CommitteeRecordDialog({
|
||||
payload,
|
||||
record?.revision
|
||||
);
|
||||
const nextDraft = draftFromRecord(kind, saved);
|
||||
setDraft(nextDraft);
|
||||
setBaseline(nextDraft);
|
||||
onSaved(saved);
|
||||
onClose();
|
||||
if (closeAfter) onClose();
|
||||
return true;
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : "Committee record could not be saved.");
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: open && dirty,
|
||||
onSave: () => save(false),
|
||||
onDiscard: () => setDraft(baseline),
|
||||
title: "i18n:govoplan-committee.unsaved_title",
|
||||
message: "i18n:govoplan-committee.unsaved_message"
|
||||
});
|
||||
|
||||
function requestClose() {
|
||||
if (busy) return;
|
||||
if (dirty) requestDiscard(onClose);
|
||||
else onClose();
|
||||
}
|
||||
|
||||
function requestSave() {
|
||||
if (record && draft.state !== record.state) {
|
||||
setConfirmLifecycleChange(true);
|
||||
return;
|
||||
}
|
||||
void save();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title={`${record ? "Edit" : "New"} ${labelForKind(kind)}`}
|
||||
onClose={onClose}
|
||||
title={i18nMessage("i18n:govoplan-committee.record_dialog_title", {
|
||||
action: translateText(record ? "Edit" : "New"),
|
||||
kind: translateText(kindLabel(kind))
|
||||
})}
|
||||
onClose={requestClose}
|
||||
closeDisabled={busy}
|
||||
portal
|
||||
className="committee-record-dialog"
|
||||
footer={
|
||||
<>
|
||||
<Button disabled={busy} onClick={onClose}>Cancel</Button>
|
||||
<Button disabled={busy} disabledReason={committeeDisabledReason({ busy })} onClick={requestClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={busy || !draft.title.trim() || !draft.changeReason.trim()}
|
||||
onClick={() => void save()}
|
||||
disabled={busy || incomplete}
|
||||
disabledReason={busy ? COMMITTEE_INTERFACE_I18N.busy : incomplete ? COMMITTEE_INTERFACE_I18N.incomplete : undefined}
|
||||
onClick={requestSave}
|
||||
>
|
||||
{busy ? "Saving" : "Save"}
|
||||
</Button>
|
||||
@@ -122,9 +174,10 @@ export default function CommitteeRecordDialog({
|
||||
}
|
||||
>
|
||||
<div className="committee-record-form">
|
||||
<div className="committee-dialog-help"><DocumentationHelpLink reference={COMMITTEE_FIELD_DOCUMENTATION} /></div>
|
||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
<div className="committee-form-grid">
|
||||
<FormField label="Title">
|
||||
<FormField label="Title" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.title}
|
||||
maxLength={500}
|
||||
@@ -132,19 +185,19 @@ export default function CommitteeRecordDialog({
|
||||
onChange={(event) => setDraft({ ...draft, title: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="State">
|
||||
<FormField label="State" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.state}
|
||||
disabled={busy}
|
||||
onChange={(event) => setDraft({ ...draft, state: event.target.value })}
|
||||
>
|
||||
{stateOptions.map((state) => <option key={state} value={state}>{humanize(state)}</option>)}
|
||||
{stateOptions.map((state) => <option key={state} value={state}>{stateLabel(state)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{kind === "body" ? (
|
||||
<FormField label="Responsible organization unit ID">
|
||||
<FormField label="Responsible organization unit ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.organizationUnitId}
|
||||
disabled={busy}
|
||||
@@ -183,7 +236,7 @@ export default function CommitteeRecordDialog({
|
||||
</FormField>
|
||||
</div>
|
||||
{draft.state === "decided" ? (
|
||||
<FormField label="Formal Decision ID">
|
||||
<FormField label="Formal Decision ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.decisionId} disabled={busy} onChange={(event) => setDraft({ ...draft, decisionId: event.target.value })} />
|
||||
</FormField>
|
||||
) : null}
|
||||
@@ -203,11 +256,11 @@ export default function CommitteeRecordDialog({
|
||||
<FormField label="Eligible voters">
|
||||
<input type="number" min="0" value={draft.eligibleCount} disabled={busy} onChange={(event) => setDraft({ ...draft, eligibleCount: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Ballot provider (optional)">
|
||||
<FormField label="Ballot provider (optional)" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.providerId} disabled={busy} onChange={(event) => setDraft({ ...draft, providerId: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="Voting ballot ID (optional)">
|
||||
<FormField label="Voting ballot ID (optional)" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.votingBallotId} disabled={busy} onChange={(event) => setDraft({ ...draft, votingBallotId: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Choices (comma separated)">
|
||||
@@ -250,7 +303,7 @@ export default function CommitteeRecordDialog({
|
||||
|
||||
{kind === "minute" ? (
|
||||
<>
|
||||
<FormField label="Minutes record ID">
|
||||
<FormField label="Minutes record ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.contentRecordId} disabled={busy} onChange={(event) => setDraft({ ...draft, contentRecordId: event.target.value })} />
|
||||
</FormField>
|
||||
{draft.state === "accepted" || draft.state === "corrected" ? (
|
||||
@@ -259,7 +312,7 @@ export default function CommitteeRecordDialog({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<FormField label="Change reason">
|
||||
<FormField label="Change reason" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.changeReason}
|
||||
maxLength={1000}
|
||||
@@ -268,6 +321,23 @@ export default function CommitteeRecordDialog({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
open={confirmLifecycleChange}
|
||||
title="i18n:govoplan-committee.save_state_title"
|
||||
message={record ? i18nMessage("i18n:govoplan-committee.confirm_state_change", {
|
||||
kind: translateText(kindLabel(kind)),
|
||||
from: translateText(stateLabel(record.state)),
|
||||
to: translateText(stateLabel(draft.state))
|
||||
}) : ""}
|
||||
confirmLabel="Save"
|
||||
tone={["cancelled", "retired", "withdrawn"].includes(draft.state) ? "danger" : "default"}
|
||||
busy={busy}
|
||||
onConfirm={() => {
|
||||
setConfirmLifecycleChange(false);
|
||||
void save();
|
||||
}}
|
||||
onCancel={() => setConfirmLifecycleChange(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -275,10 +345,10 @@ export default function CommitteeRecordDialog({
|
||||
function EvidenceFields({ draft, busy, setDraft }: { draft: Draft; busy: boolean; setDraft: (draft: Draft) => void }) {
|
||||
return (
|
||||
<div className="committee-form-grid">
|
||||
<FormField label="Approval ID">
|
||||
<FormField label="Approval ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.approvalId} disabled={busy} onChange={(event) => setDraft({ ...draft, approvalId: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Evidence record ID">
|
||||
<FormField label="Evidence record ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.evidenceId} disabled={busy} onChange={(event) => setDraft({ ...draft, evidenceId: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -432,6 +502,18 @@ function labelForKind(kind: CommitteeObjectKind): string {
|
||||
return humanize(kind);
|
||||
}
|
||||
|
||||
function kindLabel(kind: CommitteeObjectKind): string {
|
||||
return `i18n:govoplan-committee.kind_${kind}`;
|
||||
}
|
||||
|
||||
function stateLabel(state: string): string {
|
||||
return `i18n:govoplan-committee.state_${state}`;
|
||||
}
|
||||
|
||||
function draftKey(draft: Draft): string {
|
||||
return JSON.stringify(draft);
|
||||
}
|
||||
|
||||
function humanize(value: string): string {
|
||||
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user