feat: add exact retention help contexts

This commit is contained in:
2026-08-17 17:41:45 +02:00
parent 887e9beb9e
commit ff88142471
15 changed files with 85 additions and 31 deletions
@@ -67,6 +67,7 @@ type DayKey =
type FieldDefinition = {
key: PrivacyRetentionPolicyFieldKey;
label: string;
helpContextId: string;
kind: "raw-json" | "days" | "audit";
systemOnly?: boolean;
};
@@ -101,13 +102,13 @@ const defaultPrivacyPolicy: PrivacyRetentionPolicy = {
};
const fieldDefinitions: FieldDefinition[] = [
{ key: "store_raw_campaign_json", label: "i18n:govoplan-core.raw_campaign_json.4ca2265b", kind: "raw-json" },
{ key: "raw_campaign_json_retention_days", label: "i18n:govoplan-core.raw_campaign_json_days.48805a59", kind: "days" },
{ key: "generated_eml_retention_days", label: "i18n:govoplan-core.generated_eml_days.bcb273d9", kind: "days" },
{ key: "stored_report_detail_retention_days", label: "i18n:govoplan-core.stored_report_detail_days.296f6dab", kind: "days" },
{ key: "mock_mailbox_retention_days", label: "i18n:govoplan-core.mock_mailbox_days.a0cf3209", kind: "days", systemOnly: true },
{ key: "audit_detail_retention_days", label: "i18n:govoplan-core.audit_detail_days.21c84dd1", kind: "days" },
{ key: "audit_detail_level", label: "i18n:govoplan-core.audit_detail_level.b0565260", kind: "audit" }];
{ key: "store_raw_campaign_json", label: "i18n:govoplan-core.raw_campaign_json.4ca2265b", helpContextId: "policy.retention.field.store-raw-campaign-json", kind: "raw-json" },
{ key: "raw_campaign_json_retention_days", label: "i18n:govoplan-core.raw_campaign_json_days.48805a59", helpContextId: "policy.retention.field.raw-campaign-json-retention-days", kind: "days" },
{ key: "generated_eml_retention_days", label: "i18n:govoplan-core.generated_eml_days.bcb273d9", helpContextId: "policy.retention.field.generated-eml-retention-days", kind: "days" },
{ key: "stored_report_detail_retention_days", label: "i18n:govoplan-core.stored_report_detail_days.296f6dab", helpContextId: "policy.retention.field.stored-report-detail-retention-days", kind: "days" },
{ key: "mock_mailbox_retention_days", label: "i18n:govoplan-core.mock_mailbox_days.a0cf3209", helpContextId: "policy.retention.field.mock-mailbox-retention-days", kind: "days", systemOnly: true },
{ key: "audit_detail_retention_days", label: "i18n:govoplan-core.audit_detail_days.21c84dd1", helpContextId: "policy.retention.field.audit-detail-retention-days", kind: "days" },
{ key: "audit_detail_level", label: "i18n:govoplan-core.audit_detail_level.b0565260", helpContextId: "policy.retention.field.audit-detail-level", kind: "audit" }];
export function RetentionPolicyScopeManager({
@@ -148,11 +149,13 @@ export function RetentionPolicyScopeManager({
{targetSelectionRequired &&
<Card
title={i18nMessage("i18n:govoplan-core.value_scope", { value0: targetLabel })}
helpContextId="policy.retention.target"
helpModuleId="policy"
actions={<DocumentationHelpLink reference={RETENTION_DOCUMENTATION} label="i18n:govoplan-core.open_admin_documentation.6adbdae3" />}
>
<div className="retention-policy-target-row">
<FormField label={targetLabel}>
<select value={selectedTargetId} disabled={!hasSelectableTarget} onChange={(event) => setSelectedTargetId(event.target.value)}>
<FormField label={targetLabel} helpContextId="policy.retention.target" helpModuleId="policy">
<select data-help-context-id="policy.retention.target" data-help-module-id="policy" value={selectedTargetId} disabled={!hasSelectableTarget} onChange={(event) => setSelectedTargetId(event.target.value)}>
{!hasSelectableTarget && <option value="">{targetEmptyText}</option>}
{targetOptions.map((option) => <option key={option.id} value={option.id}>{option.secondary ? i18nMessage("i18n:govoplan-core.value_value.c189e8bc", { value0: option.label, value1: option.secondary }) : option.label}</option>)}
</select>
@@ -342,11 +345,13 @@ export function RetentionPolicyEditor({
return (
<Card
title={title}
helpContextId="policy.retention"
helpModuleId="policy"
actions={
<div className="button-row compact-actions">
<DocumentationHelpLink reference={RETENTION_DOCUMENTATION} label="i18n:govoplan-core.open_admin_documentation.6adbdae3" />
<Button onClick={() => void loadPolicy()} disabled={Boolean(reloadDisabledReason)} disabledReason={reloadDisabledReason}>{loading ? "i18n:govoplan-core.loading.33ce4174" : "i18n:govoplan-core.reload.cce71553"}</Button>
<Button variant="primary" onClick={() => void savePolicy()} disabled={Boolean(saveDisabledReason)} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-core.saving.56a2285c" : "i18n:govoplan-core.save_policy.77d67ce3"}</Button>
<Button helpContextId="policy.retention.action.reload" helpModuleId="policy" onClick={() => void loadPolicy()} disabled={Boolean(reloadDisabledReason)} disabledReason={reloadDisabledReason}>{loading ? "i18n:govoplan-core.loading.33ce4174" : "i18n:govoplan-core.reload.cce71553"}</Button>
<Button helpContextId="policy.retention.action.save" helpModuleId="policy" variant="primary" onClick={() => void savePolicy()} disabled={Boolean(saveDisabledReason)} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-core.saving.56a2285c" : "i18n:govoplan-core.save_policy.77d67ce3"}</Button>
</div>
}>
@@ -401,6 +406,8 @@ export function RetentionPolicyEditor({
<ToggleSwitch
checked={localAllowsLower(field.key)}
disabled={disabled || fieldLocked}
helpContextId="policy.retention.field.allow-lower-level-limits"
helpModuleId="policy"
onChange={(checked) => setAllowLowerLevelLimit(field.key, checked)}
label="i18n:govoplan-core.allow_override.ffa6e9a0" /> :
undefined} />);
@@ -519,18 +526,19 @@ parentPolicy: PrivacyRetentionPolicy | null)
{
if (field.kind === "raw-json") {
if (isSystem) {
return <RawJsonSystemSelect value={fullPolicy.store_raw_campaign_json ? "store" : "disabled"} disabled={disabled} onChange={setRawJson} />;
return <RawJsonSystemSelect value={fullPolicy.store_raw_campaign_json ? "store" : "disabled"} disabled={disabled} helpContextId={field.helpContextId} onChange={setRawJson} />;
}
return <RawJsonScopedSelect value={rawJsonValue(patchPolicy.store_raw_campaign_json)} parentStoresRawJson={parentPolicy?.store_raw_campaign_json ?? true} disabled={disabled} onChange={setRawJson} />;
return <RawJsonScopedSelect value={rawJsonValue(patchPolicy.store_raw_campaign_json)} parentStoresRawJson={parentPolicy?.store_raw_campaign_json ?? true} disabled={disabled} helpContextId={field.helpContextId} onChange={setRawJson} />;
}
if (field.kind === "audit") {
return <AuditDetailSelect value={isSystem ? fullPolicy.audit_detail_level : auditDetailValue(patchPolicy.audit_detail_level)} includeInherit={!isSystem} parentValue={isSystem ? null : parentPolicy?.audit_detail_level ?? null} disabled={disabled} onChange={setAuditDetail} />;
return <AuditDetailSelect value={isSystem ? fullPolicy.audit_detail_level : auditDetailValue(patchPolicy.audit_detail_level)} includeInherit={!isSystem} parentValue={isSystem ? null : parentPolicy?.audit_detail_level ?? null} disabled={disabled} helpContextId={field.helpContextId} onChange={setAuditDetail} />;
}
const parentDayLimit = !isSystem && parentPolicy ? parentPolicy[field.key as DayKey] : null;
return (
<RetentionDaysField
value={isSystem ? fullPolicy[field.key as DayKey] : patchPolicy[field.key as DayKey]}
disabled={disabled}
helpContextId={field.helpContextId}
placeholder={isSystem ? "i18n:govoplan-core.unlimited.b8bef37b" : "i18n:govoplan-core.inherit.18f99833"}
max={parentDayLimit}
onChange={(value) => setRetentionDays(field.key as DayKey, value)} />);
@@ -538,18 +546,18 @@ parentPolicy: PrivacyRetentionPolicy | null)
}
function RawJsonSystemSelect({ value, disabled, onChange }: {value: "store" | "disabled";disabled: boolean;onChange: (value: "store" | "disabled") => void;}) {
function RawJsonSystemSelect({ value, disabled, helpContextId, onChange }: {value: "store" | "disabled";disabled: boolean;helpContextId: string;onChange: (value: "store" | "disabled") => void;}) {
return (
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as "store" | "disabled")}>
<select data-help-scope="field" data-help-context-id={helpContextId} data-help-module-id="policy" value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as "store" | "disabled")}>
<option value="store">i18n:govoplan-core.store.0d8a7046</option>
<option value="disabled">i18n:govoplan-core.disable_storage.07c44d8b</option>
</select>);
}
function RawJsonScopedSelect({ value, parentStoresRawJson, disabled, onChange }: {value: RawJsonValue;parentStoresRawJson: boolean;disabled: boolean;onChange: (value: RawJsonValue) => void;}) {
function RawJsonScopedSelect({ value, parentStoresRawJson, disabled, helpContextId, onChange }: {value: RawJsonValue;parentStoresRawJson: boolean;disabled: boolean;helpContextId: string;onChange: (value: RawJsonValue) => void;}) {
return (
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as RawJsonValue)}>
<select data-help-scope="field" data-help-context-id={helpContextId} data-help-module-id="policy" value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as RawJsonValue)}>
<option value="inherit">i18n:govoplan-core.inherit.18f99833</option>
<option value="keep" disabled={!parentStoresRawJson}>i18n:govoplan-core.store_if_parent_allows.36dcb07b</option>
<option value="disable">i18n:govoplan-core.disable_storage.07c44d8b</option>
@@ -557,7 +565,7 @@ function RawJsonScopedSelect({ value, parentStoresRawJson, disabled, onChange }:
}
function RetentionDaysField({ value, disabled, placeholder, max, onChange }: {value?: number | null;disabled: boolean;placeholder: string;max?: number | null;onChange: (value: string) => void;}) {
function RetentionDaysField({ value, disabled, helpContextId, placeholder, max, onChange }: {value?: number | null;disabled: boolean;helpContextId: string;placeholder: string;max?: number | null;onChange: (value: string) => void;}) {
function handleChange(nextValue: string) {
const trimmed = nextValue.trim();
if (trimmed === "" || max === null || max === undefined) {
@@ -568,16 +576,16 @@ function RetentionDaysField({ value, disabled, placeholder, max, onChange }: {va
onChange(Number.isFinite(parsed) && parsed > max ? String(max) : nextValue);
}
return <input type="number" min={0} max={max ?? undefined} value={value ?? ""} disabled={disabled} placeholder={placeholder} onChange={(event) => handleChange(event.target.value)} />;
return <input data-help-scope="field" data-help-context-id={helpContextId} data-help-module-id="policy" type="number" min={0} max={max ?? undefined} value={value ?? ""} disabled={disabled} placeholder={placeholder} onChange={(event) => handleChange(event.target.value)} />;
}
function AuditDetailSelect({ value, includeInherit, parentValue, disabled, onChange }: {value: AuditDetailValue;includeInherit: boolean;parentValue?: PrivacyRetentionPolicy["audit_detail_level"] | null;disabled: boolean;onChange: (value: AuditDetailValue) => void;}) {
function AuditDetailSelect({ value, includeInherit, parentValue, disabled, helpContextId, onChange }: {value: AuditDetailValue;includeInherit: boolean;parentValue?: PrivacyRetentionPolicy["audit_detail_level"] | null;disabled: boolean;helpContextId: string;onChange: (value: AuditDetailValue) => void;}) {
function optionDisabled(option: PrivacyRetentionPolicy["audit_detail_level"]): boolean {
return privacyRetentionAuditDetailOptionDisabled(option, parentValue);
}
return (
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as AuditDetailValue)}>
<select data-help-scope="field" data-help-context-id={helpContextId} data-help-module-id="policy" value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as AuditDetailValue)}>
{includeInherit && <option value="inherit">i18n:govoplan-core.inherit.18f99833</option>}
<option value="full" disabled={optionDisabled("full")}>i18n:govoplan-core.full.10b28a8c</option>
<option value="redacted" disabled={optionDisabled("redacted")}>i18n:govoplan-core.redacted.8780785a</option>