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
@@ -8,6 +8,7 @@ const read = (path) => readFileSync(resolve(webuiRoot, path), "utf8");
const settings = read("src/features/settings/SettingsPage.tsx");
const retention = read("src/features/privacy/RetentionPolicyManagement.tsx");
const confirmDialog = read("src/components/ConfirmDialog.tsx");
const credentials = read("src/components/CredentialEnvelopeManager.tsx");
const iconRail = read("src/layout/IconRail.tsx");
const moduleLoadBoundary = read("src/components/ModuleLoadBoundary.tsx");
@@ -24,8 +25,14 @@ assert.match(settings, /There are no unsaved interface changes\./, "preference s
assert.match(retention, /<ActionBlockerHint/, "retention renders the shared actionable blocker");
assert.match(retention, /contextId: "privacy\.retention"/, "retention exposes stable admin documentation");
assert.match(retention, /policy\.retention\.field\.store-raw-campaign-json/, "retention storage controls expose exact help contexts");
assert.match(retention, /policy\.retention\.field\.audit-detail-level/, "retention audit controls expose exact help contexts");
assert.match(retention, /policy\.retention\.action\.save/, "retention persistence exposes exact help contexts");
assert.match(retention, /helpModuleId="policy"/, "embedded retention controls retain Policy as their documentation owner");
assert.match(retention, /locked by platform configuration/, "retention explains platform locks");
assert.doesNotMatch(retention, /<textarea/, "retention does not use raw text or JSON as its primary editor");
assert.match(confirmDialog, /PlatformInterfaceIdentityProps/, "confirm dialogs accept stable interface help identities");
assert.match(confirmDialog, /helpContextId=\{helpContextId\}/, "confirm dialogs propagate their explicit help context");
assert.match(credentials, /<ActionBlockerHint/, "credentials render the shared actionable blocker");
assert.match(credentials, /contextId: "access\.credentials"/, "credentials expose stable admin documentation");
@@ -50,6 +57,7 @@ assert.match(helpMenu, /helpContextForTarget\(routeHelpContext, event\.target, m
assert.match(helpContext, /function moduleRouteContext/, "context help covers contributed module routes");
assert.match(helpContext, /function sectionContext/, "context help covers contributed administration and settings sections");
assert.match(helpContext, /data-help-context-id/, "context help honors explicit control metadata");
assert.match(helpContext, /explicit\.dataset\.helpModuleId \|\| base\.moduleId/, "context help honors an explicit documentation owner");
assert.match(layoutStyles, /@media \(max-width: 600px\)[\s\S]*\.app-main \{[\s\S]*grid-template-rows: 104px 51px minmax\(0, 1fr\);/, "the narrow shell reserves two non-overlapping titlebar rows");
assert.match(layoutStyles, /@media \(max-width: 600px\)[\s\S]*\.titlebar-context-selectors \{[\s\S]*overflow-x: auto;/, "narrow context selectors remain reachable without covering titlebar actions");
assert.match(layoutStyles, /@media \(max-width: 600px\)[\s\S]*\.account-pill span \{[\s\S]*display: none;/, "narrow account controls retain the icon while removing collision-prone text");
+2 -1
View File
@@ -7,12 +7,13 @@ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & PlatformInte
disabledReason?: ReactNode;
};
export default function Button({ variant = "secondary", className = "", disabledReason, disabled, interfaceId, helpContextId, helpTopicId, children, ...props }: ButtonProps) {
export default function Button({ variant = "secondary", className = "", disabledReason, disabled, interfaceId, helpContextId, helpModuleId, helpTopicId, children, ...props }: ButtonProps) {
const button = (
<button
data-help-scope="action"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={typeof children === "string" ? children : undefined}
className={`btn btn-${variant} ${className}`}
+2 -1
View File
@@ -39,7 +39,7 @@ function writeCollapseState(storageKey: string | null, collapsed: boolean): void
// localStorage may be unavailable in private or restricted contexts.
}}
export default function Card({ title, children, actions, collapsible = false, collapseKey, persistCollapse = true, interfaceId, helpContextId, helpTopicId }: CardProps) {
export default function Card({ title, children, actions, collapsible = false, collapseKey, persistCollapse = true, interfaceId, helpContextId, helpModuleId, helpTopicId }: CardProps) {
const { translateText } = usePlatformLanguage();
const storageKey = resolveCollapseStorageKey(collapsible, persistCollapse, collapseKey, title);
const [collapseState, setCollapseState] = useState(() => ({ storageKey, collapsed: readCollapseState(storageKey) }));
@@ -65,6 +65,7 @@ export default function Card({ title, children, actions, collapsible = false, co
data-help-scope="interface"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={typeof title === "string" ? title : undefined}
>
+11 -2
View File
@@ -1,10 +1,11 @@
import Button from "./Button";
import Dialog from "./Dialog";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
export type ConfirmDialogTone = "default" | "danger";
export type ConfirmDialogProps = {
export type ConfirmDialogProps = PlatformInterfaceIdentityProps & {
open: boolean;
title: string;
message: string;
@@ -24,6 +25,10 @@ export default function ConfirmDialog({
cancelLabel = "i18n:govoplan-core.cancel.77dfd213",
tone = "default",
busy = false,
interfaceId,
helpContextId,
helpModuleId,
helpTopicId,
onConfirm,
onCancel
}: ConfirmDialogProps) {
@@ -38,11 +43,15 @@ export default function ConfirmDialog({
closeOnBackdrop={!busy}
closeDisabled={busy}
showCloseButton
interfaceId={interfaceId}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
onClose={onCancel}
footer={
<>
<Button onClick={onCancel} disabled={busy}>{translateText(cancelLabel)}</Button>
<Button variant={tone === "danger" ? "danger" : "primary"} onClick={onConfirm} disabled={busy}>{busy ? translateText("i18n:govoplan-core.working.13b7bfca") : translateText(confirmLabel)}</Button>
<Button helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} variant={tone === "danger" ? "danger" : "primary"} onClick={onConfirm} disabled={busy}>{busy ? translateText("i18n:govoplan-core.working.13b7bfca") : translateText(confirmLabel)}</Button>
</>
}>
+6 -3
View File
@@ -50,7 +50,7 @@ function combineDateTime(date: string, time: string): string {
return `${date || dateString(new Date())}T${time || "00:00"}`;
}
export function DateField({ value, onChange, min, max, disabled, className = "", placeholder = "i18n:govoplan-core.yyyy_mm_dd.d3f8f7b8", interfaceId, helpContextId, helpTopicId, ...props }: BaseProps) {
export function DateField({ value, onChange, min, max, disabled, className = "", placeholder = "i18n:govoplan-core.yyyy_mm_dd.d3f8f7b8", interfaceId, helpContextId, helpModuleId, helpTopicId, ...props }: BaseProps) {
const selectedDate = parseDate(value);
const [open, setOpen] = useState(false);
const [visibleMonth, setVisibleMonth] = useState<Date>(() => selectedDate ?? new Date());
@@ -100,6 +100,7 @@ export function DateField({ value, onChange, min, max, disabled, className = "",
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={props["aria-label"] ?? placeholder}
>
@@ -154,7 +155,7 @@ export function DateField({ value, onChange, min, max, disabled, className = "",
}
export function TimeField({ value, onChange, min, max, className = "", placeholder = "i18n:govoplan-core.hh_mm.a4c7ee9b", interfaceId, helpContextId, helpTopicId, ...props }: BaseProps) {
export function TimeField({ value, onChange, min, max, className = "", placeholder = "i18n:govoplan-core.hh_mm.a4c7ee9b", interfaceId, helpContextId, helpModuleId, helpTopicId, ...props }: BaseProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const input = inputRef.current;
@@ -172,6 +173,7 @@ export function TimeField({ value, onChange, min, max, className = "", placehold
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={props["aria-label"] ?? placeholder}
>
@@ -190,7 +192,7 @@ export function TimeField({ value, onChange, min, max, className = "", placehold
}
export function DateTimeField({ value, onChange, min, max, disabled, className = "", interfaceId, helpContextId, helpTopicId, ...props }: BaseProps) {
export function DateTimeField({ value, onChange, min, max, disabled, className = "", interfaceId, helpContextId, helpModuleId, helpTopicId, ...props }: BaseProps) {
const parts = datePartsFromDateTime(value);
const minParts = datePartsFromDateTime(min || "");
const maxParts = datePartsFromDateTime(max || "");
@@ -209,6 +211,7 @@ export function DateTimeField({ value, onChange, min, max, disabled, className =
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={props["aria-label"]}
>
+2
View File
@@ -60,6 +60,7 @@ export default function Dialog({
backdropStyle,
interfaceId,
helpContextId,
helpModuleId,
helpTopicId
}: DialogProps) {
const titleId = useId();
@@ -141,6 +142,7 @@ export default function Dialog({
data-help-scope="dialog"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={typeof title === "string" ? title : undefined}
aria-labelledby={titleId}
+2 -1
View File
@@ -12,7 +12,7 @@ type FormFieldProps = PlatformInterfaceIdentityProps & {
children: ReactNode;
};
export default function FormField({ label, help, documentation, children, interfaceId, helpContextId, helpTopicId }: FormFieldProps) {
export default function FormField({ label, help, documentation, children, interfaceId, helpContextId, helpModuleId, helpTopicId }: FormFieldProps) {
const { translateText } = usePlatformLanguage();
const renderedLabel = typeof label === "string" ? translateText(label) : label;
return (
@@ -21,6 +21,7 @@ export default function FormField({ label, help, documentation, children, interf
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId ?? documentation?.contextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId ?? documentation?.topicId}
data-help-documentation-type={documentation?.documentationType}
data-help-key={typeof label === "string" ? label : undefined}
@@ -99,6 +99,7 @@ export default function SearchableSelect({
className = "",
interfaceId,
helpContextId,
helpModuleId,
helpTopicId
}: SearchableSelectProps) {
const { translateText } = usePlatformLanguage();
@@ -301,6 +302,7 @@ export default function SearchableSelect({
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={ariaLabel}
onBlur={closeOnFocusLeave}
+2 -1
View File
@@ -14,7 +14,7 @@ type ToggleSwitchProps = PlatformInterfaceIdentityProps & {
help?: ReactNode;
};
export default function ToggleSwitch({ label, activeLabel, inactiveLabel, checked, onChange, disabled = false, help, interfaceId, helpContextId, helpTopicId }: ToggleSwitchProps) {
export default function ToggleSwitch({ label, activeLabel, inactiveLabel, checked, onChange, disabled = false, help, interfaceId, helpContextId, helpModuleId, helpTopicId }: ToggleSwitchProps) {
const { translateText } = usePlatformLanguage();
const hasStateLabels = activeLabel !== undefined || inactiveLabel !== undefined;
const renderedLabel = typeof label === "string" ? translateText(label) : label;
@@ -27,6 +27,7 @@ export default function ToggleSwitch({ label, activeLabel, inactiveLabel, checke
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={typeof label === "string" ? label : undefined}
>
@@ -29,6 +29,7 @@ export default function AdminPageLayout({
className = "",
interfaceId,
helpContextId,
helpModuleId,
helpTopicId
}: Props) {
const { translateText } = usePlatformLanguage();
@@ -38,6 +39,7 @@ export default function AdminPageLayout({
data-help-scope="page"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-documentation-type="admin"
data-help-key={title}
@@ -47,6 +47,7 @@ export default function EmailAddressInput({
showAddButton,
interfaceId,
helpContextId,
helpModuleId,
helpTopicId
}: EmailAddressInputProps) {
const { translateText } = usePlatformLanguage();
@@ -208,6 +209,7 @@ export default function EmailAddressInput({
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-key={emailPlaceholder}
>
@@ -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>
+2
View File
@@ -426,6 +426,8 @@ export type PlatformInterfaceIdentityProps = {
interfaceId?: string;
/** Stable contextual-help identifier associated with the control. */
helpContextId?: string;
/** Documentation-owning module when it differs from the containing surface. */
helpModuleId?: string;
/** Optional stable documentation/help topic associated with the control. */
helpTopicId?: string;
};