Localize shared blocker guidance
This commit is contained in:
@@ -339,6 +339,8 @@ Every new or changed admin/configuration surface should answer:
|
|||||||
- Does the screen explain disabled actions and failed validation in plain
|
- Does the screen explain disabled actions and failed validation in plain
|
||||||
language?
|
language?
|
||||||
- Does it say who can fix a blocker and where?
|
- Does it say who can fix a blocker and where?
|
||||||
|
- Does a module-localized blocker pass its translated row labels through the
|
||||||
|
shared `ActionBlockerHint` contract instead of reproducing the component?
|
||||||
- Does it reuse existing core patterns for wizard steps, problem lists, modals,
|
- Does it reuse existing core patterns for wizard steps, problem lists, modals,
|
||||||
help, and review?
|
help, and review?
|
||||||
- Is there a review or preflight step before broad, destructive, or risky
|
- Is there a review or preflight step before broad, destructive, or risky
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"test:metric-card": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/metric-card.test.js",
|
"test:metric-card": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/metric-card.test.js",
|
||||||
"test:people-picker": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/people-picker.test.js",
|
"test:people-picker": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/people-picker.test.js",
|
||||||
"test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js",
|
"test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js",
|
||||||
|
"test:action-blocker": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/action-blocker-hint.test.js",
|
||||||
"test:selection-list": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/selection-list.test.js",
|
"test:selection-list": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/selection-list.test.js",
|
||||||
"test:wysiwyg-editor": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/wysiwyg-editor-utils.test.js"
|
"test:wysiwyg-editor": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/wysiwyg-editor-utils.test.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,17 +11,30 @@ export type ActionBlockerReason = {
|
|||||||
technicalDetails?: ReactNode;
|
technicalDetails?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionBlockerLabels = {
|
||||||
|
requiredAction?: ReactNode;
|
||||||
|
actor?: ReactNode;
|
||||||
|
target?: ReactNode;
|
||||||
|
technicalDetails?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
type ActionBlockerHintProps = {
|
type ActionBlockerHintProps = {
|
||||||
reason: ActionBlockerReason;
|
reason: ActionBlockerReason;
|
||||||
tone?: "info" | "warning" | "danger";
|
tone?: "info" | "warning" | "danger";
|
||||||
className?: string;
|
className?: string;
|
||||||
|
labels?: ActionBlockerLabels;
|
||||||
};
|
};
|
||||||
|
|
||||||
function joinClasses(...classes: Array<string | undefined | false>) {
|
function joinClasses(...classes: Array<string | undefined | false>) {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActionBlockerHint({ reason, tone = "warning", className = "" }: ActionBlockerHintProps) {
|
export default function ActionBlockerHint({
|
||||||
|
reason,
|
||||||
|
tone = "warning",
|
||||||
|
className = "",
|
||||||
|
labels = {}
|
||||||
|
}: ActionBlockerHintProps) {
|
||||||
const Icon = tone === "info" ? Info : AlertTriangle;
|
const Icon = tone === "info" ? Info : AlertTriangle;
|
||||||
const hasActionRows = Boolean(reason.requiredAction || reason.actor || reason.target);
|
const hasActionRows = Boolean(reason.requiredAction || reason.actor || reason.target);
|
||||||
|
|
||||||
@@ -35,26 +48,26 @@ export default function ActionBlockerHint({ reason, tone = "warning", className
|
|||||||
<dl>
|
<dl>
|
||||||
{reason.requiredAction && (
|
{reason.requiredAction && (
|
||||||
<>
|
<>
|
||||||
<dt>Required action</dt>
|
<dt>{labels.requiredAction ?? "Required action"}</dt>
|
||||||
<dd>{reason.requiredAction}</dd>
|
<dd>{reason.requiredAction}</dd>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{reason.actor && (
|
{reason.actor && (
|
||||||
<>
|
<>
|
||||||
<dt>Who can fix it</dt>
|
<dt>{labels.actor ?? "Who can fix it"}</dt>
|
||||||
<dd>{reason.actor}</dd>
|
<dd>{reason.actor}</dd>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{reason.target && (
|
{reason.target && (
|
||||||
<>
|
<>
|
||||||
<dt>Where to go</dt>
|
<dt>{labels.target ?? "Where to go"}</dt>
|
||||||
<dd>{reason.target}</dd>
|
<dd>{reason.target}</dd>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
)}
|
)}
|
||||||
{reason.technicalDetails && (
|
{reason.technicalDetails && (
|
||||||
<AdvancedOptionsPanel title="Technical details" className="action-blocker-technical">
|
<AdvancedOptionsPanel title={labels.technicalDetails ?? "Technical details"} className="action-blocker-technical">
|
||||||
<div>{reason.technicalDetails}</div>
|
<div>{reason.technicalDetails}</div>
|
||||||
</AdvancedOptionsPanel>
|
</AdvancedOptionsPanel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ export * from "./i18n/LanguageContext";
|
|||||||
|
|
||||||
export { PermissionBoundary, ResourceAccessBoundary } from "./components/AccessBoundary";
|
export { PermissionBoundary, ResourceAccessBoundary } from "./components/AccessBoundary";
|
||||||
export { default as ActionBlockerHint } from "./components/ActionBlockerHint";
|
export { default as ActionBlockerHint } from "./components/ActionBlockerHint";
|
||||||
export type { ActionBlockerReason } from "./components/ActionBlockerHint";
|
export type { ActionBlockerLabels, ActionBlockerReason } from "./components/ActionBlockerHint";
|
||||||
export { default as AdvancedOptionsPanel } from "./components/AdvancedOptionsPanel";
|
export { default as AdvancedOptionsPanel } from "./components/AdvancedOptionsPanel";
|
||||||
export { default as AdminIconButton } from "./components/admin/AdminIconButton";
|
export { default as AdminIconButton } from "./components/admin/AdminIconButton";
|
||||||
export type { AdminIconButtonProps } from "./components/admin/AdminIconButton";
|
export type { AdminIconButtonProps } from "./components/admin/AdminIconButton";
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
function assert(condition: unknown, message = "assertion failed"): void {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import ActionBlockerHint from "../src/components/ActionBlockerHint";
|
||||||
|
|
||||||
|
const defaultMarkup = renderToStaticMarkup(
|
||||||
|
<ActionBlockerHint
|
||||||
|
reason={{
|
||||||
|
summary: "Delivery is blocked.",
|
||||||
|
requiredAction: "Resolve the recipient error.",
|
||||||
|
actor: "Campaign editor",
|
||||||
|
target: "Recipients"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
assert(defaultMarkup.includes("Required action"), "the established default labels remain available");
|
||||||
|
assert(defaultMarkup.includes("Who can fix it"), "the established actor label remains available");
|
||||||
|
assert(defaultMarkup.includes("Where to go"), "the established target label remains available");
|
||||||
|
|
||||||
|
const localizedMarkup = renderToStaticMarkup(
|
||||||
|
<ActionBlockerHint
|
||||||
|
tone="danger"
|
||||||
|
labels={{
|
||||||
|
requiredAction: "i18n:required-action",
|
||||||
|
actor: "i18n:actor",
|
||||||
|
target: "i18n:target",
|
||||||
|
technicalDetails: "i18n:technical-details"
|
||||||
|
}}
|
||||||
|
reason={{
|
||||||
|
summary: "i18n:summary",
|
||||||
|
requiredAction: "i18n:action-copy",
|
||||||
|
actor: "i18n:actor-copy",
|
||||||
|
target: "i18n:target-copy",
|
||||||
|
technicalDetails: "i18n:diagnostics"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
for (const token of [
|
||||||
|
"i18n:required-action",
|
||||||
|
"i18n:actor",
|
||||||
|
"i18n:target",
|
||||||
|
"i18n:technical-details",
|
||||||
|
"i18n:diagnostics"
|
||||||
|
]) {
|
||||||
|
assert(localizedMarkup.includes(token), `${token} is rendered through the shared blocker contract`);
|
||||||
|
}
|
||||||
|
assert(localizedMarkup.includes("tone-danger"), "the consequence tone remains explicit");
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"tests/action-blocker-hint.test.tsx",
|
||||||
"tests/data-grid-actions.test.tsx",
|
"tests/data-grid-actions.test.tsx",
|
||||||
"tests/data-grid-sizing.test.ts",
|
"tests/data-grid-sizing.test.ts",
|
||||||
"tests/dialog-focus.test.tsx",
|
"tests/dialog-focus.test.tsx",
|
||||||
@@ -30,6 +31,8 @@
|
|||||||
"tests/selection-list.test.tsx",
|
"tests/selection-list.test.tsx",
|
||||||
"tests/wysiwyg-editor-utils.test.ts",
|
"tests/wysiwyg-editor-utils.test.ts",
|
||||||
"src/components/CredentialPanel.tsx",
|
"src/components/CredentialPanel.tsx",
|
||||||
|
"src/components/ActionBlockerHint.tsx",
|
||||||
|
"src/components/AdvancedOptionsPanel.tsx",
|
||||||
"src/components/email/EmailAddressInput.tsx",
|
"src/components/email/EmailAddressInput.tsx",
|
||||||
"src/components/PasswordField.tsx",
|
"src/components/PasswordField.tsx",
|
||||||
"src/components/MessageDisplayPanel.tsx",
|
"src/components/MessageDisplayPanel.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user