feat: add exact retention help contexts
This commit is contained in:
@@ -46,6 +46,8 @@ than adding custom `F1` listeners:
|
|||||||
- `interfaceId` identifies a durable UI surface or action.
|
- `interfaceId` identifies a durable UI surface or action.
|
||||||
- `helpContextId` identifies a documentation context when it differs from the
|
- `helpContextId` identifies a documentation context when it differs from the
|
||||||
interface identity.
|
interface identity.
|
||||||
|
- `helpModuleId` identifies the documentation-owning module when a shared
|
||||||
|
control is embedded in another module's page.
|
||||||
- `helpTopicId` links directly to a module-owned documentation topic.
|
- `helpTopicId` links directly to a module-owned documentation topic.
|
||||||
- translated label keys provide deterministic field identities for ordinary
|
- translated label keys provide deterministic field identities for ordinary
|
||||||
`FormField`, `ToggleSwitch`, search, date/time, email, button, dialog, and card
|
`FormField`, `ToggleSwitch`, search, date/time, email, button, dialog, and card
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ should still add exact `metadata.help_contexts` entries for consequential,
|
|||||||
unfamiliar, policy-controlled, destructive, security-sensitive, or legally
|
unfamiliar, policy-controlled, destructive, security-sensitive, or legally
|
||||||
meaningful fields and actions.
|
meaningful fields and actions.
|
||||||
|
|
||||||
|
The shared retention-policy editor exposes explicit contexts for each stored
|
||||||
|
data category, audit-detail control, lower-level override switch, target
|
||||||
|
selector, reload, and save action. The Policy module owns the matching German
|
||||||
|
administrator guidance. Retention execution surfaces use separate contexts for
|
||||||
|
dry-run, destructive apply, confirmation, and outcome review so F1 opens the
|
||||||
|
consequence and recovery guidance closest to the focused control.
|
||||||
|
Shared controls may set `helpModuleId` when their documentation owner differs
|
||||||
|
from the containing page; the retention editor uses this to resolve Policy help
|
||||||
|
from both administration and Campaign surfaces.
|
||||||
|
|
||||||
The generated `help_review_candidates` list is therefore a content-depth queue,
|
The generated `help_review_candidates` list is therefore a content-depth queue,
|
||||||
not a list of controls on which F1 cannot work. It should prioritize:
|
not a list of controls on which F1 cannot work. It should prioritize:
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const read = (path) => readFileSync(resolve(webuiRoot, path), "utf8");
|
|||||||
|
|
||||||
const settings = read("src/features/settings/SettingsPage.tsx");
|
const settings = read("src/features/settings/SettingsPage.tsx");
|
||||||
const retention = read("src/features/privacy/RetentionPolicyManagement.tsx");
|
const retention = read("src/features/privacy/RetentionPolicyManagement.tsx");
|
||||||
|
const confirmDialog = read("src/components/ConfirmDialog.tsx");
|
||||||
const credentials = read("src/components/CredentialEnvelopeManager.tsx");
|
const credentials = read("src/components/CredentialEnvelopeManager.tsx");
|
||||||
const iconRail = read("src/layout/IconRail.tsx");
|
const iconRail = read("src/layout/IconRail.tsx");
|
||||||
const moduleLoadBoundary = read("src/components/ModuleLoadBoundary.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, /<ActionBlockerHint/, "retention renders the shared actionable blocker");
|
||||||
assert.match(retention, /contextId: "privacy\.retention"/, "retention exposes stable admin documentation");
|
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.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.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, /<ActionBlockerHint/, "credentials render the shared actionable blocker");
|
||||||
assert.match(credentials, /contextId: "access\.credentials"/, "credentials expose stable admin documentation");
|
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 moduleRouteContext/, "context help covers contributed module routes");
|
||||||
assert.match(helpContext, /function sectionContext/, "context help covers contributed administration and settings sections");
|
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, /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]*\.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]*\.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");
|
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");
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & PlatformInte
|
|||||||
disabledReason?: ReactNode;
|
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 = (
|
const button = (
|
||||||
<button
|
<button
|
||||||
data-help-scope="action"
|
data-help-scope="action"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={typeof children === "string" ? children : undefined}
|
data-help-key={typeof children === "string" ? children : undefined}
|
||||||
className={`btn btn-${variant} ${className}`}
|
className={`btn btn-${variant} ${className}`}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function writeCollapseState(storageKey: string | null, collapsed: boolean): void
|
|||||||
// localStorage may be unavailable in private or restricted contexts.
|
// 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 { translateText } = usePlatformLanguage();
|
||||||
const storageKey = resolveCollapseStorageKey(collapsible, persistCollapse, collapseKey, title);
|
const storageKey = resolveCollapseStorageKey(collapsible, persistCollapse, collapseKey, title);
|
||||||
const [collapseState, setCollapseState] = useState(() => ({ storageKey, collapsed: readCollapseState(storageKey) }));
|
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-help-scope="interface"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={typeof title === "string" ? title : undefined}
|
data-help-key={typeof title === "string" ? title : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import Dialog from "./Dialog";
|
import Dialog from "./Dialog";
|
||||||
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||||
|
import type { PlatformInterfaceIdentityProps } from "../types";
|
||||||
|
|
||||||
export type ConfirmDialogTone = "default" | "danger";
|
export type ConfirmDialogTone = "default" | "danger";
|
||||||
|
|
||||||
export type ConfirmDialogProps = {
|
export type ConfirmDialogProps = PlatformInterfaceIdentityProps & {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
@@ -24,6 +25,10 @@ export default function ConfirmDialog({
|
|||||||
cancelLabel = "i18n:govoplan-core.cancel.77dfd213",
|
cancelLabel = "i18n:govoplan-core.cancel.77dfd213",
|
||||||
tone = "default",
|
tone = "default",
|
||||||
busy = false,
|
busy = false,
|
||||||
|
interfaceId,
|
||||||
|
helpContextId,
|
||||||
|
helpModuleId,
|
||||||
|
helpTopicId,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel
|
onCancel
|
||||||
}: ConfirmDialogProps) {
|
}: ConfirmDialogProps) {
|
||||||
@@ -38,11 +43,15 @@ export default function ConfirmDialog({
|
|||||||
closeOnBackdrop={!busy}
|
closeOnBackdrop={!busy}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
showCloseButton
|
showCloseButton
|
||||||
|
interfaceId={interfaceId}
|
||||||
|
helpContextId={helpContextId}
|
||||||
|
helpModuleId={helpModuleId}
|
||||||
|
helpTopicId={helpTopicId}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button onClick={onCancel} disabled={busy}>{translateText(cancelLabel)}</Button>
|
<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>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function combineDateTime(date: string, time: string): string {
|
|||||||
return `${date || dateString(new Date())}T${time || "00:00"}`;
|
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 selectedDate = parseDate(value);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [visibleMonth, setVisibleMonth] = useState<Date>(() => selectedDate ?? new Date());
|
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-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={props["aria-label"] ?? placeholder}
|
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);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const input = inputRef.current;
|
const input = inputRef.current;
|
||||||
@@ -172,6 +173,7 @@ export function TimeField({ value, onChange, min, max, className = "", placehold
|
|||||||
data-help-scope="field"
|
data-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={props["aria-label"] ?? placeholder}
|
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 parts = datePartsFromDateTime(value);
|
||||||
const minParts = datePartsFromDateTime(min || "");
|
const minParts = datePartsFromDateTime(min || "");
|
||||||
const maxParts = datePartsFromDateTime(max || "");
|
const maxParts = datePartsFromDateTime(max || "");
|
||||||
@@ -209,6 +211,7 @@ export function DateTimeField({ value, onChange, min, max, disabled, className =
|
|||||||
data-help-scope="field"
|
data-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={props["aria-label"]}
|
data-help-key={props["aria-label"]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function Dialog({
|
|||||||
backdropStyle,
|
backdropStyle,
|
||||||
interfaceId,
|
interfaceId,
|
||||||
helpContextId,
|
helpContextId,
|
||||||
|
helpModuleId,
|
||||||
helpTopicId
|
helpTopicId
|
||||||
}: DialogProps) {
|
}: DialogProps) {
|
||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
@@ -141,6 +142,7 @@ export default function Dialog({
|
|||||||
data-help-scope="dialog"
|
data-help-scope="dialog"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={typeof title === "string" ? title : undefined}
|
data-help-key={typeof title === "string" ? title : undefined}
|
||||||
aria-labelledby={titleId}
|
aria-labelledby={titleId}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type FormFieldProps = PlatformInterfaceIdentityProps & {
|
|||||||
children: ReactNode;
|
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 { translateText } = usePlatformLanguage();
|
||||||
const renderedLabel = typeof label === "string" ? translateText(label) : label;
|
const renderedLabel = typeof label === "string" ? translateText(label) : label;
|
||||||
return (
|
return (
|
||||||
@@ -21,6 +21,7 @@ export default function FormField({ label, help, documentation, children, interf
|
|||||||
data-help-scope="field"
|
data-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId ?? documentation?.contextId}
|
data-help-context-id={helpContextId ?? documentation?.contextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId ?? documentation?.topicId}
|
data-help-topic-id={helpTopicId ?? documentation?.topicId}
|
||||||
data-help-documentation-type={documentation?.documentationType}
|
data-help-documentation-type={documentation?.documentationType}
|
||||||
data-help-key={typeof label === "string" ? label : undefined}
|
data-help-key={typeof label === "string" ? label : undefined}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export default function SearchableSelect({
|
|||||||
className = "",
|
className = "",
|
||||||
interfaceId,
|
interfaceId,
|
||||||
helpContextId,
|
helpContextId,
|
||||||
|
helpModuleId,
|
||||||
helpTopicId
|
helpTopicId
|
||||||
}: SearchableSelectProps) {
|
}: SearchableSelectProps) {
|
||||||
const { translateText } = usePlatformLanguage();
|
const { translateText } = usePlatformLanguage();
|
||||||
@@ -301,6 +302,7 @@ export default function SearchableSelect({
|
|||||||
data-help-scope="field"
|
data-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={ariaLabel}
|
data-help-key={ariaLabel}
|
||||||
onBlur={closeOnFocusLeave}
|
onBlur={closeOnFocusLeave}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type ToggleSwitchProps = PlatformInterfaceIdentityProps & {
|
|||||||
help?: ReactNode;
|
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 { translateText } = usePlatformLanguage();
|
||||||
const hasStateLabels = activeLabel !== undefined || inactiveLabel !== undefined;
|
const hasStateLabels = activeLabel !== undefined || inactiveLabel !== undefined;
|
||||||
const renderedLabel = typeof label === "string" ? translateText(label) : label;
|
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-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={typeof label === "string" ? label : undefined}
|
data-help-key={typeof label === "string" ? label : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default function AdminPageLayout({
|
|||||||
className = "",
|
className = "",
|
||||||
interfaceId,
|
interfaceId,
|
||||||
helpContextId,
|
helpContextId,
|
||||||
|
helpModuleId,
|
||||||
helpTopicId
|
helpTopicId
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { translateText } = usePlatformLanguage();
|
const { translateText } = usePlatformLanguage();
|
||||||
@@ -38,6 +39,7 @@ export default function AdminPageLayout({
|
|||||||
data-help-scope="page"
|
data-help-scope="page"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-documentation-type="admin"
|
data-help-documentation-type="admin"
|
||||||
data-help-key={title}
|
data-help-key={title}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function EmailAddressInput({
|
|||||||
showAddButton,
|
showAddButton,
|
||||||
interfaceId,
|
interfaceId,
|
||||||
helpContextId,
|
helpContextId,
|
||||||
|
helpModuleId,
|
||||||
helpTopicId
|
helpTopicId
|
||||||
}: EmailAddressInputProps) {
|
}: EmailAddressInputProps) {
|
||||||
const { translateText } = usePlatformLanguage();
|
const { translateText } = usePlatformLanguage();
|
||||||
@@ -208,6 +209,7 @@ export default function EmailAddressInput({
|
|||||||
data-help-scope="field"
|
data-help-scope="field"
|
||||||
data-interface-id={interfaceId}
|
data-interface-id={interfaceId}
|
||||||
data-help-context-id={helpContextId}
|
data-help-context-id={helpContextId}
|
||||||
|
data-help-module-id={helpModuleId}
|
||||||
data-help-topic-id={helpTopicId}
|
data-help-topic-id={helpTopicId}
|
||||||
data-help-key={emailPlaceholder}
|
data-help-key={emailPlaceholder}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ type DayKey =
|
|||||||
type FieldDefinition = {
|
type FieldDefinition = {
|
||||||
key: PrivacyRetentionPolicyFieldKey;
|
key: PrivacyRetentionPolicyFieldKey;
|
||||||
label: string;
|
label: string;
|
||||||
|
helpContextId: string;
|
||||||
kind: "raw-json" | "days" | "audit";
|
kind: "raw-json" | "days" | "audit";
|
||||||
systemOnly?: boolean;
|
systemOnly?: boolean;
|
||||||
};
|
};
|
||||||
@@ -101,13 +102,13 @@ const defaultPrivacyPolicy: PrivacyRetentionPolicy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fieldDefinitions: FieldDefinition[] = [
|
const fieldDefinitions: FieldDefinition[] = [
|
||||||
{ key: "store_raw_campaign_json", label: "i18n:govoplan-core.raw_campaign_json.4ca2265b", kind: "raw-json" },
|
{ 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", kind: "days" },
|
{ 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", 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", 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", kind: "days", systemOnly: true },
|
{ 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", kind: "days" },
|
{ 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", kind: "audit" }];
|
{ key: "audit_detail_level", label: "i18n:govoplan-core.audit_detail_level.b0565260", helpContextId: "policy.retention.field.audit-detail-level", kind: "audit" }];
|
||||||
|
|
||||||
|
|
||||||
export function RetentionPolicyScopeManager({
|
export function RetentionPolicyScopeManager({
|
||||||
@@ -148,11 +149,13 @@ export function RetentionPolicyScopeManager({
|
|||||||
{targetSelectionRequired &&
|
{targetSelectionRequired &&
|
||||||
<Card
|
<Card
|
||||||
title={i18nMessage("i18n:govoplan-core.value_scope", { value0: targetLabel })}
|
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" />}
|
actions={<DocumentationHelpLink reference={RETENTION_DOCUMENTATION} label="i18n:govoplan-core.open_admin_documentation.6adbdae3" />}
|
||||||
>
|
>
|
||||||
<div className="retention-policy-target-row">
|
<div className="retention-policy-target-row">
|
||||||
<FormField label={targetLabel}>
|
<FormField label={targetLabel} helpContextId="policy.retention.target" helpModuleId="policy">
|
||||||
<select value={selectedTargetId} disabled={!hasSelectableTarget} onChange={(event) => setSelectedTargetId(event.target.value)}>
|
<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>}
|
{!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>)}
|
{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>
|
</select>
|
||||||
@@ -342,11 +345,13 @@ export function RetentionPolicyEditor({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={title}
|
title={title}
|
||||||
|
helpContextId="policy.retention"
|
||||||
|
helpModuleId="policy"
|
||||||
actions={
|
actions={
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<DocumentationHelpLink reference={RETENTION_DOCUMENTATION} label="i18n:govoplan-core.open_admin_documentation.6adbdae3" />
|
<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 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 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.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>
|
</div>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
@@ -401,6 +406,8 @@ export function RetentionPolicyEditor({
|
|||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={localAllowsLower(field.key)}
|
checked={localAllowsLower(field.key)}
|
||||||
disabled={disabled || fieldLocked}
|
disabled={disabled || fieldLocked}
|
||||||
|
helpContextId="policy.retention.field.allow-lower-level-limits"
|
||||||
|
helpModuleId="policy"
|
||||||
onChange={(checked) => setAllowLowerLevelLimit(field.key, checked)}
|
onChange={(checked) => setAllowLowerLevelLimit(field.key, checked)}
|
||||||
label="i18n:govoplan-core.allow_override.ffa6e9a0" /> :
|
label="i18n:govoplan-core.allow_override.ffa6e9a0" /> :
|
||||||
undefined} />);
|
undefined} />);
|
||||||
@@ -519,18 +526,19 @@ parentPolicy: PrivacyRetentionPolicy | null)
|
|||||||
{
|
{
|
||||||
if (field.kind === "raw-json") {
|
if (field.kind === "raw-json") {
|
||||||
if (isSystem) {
|
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") {
|
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;
|
const parentDayLimit = !isSystem && parentPolicy ? parentPolicy[field.key as DayKey] : null;
|
||||||
return (
|
return (
|
||||||
<RetentionDaysField
|
<RetentionDaysField
|
||||||
value={isSystem ? fullPolicy[field.key as DayKey] : patchPolicy[field.key as DayKey]}
|
value={isSystem ? fullPolicy[field.key as DayKey] : patchPolicy[field.key as DayKey]}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
helpContextId={field.helpContextId}
|
||||||
placeholder={isSystem ? "i18n:govoplan-core.unlimited.b8bef37b" : "i18n:govoplan-core.inherit.18f99833"}
|
placeholder={isSystem ? "i18n:govoplan-core.unlimited.b8bef37b" : "i18n:govoplan-core.inherit.18f99833"}
|
||||||
max={parentDayLimit}
|
max={parentDayLimit}
|
||||||
onChange={(value) => setRetentionDays(field.key as DayKey, value)} />);
|
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 (
|
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="store">i18n:govoplan-core.store.0d8a7046</option>
|
||||||
<option value="disabled">i18n:govoplan-core.disable_storage.07c44d8b</option>
|
<option value="disabled">i18n:govoplan-core.disable_storage.07c44d8b</option>
|
||||||
</select>);
|
</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 (
|
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="inherit">i18n:govoplan-core.inherit.18f99833</option>
|
||||||
<option value="keep" disabled={!parentStoresRawJson}>i18n:govoplan-core.store_if_parent_allows.36dcb07b</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>
|
<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) {
|
function handleChange(nextValue: string) {
|
||||||
const trimmed = nextValue.trim();
|
const trimmed = nextValue.trim();
|
||||||
if (trimmed === "" || max === null || max === undefined) {
|
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);
|
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 {
|
function optionDisabled(option: PrivacyRetentionPolicy["audit_detail_level"]): boolean {
|
||||||
return privacyRetentionAuditDetailOptionDisabled(option, parentValue);
|
return privacyRetentionAuditDetailOptionDisabled(option, parentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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>}
|
{includeInherit && <option value="inherit">i18n:govoplan-core.inherit.18f99833</option>}
|
||||||
<option value="full" disabled={optionDisabled("full")}>i18n:govoplan-core.full.10b28a8c</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>
|
<option value="redacted" disabled={optionDisabled("redacted")}>i18n:govoplan-core.redacted.8780785a</option>
|
||||||
|
|||||||
@@ -426,6 +426,8 @@ export type PlatformInterfaceIdentityProps = {
|
|||||||
interfaceId?: string;
|
interfaceId?: string;
|
||||||
/** Stable contextual-help identifier associated with the control. */
|
/** Stable contextual-help identifier associated with the control. */
|
||||||
helpContextId?: string;
|
helpContextId?: string;
|
||||||
|
/** Documentation-owning module when it differs from the containing surface. */
|
||||||
|
helpModuleId?: string;
|
||||||
/** Optional stable documentation/help topic associated with the control. */
|
/** Optional stable documentation/help topic associated with the control. */
|
||||||
helpTopicId?: string;
|
helpTopicId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user