mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
i18n(policies): route policy UI strings through i18n (English only) (#6628)
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e88d22d2fc
commit
cc1235bbf2
@@ -5865,6 +5865,106 @@ deleteConfirmTitle = "Delete {{label}} policy?"
|
|||||||
deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected."
|
deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected."
|
||||||
deleteConfirmTitle = "Delete {{label}} policy?"
|
deleteConfirmTitle = "Delete {{label}} policy?"
|
||||||
|
|
||||||
|
[policies.catalog]
|
||||||
|
compliance = "Compliance"
|
||||||
|
ingestion = "Ingestion"
|
||||||
|
retention = "Retention"
|
||||||
|
routing = "Routing"
|
||||||
|
security = "Security"
|
||||||
|
|
||||||
|
[policies.detail]
|
||||||
|
editSettings = "Edit Settings"
|
||||||
|
enforces = "Enforces"
|
||||||
|
managedByOrg = "Managed by your organization. Contact an admin to change this policy."
|
||||||
|
noActivityDescription = "Documents will appear here once this policy runs."
|
||||||
|
noActivityTitle = "No activity yet"
|
||||||
|
onEveryUpload = "On every upload"
|
||||||
|
originalsNote = "Originals stay untouched • Enforced version saved alongside"
|
||||||
|
pause = "Pause"
|
||||||
|
recentActivity = "Recent Activity"
|
||||||
|
resume = "Resume"
|
||||||
|
retry = "Retry"
|
||||||
|
showLess = "Show less"
|
||||||
|
showMore = "Show more"
|
||||||
|
statActive = "Active"
|
||||||
|
statDataProcessed = "Data processed"
|
||||||
|
statDocsEnforced = "Docs enforced"
|
||||||
|
statusActive = "Active"
|
||||||
|
statusPaused = "Paused"
|
||||||
|
|
||||||
|
[policies.fields]
|
||||||
|
selectedCount = "{{count}} selected"
|
||||||
|
|
||||||
|
[policies.pii]
|
||||||
|
fieldLabel = "PII to redact"
|
||||||
|
placeholder = "Select PII types"
|
||||||
|
|
||||||
|
[policies.sidebar]
|
||||||
|
activeCount = "{{count}} active"
|
||||||
|
infoAriaLabel = "What is a policy?"
|
||||||
|
infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps."
|
||||||
|
loading = "Loading…"
|
||||||
|
railAriaLabel = "{{label}} policy — {{status}}"
|
||||||
|
railSuffixActive = " (Active)"
|
||||||
|
railSuffixPaused = " (Paused)"
|
||||||
|
setUp = "Set up"
|
||||||
|
title = "Policies"
|
||||||
|
upgradeToEnterprise = "Upgrade to enterprise"
|
||||||
|
|
||||||
|
[policies.status]
|
||||||
|
active = "Active"
|
||||||
|
paused = "Paused"
|
||||||
|
setup = "Set up"
|
||||||
|
|
||||||
|
[policies.toolConfig]
|
||||||
|
enableAriaLabel = "Enable {{tool}}"
|
||||||
|
infoAriaLabel = "What does {{tool}} do?"
|
||||||
|
|
||||||
|
[policies.wizard]
|
||||||
|
allDocTypesDescription = "Enable the Classification policy to filter by document type."
|
||||||
|
allDocTypesTitle = "All document types"
|
||||||
|
back = "Back"
|
||||||
|
builderDesc = "Build the sequence of tools this policy runs on each document."
|
||||||
|
clear = "Clear"
|
||||||
|
continue = "Continue"
|
||||||
|
docTypesLabel = "Document types"
|
||||||
|
edit = "Edit"
|
||||||
|
editTitle = "Edit {{label}} Policy"
|
||||||
|
enablePolicy = "Enable Policy"
|
||||||
|
filenameAutoNumber = "Auto-number"
|
||||||
|
filenamePositionAria = "Filename position"
|
||||||
|
filenamePrefix = "Prefix"
|
||||||
|
filenameSuffix = "Suffix"
|
||||||
|
filenameTextAria = "Filename text"
|
||||||
|
filenameTextPlaceholder = "Text to add (optional)"
|
||||||
|
lockedDescription = "Contact an admin to change this policy."
|
||||||
|
lockedTitle = "Managed by your organization"
|
||||||
|
maxRetriesLabel = "Max retries"
|
||||||
|
noToolsError = "Add at least one configured tool to the workflow first."
|
||||||
|
outputAsLabel = "Output as"
|
||||||
|
outputFilenameSubhead = "Output filename"
|
||||||
|
outputModeAria = "Output mode"
|
||||||
|
outputNewFile = "New file"
|
||||||
|
outputNewVersion = "New version"
|
||||||
|
outputRetriesLabel = "Output & retries"
|
||||||
|
outputSubhead = "Output"
|
||||||
|
retryDelayAria = "Retry delay minutes"
|
||||||
|
retryDelayLabel = "Retry delay (min)"
|
||||||
|
runOnExport = "Export"
|
||||||
|
runOnLabel = "Run on"
|
||||||
|
runOnSubhead = "Run on"
|
||||||
|
runOnUpload = "Upload"
|
||||||
|
saveChanges = "Save Changes"
|
||||||
|
saveError = "Couldn't save the policy. Please try again."
|
||||||
|
setupClassification = "Set up Classification"
|
||||||
|
setupTitle = "Set up {{label}} Policy"
|
||||||
|
sourcesDesc = "Choose where this policy runs and which document types it applies to."
|
||||||
|
sourcesLabel = "Sources"
|
||||||
|
stepOf = "Step {{step}} of {{total}}"
|
||||||
|
toolChainDesc = "Configure the tools this policy runs on each document."
|
||||||
|
typesSelected = "{{count}} types selected"
|
||||||
|
|
||||||
|
|
||||||
[printFile]
|
[printFile]
|
||||||
title = "Print File"
|
title = "Print File"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,29 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
|
||||||
|
// The global setup mock returns the i18n KEY (t: (key) => key); these tests
|
||||||
|
// assert the rendered English copy, so override locally to return the default
|
||||||
|
// value passed to t() — mirroring i18next's runtime fallback when a key is
|
||||||
|
// missing. (Interpolated strings are returned raw; no test here asserts them.)
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string, defaultValue?: string) =>
|
||||||
|
typeof defaultValue === "string" ? defaultValue : key,
|
||||||
|
i18n: { changeLanguage: vi.fn() },
|
||||||
|
}),
|
||||||
|
initReactI18next: { type: "3rdParty", init: vi.fn() },
|
||||||
|
I18nextProvider: ({ children }: { children: ReactNode }) => children,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// usePolicies derives `canConfigure` from app-config; with no AppConfigProvider
|
||||||
|
// here `config` is null, which (tri-state gate) hides the edit affordances. Mock
|
||||||
|
// app-config as a single-user deployment (login off) so the local operator can
|
||||||
|
// configure and the narrative view's Edit/Pause/Delete actions render.
|
||||||
|
vi.mock("@app/contexts/AppConfigContext", async (orig) => ({
|
||||||
|
...(await orig<typeof import("@app/contexts/AppConfigContext")>()),
|
||||||
|
useAppConfig: () => ({ config: { enableLogin: false } }),
|
||||||
|
}));
|
||||||
|
|
||||||
// The shared Tooltip (used by the "what is a policy?" info button) pulls in
|
// The shared Tooltip (used by the "what is a policy?" info button) pulls in
|
||||||
// preferences/sidebar contexts we don't set up here — passthrough it.
|
// preferences/sidebar contexts we don't set up here — passthrough it.
|
||||||
vi.mock("@app/components/shared/Tooltip", () => ({
|
vi.mock("@app/components/shared/Tooltip", () => ({
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, type ReactNode } from "react";
|
import { useState, useEffect, useMemo, type ReactNode } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||||
import { usePolicies } from "@app/hooks/usePolicies";
|
import { usePolicies } from "@app/hooks/usePolicies";
|
||||||
@@ -75,6 +76,7 @@ export function PoliciesSection({
|
|||||||
* collapse button), mirroring the back-button + title in a policy. */
|
* collapse button), mirroring the back-button + title in a policy. */
|
||||||
leadingControl?: ReactNode;
|
leadingControl?: ReactNode;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const pol = usePolicies();
|
const pol = usePolicies();
|
||||||
const { categories } = usePolicyCatalog();
|
const { categories } = usePolicyCatalog();
|
||||||
// Persist the expand/collapse state across refreshes.
|
// Persist the expand/collapse state across refreshes.
|
||||||
@@ -109,21 +111,26 @@ export function PoliciesSection({
|
|||||||
<div className="pol-list-head">
|
<div className="pol-list-head">
|
||||||
{leadingControl}
|
{leadingControl}
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Policies"
|
title={t("policies.sidebar.title", "Policies")}
|
||||||
count={`${configuredCount} active`}
|
count={t("policies.sidebar.activeCount", "{{count}} active", {
|
||||||
|
count: configuredCount,
|
||||||
|
})}
|
||||||
collapsible
|
collapsible
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onToggle={toggleExpanded}
|
onToggle={toggleExpanded}
|
||||||
/>
|
/>
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
content="A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps."
|
content={t(
|
||||||
|
"policies.sidebar.infoTooltip",
|
||||||
|
"A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps.",
|
||||||
|
)}
|
||||||
sidebarTooltip
|
sidebarTooltip
|
||||||
pinOnClick
|
pinOnClick
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="pol-info-btn"
|
className="pol-info-btn"
|
||||||
aria-label="What is a policy?"
|
aria-label={t("policies.sidebar.infoAriaLabel", "What is a policy?")}
|
||||||
>
|
>
|
||||||
<LocalIcon
|
<LocalIcon
|
||||||
icon="info-outline-rounded"
|
icon="info-outline-rounded"
|
||||||
@@ -149,10 +156,15 @@ export function PoliciesSection({
|
|||||||
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
||||||
{cat.icon}
|
{cat.icon}
|
||||||
</IconBadge>
|
</IconBadge>
|
||||||
<span className="pol-row-label">{cat.label}</span>
|
<span className="pol-row-label">
|
||||||
|
{t(`policies.catalog.${cat.id}`, cat.label)}
|
||||||
|
</span>
|
||||||
<span className="pol-row-trail">
|
<span className="pol-row-trail">
|
||||||
<span className="pol-row-soon">
|
<span className="pol-row-soon">
|
||||||
Upgrade to enterprise
|
{t(
|
||||||
|
"policies.sidebar.upgradeToEnterprise",
|
||||||
|
"Upgrade to enterprise",
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,16 +181,20 @@ export function PoliciesSection({
|
|||||||
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
||||||
{cat.icon}
|
{cat.icon}
|
||||||
</IconBadge>
|
</IconBadge>
|
||||||
<span className="pol-row-label">{cat.label}</span>
|
<span className="pol-row-label">
|
||||||
|
{t(`policies.catalog.${cat.id}`, cat.label)}
|
||||||
|
</span>
|
||||||
<span className="pol-row-trail">
|
<span className="pol-row-trail">
|
||||||
{status === "setup" ? (
|
{status === "setup" ? (
|
||||||
<span className="pol-row-setup">Set up</span>
|
<span className="pol-row-setup">
|
||||||
|
{t("policies.sidebar.setUp", "Set up")}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
tone={status === "active" ? "success" : "warning"}
|
tone={status === "active" ? "success" : "warning"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{STATUS_LABEL[status]}
|
{t(`policies.status.${status}`, STATUS_LABEL[status])}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
)}
|
)}
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
@@ -201,6 +217,7 @@ export function PoliciesSection({
|
|||||||
* which replaces the Tools area while a policy is selected.
|
* which replaces the Tools area while a policy is selected.
|
||||||
*/
|
*/
|
||||||
export function PolicyDetailTakeover() {
|
export function PolicyDetailTakeover() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const pol = usePolicies();
|
const pol = usePolicies();
|
||||||
const { categories, configs, sources, docTypes } = usePolicyCatalog();
|
const { categories, configs, sources, docTypes } = usePolicyCatalog();
|
||||||
const { selectedId, detailView } = usePolicySelection();
|
const { selectedId, detailView } = usePolicySelection();
|
||||||
@@ -311,7 +328,9 @@ export function PolicyDetailTakeover() {
|
|||||||
return (
|
return (
|
||||||
<div className="pol-detail">
|
<div className="pol-detail">
|
||||||
<div className="pol-scroll">
|
<div className="pol-scroll">
|
||||||
<p className="pol-desc">Loading…</p>
|
<p className="pol-desc">
|
||||||
|
{t("policies.sidebar.loading", "Loading…")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -404,6 +423,7 @@ export function PoliciesCollapsedButton({
|
|||||||
}: {
|
}: {
|
||||||
onExpand: () => void;
|
onExpand: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const pol = usePolicies();
|
const pol = usePolicies();
|
||||||
const { categories } = usePolicyCatalog();
|
const { categories } = usePolicyCatalog();
|
||||||
|
|
||||||
@@ -416,16 +436,21 @@ export function PoliciesCollapsedButton({
|
|||||||
.filter((cat) => !cat.comingSoon)
|
.filter((cat) => !cat.comingSoon)
|
||||||
.map((cat) => {
|
.map((cat) => {
|
||||||
const status = deriveRowStatus(pol.policies[cat.id]);
|
const status = deriveRowStatus(pol.policies[cat.id]);
|
||||||
|
const label = t(`policies.catalog.${cat.id}`, cat.label);
|
||||||
|
const statusLabel = t(
|
||||||
|
`policies.status.${status}`,
|
||||||
|
STATUS_LABEL[status],
|
||||||
|
);
|
||||||
const suffix =
|
const suffix =
|
||||||
status === "active"
|
status === "active"
|
||||||
? " (Active)"
|
? t("policies.sidebar.railSuffixActive", " (Active)")
|
||||||
: status === "paused"
|
: status === "paused"
|
||||||
? " (Paused)"
|
? t("policies.sidebar.railSuffixPaused", " (Paused)")
|
||||||
: "";
|
: "";
|
||||||
return (
|
return (
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
key={cat.id}
|
key={cat.id}
|
||||||
content={`${cat.label}${suffix}`}
|
content={`${label}${suffix}`}
|
||||||
position="left"
|
position="left"
|
||||||
arrow
|
arrow
|
||||||
delay={300}
|
delay={300}
|
||||||
@@ -434,7 +459,11 @@ export function PoliciesCollapsedButton({
|
|||||||
type="button"
|
type="button"
|
||||||
className="pol-crail-btn"
|
className="pol-crail-btn"
|
||||||
data-status={status}
|
data-status={status}
|
||||||
aria-label={`${cat.label} policy — ${STATUS_LABEL[status]}`}
|
aria-label={t(
|
||||||
|
"policies.sidebar.railAriaLabel",
|
||||||
|
"{{label}} policy — {{status}}",
|
||||||
|
{ label, status: statusLabel },
|
||||||
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
selectPolicy(cat.id);
|
selectPolicy(cat.id);
|
||||||
onExpand();
|
onExpand();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import PublicIcon from "@mui/icons-material/Public";
|
import PublicIcon from "@mui/icons-material/Public";
|
||||||
import ScheduleIcon from "@mui/icons-material/Schedule";
|
import ScheduleIcon from "@mui/icons-material/Schedule";
|
||||||
import HistoryIcon from "@mui/icons-material/History";
|
import HistoryIcon from "@mui/icons-material/History";
|
||||||
@@ -66,7 +67,13 @@ function humanizeOperation(op: string): string {
|
|||||||
* lengthy is clamped and collapsed by default with a Show more/less toggle.
|
* lengthy is clamped and collapsed by default with a Show more/less toggle.
|
||||||
* Short messages (e.g. "Enforcement failed") render plainly with no toggle.
|
* Short messages (e.g. "Enforcement failed") render plainly with no toggle.
|
||||||
*/
|
*/
|
||||||
function ActivityError({ message }: { message: string }) {
|
function ActivityError({
|
||||||
|
message,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
message: string;
|
||||||
|
t: (key: string, defaultValue: string) => string;
|
||||||
|
}) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const needsToggle = message.length > 80 || message.includes("\n");
|
const needsToggle = message.length > 80 || message.includes("\n");
|
||||||
if (!needsToggle) return <>{message}</>;
|
if (!needsToggle) return <>{message}</>;
|
||||||
@@ -82,7 +89,9 @@ function ActivityError({ message }: { message: string }) {
|
|||||||
className="pol-activity-error__toggle"
|
className="pol-activity-error__toggle"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
>
|
>
|
||||||
{expanded ? "Show less" : "Show more"}
|
{expanded
|
||||||
|
? t("policies.detail.showLess", "Show less")
|
||||||
|
: t("policies.detail.showMore", "Show more")}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -104,6 +113,7 @@ export function PolicyDetailPanel({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onRetry,
|
onRetry,
|
||||||
}: PolicyDetailPanelProps) {
|
}: PolicyDetailPanelProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const isPaused = status === "paused";
|
const isPaused = status === "paused";
|
||||||
// Real configured steps drive the flow; fall back to the preset's rule labels.
|
// Real configured steps drive the flow; fall back to the preset's rule labels.
|
||||||
const enforceItems =
|
const enforceItems =
|
||||||
@@ -123,7 +133,7 @@ export function PolicyDetailPanel({
|
|||||||
<PanelHeader
|
<PanelHeader
|
||||||
icon={category.icon}
|
icon={category.icon}
|
||||||
iconAccent={ROW_ACCENT[category.id] ?? "blue"}
|
iconAccent={ROW_ACCENT[category.id] ?? "blue"}
|
||||||
title={category.label}
|
title={t(`policies.catalog.${category.id}`, category.label)}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
actions={
|
actions={
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -131,7 +141,9 @@ export function PolicyDetailPanel({
|
|||||||
showDot
|
showDot
|
||||||
pulse={!isPaused}
|
pulse={!isPaused}
|
||||||
>
|
>
|
||||||
{isPaused ? "Paused" : "Active"}
|
{isPaused
|
||||||
|
? t("policies.detail.statusPaused", "Paused")
|
||||||
|
: t("policies.detail.statusActive", "Active")}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +151,9 @@ export function PolicyDetailPanel({
|
|||||||
<div className="pol-scroll">
|
<div className="pol-scroll">
|
||||||
{/* Enforces */}
|
{/* Enforces */}
|
||||||
<div>
|
<div>
|
||||||
<p className="pol-section-label">Enforces</p>
|
<p className="pol-section-label">
|
||||||
|
{t("policies.detail.enforces", "Enforces")}
|
||||||
|
</p>
|
||||||
<Card padding="default">
|
<Card padding="default">
|
||||||
<div className="pol-rule-flow">
|
<div className="pol-rule-flow">
|
||||||
<ChipFlow items={enforceItems} separator="arrow" />
|
<ChipFlow items={enforceItems} separator="arrow" />
|
||||||
@@ -151,19 +165,24 @@ export function PolicyDetailPanel({
|
|||||||
</span>
|
</span>
|
||||||
<span className="pol-meta-item">
|
<span className="pol-meta-item">
|
||||||
<ScheduleIcon sx={{ fontSize: "0.8rem" }} />
|
<ScheduleIcon sx={{ fontSize: "0.8rem" }} />
|
||||||
On every upload
|
{t("policies.detail.onEveryUpload", "On every upload")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-note">
|
<div className="pol-note">
|
||||||
<HistoryIcon sx={{ fontSize: "0.8rem" }} />
|
<HistoryIcon sx={{ fontSize: "0.8rem" }} />
|
||||||
Originals stay untouched • Enforced version saved alongside
|
{t(
|
||||||
|
"policies.detail.originalsNote",
|
||||||
|
"Originals stay untouched • Enforced version saved alongside",
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
<div>
|
<div>
|
||||||
<p className="pol-section-label">Recent Activity</p>
|
<p className="pol-section-label">
|
||||||
|
{t("policies.detail.recentActivity", "Recent Activity")}
|
||||||
|
</p>
|
||||||
{activityItems.length > 0 ? (
|
{activityItems.length > 0 ? (
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
{activityItems.map((item, i) => (
|
{activityItems.map((item, i) => (
|
||||||
@@ -192,7 +211,7 @@ export function PolicyDetailPanel({
|
|||||||
title={item.doc}
|
title={item.doc}
|
||||||
description={
|
description={
|
||||||
item.status === "flagged" ? (
|
item.status === "flagged" ? (
|
||||||
<ActivityError message={item.action} />
|
<ActivityError message={item.action} t={t} />
|
||||||
) : (
|
) : (
|
||||||
item.action
|
item.action
|
||||||
)
|
)
|
||||||
@@ -205,7 +224,7 @@ export function PolicyDetailPanel({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onRetry(item)}
|
onClick={() => onRetry(item)}
|
||||||
>
|
>
|
||||||
Retry
|
{t("policies.detail.retry", "Retry")}
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
@@ -217,8 +236,11 @@ export function PolicyDetailPanel({
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
size="compact"
|
size="compact"
|
||||||
icon={<DescriptionIcon sx={{ fontSize: "1.5rem" }} />}
|
icon={<DescriptionIcon sx={{ fontSize: "1.5rem" }} />}
|
||||||
title="No activity yet"
|
title={t("policies.detail.noActivityTitle", "No activity yet")}
|
||||||
description="Documents will appear here once this policy runs."
|
description={t(
|
||||||
|
"policies.detail.noActivityDescription",
|
||||||
|
"Documents will appear here once this policy runs.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -232,15 +254,21 @@ export function PolicyDetailPanel({
|
|||||||
<span className="pol-stat-value">
|
<span className="pol-stat-value">
|
||||||
{statValues.enforced.toLocaleString()}
|
{statValues.enforced.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="pol-stat-label">Docs enforced</span>
|
<span className="pol-stat-label">
|
||||||
|
{t("policies.detail.statDocsEnforced", "Docs enforced")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-stat">
|
<div className="pol-stat">
|
||||||
<span className="pol-stat-value">{statValues.dataProcessed}</span>
|
<span className="pol-stat-value">{statValues.dataProcessed}</span>
|
||||||
<span className="pol-stat-label">Data processed</span>
|
<span className="pol-stat-label">
|
||||||
|
{t("policies.detail.statDataProcessed", "Data processed")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-stat">
|
<div className="pol-stat">
|
||||||
<span className="pol-stat-value">{statValues.activeFor}</span>
|
<span className="pol-stat-value">{statValues.activeFor}</span>
|
||||||
<span className="pol-stat-label">Active</span>
|
<span className="pol-stat-label">
|
||||||
|
{t("policies.detail.statActive", "Active")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -249,7 +277,10 @@ export function PolicyDetailPanel({
|
|||||||
<Banner
|
<Banner
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
icon={<LockIcon sx={{ fontSize: "1rem" }} />}
|
icon={<LockIcon sx={{ fontSize: "1rem" }} />}
|
||||||
description="Managed by your organization. Contact an admin to change this policy."
|
description={t(
|
||||||
|
"policies.detail.managedByOrg",
|
||||||
|
"Managed by your organization. Contact an admin to change this policy.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -265,14 +296,16 @@ export function PolicyDetailPanel({
|
|||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
style={{ marginRight: "auto" }}
|
style={{ marginRight: "auto" }}
|
||||||
>
|
>
|
||||||
Delete
|
{t("delete", "Delete")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="outline" size="sm" onClick={onTogglePause}>
|
<Button variant="outline" size="sm" onClick={onTogglePause}>
|
||||||
{isPaused ? "Resume" : "Pause"}
|
{isPaused
|
||||||
|
? t("policies.detail.resume", "Resume")
|
||||||
|
: t("policies.detail.pause", "Pause")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="gradient" size="sm" onClick={onEditSettings}>
|
<Button variant="gradient" size="sm" onClick={onEditSettings}>
|
||||||
Edit Settings
|
{t("policies.detail.editSettings", "Edit Settings")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
|
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
|
||||||
import { Select } from "@shared/components/Select";
|
import { Select } from "@shared/components/Select";
|
||||||
import { Input } from "@shared/components/Input";
|
import { Input } from "@shared/components/Input";
|
||||||
@@ -25,6 +26,11 @@ export function PolicyFieldRow({
|
|||||||
onChange,
|
onChange,
|
||||||
first,
|
first,
|
||||||
}: PolicyFieldRowProps) {
|
}: PolicyFieldRowProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
// Field labels and option labels come from the policy catalog data, so they're
|
||||||
|
// wrapped at the render site with data-keyed ids (English stays the fallback).
|
||||||
|
const fieldLabel = t(`policies.field.${field.key}`, field.label);
|
||||||
|
|
||||||
if (field.type === "chips") {
|
if (field.type === "chips") {
|
||||||
const selected = Array.isArray(value) ? value : [];
|
const selected = Array.isArray(value) ? value : [];
|
||||||
const toggle = (opt: string) =>
|
const toggle = (opt: string) =>
|
||||||
@@ -36,8 +42,12 @@ export function PolicyFieldRow({
|
|||||||
return (
|
return (
|
||||||
<div className="pol-field" data-first={first || undefined}>
|
<div className="pol-field" data-first={first || undefined}>
|
||||||
<div className="pol-field-chips-head">
|
<div className="pol-field-chips-head">
|
||||||
<span className="pol-field-label">{field.label}</span>
|
<span className="pol-field-label">{fieldLabel}</span>
|
||||||
<span className="pol-field-count">{selected.length} selected</span>
|
<span className="pol-field-count">
|
||||||
|
{t("policies.fields.selectedCount", "{{count}} selected", {
|
||||||
|
count: selected.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-field-chips">
|
<div className="pol-field-chips">
|
||||||
{(field.options ?? []).map((opt) => (
|
{(field.options ?? []).map((opt) => (
|
||||||
@@ -47,7 +57,7 @@ export function PolicyFieldRow({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => toggle(opt)}
|
onClick={() => toggle(opt)}
|
||||||
>
|
>
|
||||||
{opt}
|
{t(`policies.fieldOption.${field.key}.${opt}`, opt)}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -61,28 +71,31 @@ export function PolicyFieldRow({
|
|||||||
size="sm"
|
size="sm"
|
||||||
checked={Boolean(value)}
|
checked={Boolean(value)}
|
||||||
onChange={(checked) => onChange(checked)}
|
onChange={(checked) => onChange(checked)}
|
||||||
aria-label={field.label}
|
aria-label={fieldLabel}
|
||||||
/>
|
/>
|
||||||
) : field.type === "select" ? (
|
) : field.type === "select" ? (
|
||||||
<Select
|
<Select
|
||||||
inputSize="sm"
|
inputSize="sm"
|
||||||
options={(field.options ?? []).map((o) => ({ value: o, label: o }))}
|
options={(field.options ?? []).map((o) => ({
|
||||||
|
value: o,
|
||||||
|
label: t(`policies.fieldOption.${field.key}.${o}`, o),
|
||||||
|
}))}
|
||||||
value={typeof value === "string" ? value : ""}
|
value={typeof value === "string" ? value : ""}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
aria-label={field.label}
|
aria-label={fieldLabel}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
inputSize="sm"
|
inputSize="sm"
|
||||||
value={typeof value === "string" ? value : ""}
|
value={typeof value === "string" ? value : ""}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
aria-label={field.label}
|
aria-label={fieldLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pol-field" data-first={first || undefined}>
|
<div className="pol-field" data-first={first || undefined}>
|
||||||
<SettingsRow label={field.label} control={control} />
|
<SettingsRow label={fieldLabel} control={control} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MultiSelect } from "@mantine/core";
|
import { MultiSelect } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { PII_PRESETS } from "@app/data/policyDefinitions";
|
import { PII_PRESETS } from "@app/data/policyDefinitions";
|
||||||
|
|
||||||
/** The set of preset regexes — used to separate preset words from custom ones. */
|
/** The set of preset regexes — used to separate preset words from custom ones. */
|
||||||
@@ -24,6 +25,7 @@ export function PolicyPiiField({
|
|||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
}: PolicyPiiFieldProps) {
|
}: PolicyPiiFieldProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const words = Array.isArray(parameters.wordsToRedact)
|
const words = Array.isArray(parameters.wordsToRedact)
|
||||||
? (parameters.wordsToRedact as string[])
|
? (parameters.wordsToRedact as string[])
|
||||||
: [];
|
: [];
|
||||||
@@ -48,9 +50,14 @@ export function PolicyPiiField({
|
|||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
size="sm"
|
size="sm"
|
||||||
label="PII to redact"
|
label={t("policies.pii.fieldLabel", "PII to redact")}
|
||||||
placeholder="Select PII types"
|
placeholder={t("policies.pii.placeholder", "Select PII types")}
|
||||||
data={PII_PRESETS.map((p) => ({ value: p.value, label: p.label }))}
|
data={PII_PRESETS.map((p) => ({
|
||||||
|
value: p.value,
|
||||||
|
// Preset labels are catalog data — keyed by preset value with the
|
||||||
|
// English label as fallback.
|
||||||
|
label: t(`policies.pii.${p.value}`, p.label),
|
||||||
|
}))}
|
||||||
value={selected}
|
value={selected}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useMemo, useRef } from "react";
|
import { useState, useMemo, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||||
import { PanelHeader } from "@shared/components/PanelHeader";
|
import { PanelHeader } from "@shared/components/PanelHeader";
|
||||||
@@ -98,6 +99,7 @@ export function PolicySetupWizard({
|
|||||||
onCommitConfig,
|
onCommitConfig,
|
||||||
onSetupClassification,
|
onSetupClassification,
|
||||||
}: PolicySetupWizardProps) {
|
}: PolicySetupWizardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const isEdit = mode === "edit";
|
const isEdit = mode === "edit";
|
||||||
// Preset (tool-chain) policies render the locked tool config as their Workflow
|
// Preset (tool-chain) policies render the locked tool config as their Workflow
|
||||||
// step instead of the add/remove builder.
|
// step instead of the add/remove builder.
|
||||||
@@ -158,13 +160,27 @@ export function PolicySetupWizard({
|
|||||||
<PanelHeader
|
<PanelHeader
|
||||||
icon={category.icon}
|
icon={category.icon}
|
||||||
iconAccent={ROW_ACCENT[category.id] ?? "blue"}
|
iconAccent={ROW_ACCENT[category.id] ?? "blue"}
|
||||||
title={`${isEdit ? "Edit" : "Set up"} ${category.label} Policy`}
|
title={
|
||||||
|
isEdit
|
||||||
|
? t("policies.wizard.editTitle", "Edit {{label}} Policy", {
|
||||||
|
label: t(`policies.catalog.${category.id}`, category.label),
|
||||||
|
})
|
||||||
|
: t("policies.wizard.setupTitle", "Set up {{label}} Policy", {
|
||||||
|
label: t(`policies.catalog.${category.id}`, category.label),
|
||||||
|
})
|
||||||
|
}
|
||||||
onBack={onCancel}
|
onBack={onCancel}
|
||||||
/>
|
/>
|
||||||
<div className="pol-scroll">
|
<div className="pol-scroll">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Managed by your organization"
|
title={t(
|
||||||
description="Contact an admin to change this policy."
|
"policies.wizard.lockedTitle",
|
||||||
|
"Managed by your organization",
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"policies.wizard.lockedDescription",
|
||||||
|
"Contact an admin to change this policy.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,7 +228,12 @@ export function PolicySetupWizard({
|
|||||||
}),
|
}),
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setSaveError("Couldn't save the policy. Please try again.");
|
setSaveError(
|
||||||
|
t(
|
||||||
|
"policies.wizard.saveError",
|
||||||
|
"Couldn't save the policy. Please try again.",
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,7 +264,12 @@ export function PolicySetupWizard({
|
|||||||
}),
|
}),
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setSaveError("Couldn't save the policy. Please try again.");
|
setSaveError(
|
||||||
|
t(
|
||||||
|
"policies.wizard.saveError",
|
||||||
|
"Couldn't save the policy. Please try again.",
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,7 +286,12 @@ export function PolicySetupWizard({
|
|||||||
// the user back to the Workflow step to fix it.
|
// the user back to the Workflow step to fix it.
|
||||||
const handleSaveFailed = () => {
|
const handleSaveFailed = () => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setSaveError("Add at least one configured tool to the workflow first.");
|
setSaveError(
|
||||||
|
t(
|
||||||
|
"policies.wizard.noToolsError",
|
||||||
|
"Add at least one configured tool to the workflow first.",
|
||||||
|
),
|
||||||
|
);
|
||||||
setStep(1);
|
setStep(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,14 +300,25 @@ export function PolicySetupWizard({
|
|||||||
<PanelHeader
|
<PanelHeader
|
||||||
icon={category.icon}
|
icon={category.icon}
|
||||||
iconAccent={ROW_ACCENT[category.id] ?? "blue"}
|
iconAccent={ROW_ACCENT[category.id] ?? "blue"}
|
||||||
title={`${isEdit ? "Edit" : "Set up"} ${category.label} Policy`}
|
title={
|
||||||
subtitle={`Step ${step} of ${TOTAL_STEPS}`}
|
isEdit
|
||||||
|
? t("policies.wizard.editTitle", "Edit {{label}} Policy", {
|
||||||
|
label: t(`policies.catalog.${category.id}`, category.label),
|
||||||
|
})
|
||||||
|
: t("policies.wizard.setupTitle", "Set up {{label}} Policy", {
|
||||||
|
label: t(`policies.catalog.${category.id}`, category.label),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
subtitle={t("policies.wizard.stepOf", "Step {{step}} of {{total}}", {
|
||||||
|
step,
|
||||||
|
total: TOTAL_STEPS,
|
||||||
|
})}
|
||||||
onBack={back}
|
onBack={back}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="Cancel"
|
aria-label={t("cancel", "Cancel")}
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
leadingIcon={<CloseIcon sx={{ fontSize: "1.1rem" }} />}
|
leadingIcon={<CloseIcon sx={{ fontSize: "1.1rem" }} />}
|
||||||
/>
|
/>
|
||||||
@@ -302,7 +344,10 @@ export function PolicySetupWizard({
|
|||||||
{toolChain ? (
|
{toolChain ? (
|
||||||
<>
|
<>
|
||||||
<p className="pol-desc">
|
<p className="pol-desc">
|
||||||
Configure the tools this policy runs on each document.
|
{t(
|
||||||
|
"policies.wizard.toolChainDesc",
|
||||||
|
"Configure the tools this policy runs on each document.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<PolicyToolConfigStep
|
<PolicyToolConfigStep
|
||||||
chainIds={toolChain}
|
chainIds={toolChain}
|
||||||
@@ -318,7 +363,10 @@ export function PolicySetupWizard({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="pol-desc">
|
<p className="pol-desc">
|
||||||
Build the sequence of tools this policy runs on each document.
|
{t(
|
||||||
|
"policies.wizard.builderDesc",
|
||||||
|
"Build the sequence of tools this policy runs on each document.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<PolicyWorkflowStep
|
<PolicyWorkflowStep
|
||||||
automation={seedAutomation}
|
automation={seedAutomation}
|
||||||
@@ -351,14 +399,18 @@ export function PolicySetupWizard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Real, working output + retry settings (applied by the engine). */}
|
{/* Real, working output + retry settings (applied by the engine). */}
|
||||||
<p className="pol-section-label">Output & retries</p>
|
<p className="pol-section-label">
|
||||||
|
{t("policies.wizard.outputRetriesLabel", "Output & retries")}
|
||||||
|
</p>
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
{/* The editor event the policy runs on: input on upload, or
|
{/* The editor event the policy runs on: input on upload, or
|
||||||
output on export (enforced before the file is exported). */}
|
output on export (enforced before the file is exported). */}
|
||||||
<div className="pol-subhead">Run on</div>
|
<div className="pol-subhead">
|
||||||
|
{t("policies.wizard.runOnSubhead", "Run on")}
|
||||||
|
</div>
|
||||||
<div className="pol-field" data-first>
|
<div className="pol-field" data-first>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
label="Run on"
|
label={t("policies.wizard.runOnLabel", "Run on")}
|
||||||
control={
|
control={
|
||||||
<Select
|
<Select
|
||||||
inputSize="sm"
|
inputSize="sm"
|
||||||
@@ -366,19 +418,27 @@ export function PolicySetupWizard({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRunOn(e.target.value as "upload" | "export")
|
setRunOn(e.target.value as "upload" | "export")
|
||||||
}
|
}
|
||||||
aria-label="Run on"
|
aria-label={t("policies.wizard.runOnLabel", "Run on")}
|
||||||
options={[
|
options={[
|
||||||
{ value: "upload", label: "Upload" },
|
{
|
||||||
{ value: "export", label: "Export" },
|
value: "upload",
|
||||||
|
label: t("policies.wizard.runOnUpload", "Upload"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "export",
|
||||||
|
label: t("policies.wizard.runOnExport", "Export"),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-subhead">Output</div>
|
<div className="pol-subhead">
|
||||||
|
{t("policies.wizard.outputSubhead", "Output")}
|
||||||
|
</div>
|
||||||
<div className="pol-field" data-first>
|
<div className="pol-field" data-first>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
label="Output as"
|
label={t("policies.wizard.outputAsLabel", "Output as")}
|
||||||
control={
|
control={
|
||||||
<Select
|
<Select
|
||||||
inputSize="sm"
|
inputSize="sm"
|
||||||
@@ -397,17 +457,31 @@ export function PolicySetupWizard({
|
|||||||
setOutputNamePosition("suffix");
|
setOutputNamePosition("suffix");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label="Output mode"
|
aria-label={t(
|
||||||
|
"policies.wizard.outputModeAria",
|
||||||
|
"Output mode",
|
||||||
|
)}
|
||||||
options={[
|
options={[
|
||||||
{ value: "new_file", label: "New file" },
|
{
|
||||||
{ value: "new_version", label: "New version" },
|
value: "new_file",
|
||||||
|
label: t("policies.wizard.outputNewFile", "New file"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "new_version",
|
||||||
|
label: t(
|
||||||
|
"policies.wizard.outputNewVersion",
|
||||||
|
"New version",
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Output filename: position + custom text together as one row. */}
|
{/* Output filename: position + custom text together as one row. */}
|
||||||
<div className="pol-subhead">Output filename</div>
|
<div className="pol-subhead">
|
||||||
|
{t("policies.wizard.outputFilenameSubhead", "Output filename")}
|
||||||
|
</div>
|
||||||
<div className="pol-field" data-first>
|
<div className="pol-field" data-first>
|
||||||
<div className="pol-name-row">
|
<div className="pol-name-row">
|
||||||
<Select
|
<Select
|
||||||
@@ -418,13 +492,30 @@ export function PolicySetupWizard({
|
|||||||
e.target.value as "prefix" | "suffix" | "auto-number",
|
e.target.value as "prefix" | "suffix" | "auto-number",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
aria-label="Filename position"
|
aria-label={t(
|
||||||
|
"policies.wizard.filenamePositionAria",
|
||||||
|
"Filename position",
|
||||||
|
)}
|
||||||
options={[
|
options={[
|
||||||
{ value: "prefix", label: "Prefix" },
|
{
|
||||||
{ value: "suffix", label: "Suffix" },
|
value: "prefix",
|
||||||
|
label: t("policies.wizard.filenamePrefix", "Prefix"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "suffix",
|
||||||
|
label: t("policies.wizard.filenameSuffix", "Suffix"),
|
||||||
|
},
|
||||||
// Auto-number only makes sense for separate new files.
|
// Auto-number only makes sense for separate new files.
|
||||||
...(outputMode === "new_file"
|
...(outputMode === "new_file"
|
||||||
? [{ value: "auto-number", label: "Auto-number" }]
|
? [
|
||||||
|
{
|
||||||
|
value: "auto-number",
|
||||||
|
label: t(
|
||||||
|
"policies.wizard.filenameAutoNumber",
|
||||||
|
"Auto-number",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -435,15 +526,21 @@ export function PolicySetupWizard({
|
|||||||
inputSize="sm"
|
inputSize="sm"
|
||||||
value={outputName}
|
value={outputName}
|
||||||
onChange={(e) => setOutputName(e.target.value)}
|
onChange={(e) => setOutputName(e.target.value)}
|
||||||
placeholder="Text to add (optional)"
|
placeholder={t(
|
||||||
aria-label="Filename text"
|
"policies.wizard.filenameTextPlaceholder",
|
||||||
|
"Text to add (optional)",
|
||||||
|
)}
|
||||||
|
aria-label={t(
|
||||||
|
"policies.wizard.filenameTextAria",
|
||||||
|
"Filename text",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-field">
|
<div className="pol-field">
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
label="Max retries"
|
label={t("policies.wizard.maxRetriesLabel", "Max retries")}
|
||||||
control={
|
control={
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -452,14 +549,20 @@ export function PolicySetupWizard({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setMaxRetries(Math.max(0, Number(e.target.value) || 0))
|
setMaxRetries(Math.max(0, Number(e.target.value) || 0))
|
||||||
}
|
}
|
||||||
aria-label="Max retries"
|
aria-label={t(
|
||||||
|
"policies.wizard.maxRetriesLabel",
|
||||||
|
"Max retries",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pol-field">
|
<div className="pol-field">
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
label="Retry delay (min)"
|
label={t(
|
||||||
|
"policies.wizard.retryDelayLabel",
|
||||||
|
"Retry delay (min)",
|
||||||
|
)}
|
||||||
control={
|
control={
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -470,7 +573,10 @@ export function PolicySetupWizard({
|
|||||||
Math.max(0, Number(e.target.value) || 0),
|
Math.max(0, Number(e.target.value) || 0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
aria-label="Retry delay minutes"
|
aria-label={t(
|
||||||
|
"policies.wizard.retryDelayAria",
|
||||||
|
"Retry delay minutes",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -484,10 +590,14 @@ export function PolicySetupWizard({
|
|||||||
{SOURCES_IN_FLOW && step === 3 && (
|
{SOURCES_IN_FLOW && step === 3 && (
|
||||||
<>
|
<>
|
||||||
<p className="pol-desc">
|
<p className="pol-desc">
|
||||||
Choose where this policy runs and which document types it applies
|
{t(
|
||||||
to.
|
"policies.wizard.sourcesDesc",
|
||||||
|
"Choose where this policy runs and which document types it applies to.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="pol-section-label">
|
||||||
|
{t("policies.wizard.sourcesLabel", "Sources")}
|
||||||
</p>
|
</p>
|
||||||
<p className="pol-section-label">Sources</p>
|
|
||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
{sourceDefs.map((src, i) => (
|
{sourceDefs.map((src, i) => (
|
||||||
<div
|
<div
|
||||||
@@ -506,20 +616,31 @@ export function PolicySetupWizard({
|
|||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<p className="pol-section-label">Document types</p>
|
<p className="pol-section-label">
|
||||||
|
{t("policies.wizard.docTypesLabel", "Document types")}
|
||||||
|
</p>
|
||||||
{!classificationEnabled ? (
|
{!classificationEnabled ? (
|
||||||
<Banner
|
<Banner
|
||||||
tone="warning"
|
tone="warning"
|
||||||
icon={<InfoOutlinedIcon sx={{ fontSize: "1rem" }} />}
|
icon={<InfoOutlinedIcon sx={{ fontSize: "1rem" }} />}
|
||||||
title="All document types"
|
title={t(
|
||||||
description="Enable the Classification policy to filter by document type."
|
"policies.wizard.allDocTypesTitle",
|
||||||
|
"All document types",
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"policies.wizard.allDocTypesDescription",
|
||||||
|
"Enable the Classification policy to filter by document type.",
|
||||||
|
)}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onSetupClassification}
|
onClick={onSetupClassification}
|
||||||
>
|
>
|
||||||
Set up Classification
|
{t(
|
||||||
|
"policies.wizard.setupClassification",
|
||||||
|
"Set up Classification",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -528,14 +649,23 @@ export function PolicySetupWizard({
|
|||||||
<div className="pol-doctypes-head">
|
<div className="pol-doctypes-head">
|
||||||
<span className="pol-field-label">
|
<span className="pol-field-label">
|
||||||
{scopeTypes.length === 0
|
{scopeTypes.length === 0
|
||||||
? "All document types"
|
? t(
|
||||||
: `${scopeTypes.length} types selected`}
|
"policies.wizard.allDocTypesTitle",
|
||||||
|
"All document types",
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"policies.wizard.typesSelected",
|
||||||
|
"{{count}} types selected",
|
||||||
|
{ count: scopeTypes.length },
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="pol-link"
|
className="pol-link"
|
||||||
onClick={() => setScopeNarrow((v) => !v)}
|
onClick={() => setScopeNarrow((v) => !v)}
|
||||||
>
|
>
|
||||||
{scopeNarrow ? "Clear" : "Edit"}
|
{scopeNarrow
|
||||||
|
? t("policies.wizard.clear", "Clear")
|
||||||
|
: t("policies.wizard.edit", "Edit")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{scopeNarrow && (
|
{scopeNarrow && (
|
||||||
@@ -564,7 +694,9 @@ export function PolicySetupWizard({
|
|||||||
|
|
||||||
<div className="pol-footer">
|
<div className="pol-footer">
|
||||||
<Button variant="ghost" size="sm" onClick={back}>
|
<Button variant="ghost" size="sm" onClick={back}>
|
||||||
{step > 1 ? "Back" : "Cancel"}
|
{step > 1
|
||||||
|
? t("policies.wizard.back", "Back")
|
||||||
|
: t("cancel", "Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
{step < TOTAL_STEPS ? (
|
{step < TOTAL_STEPS ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -573,7 +705,7 @@ export function PolicySetupWizard({
|
|||||||
style={{ marginLeft: "auto" }}
|
style={{ marginLeft: "auto" }}
|
||||||
onClick={() => setStep((s) => Math.min(TOTAL_STEPS, s + 1))}
|
onClick={() => setStep((s) => Math.min(TOTAL_STEPS, s + 1))}
|
||||||
>
|
>
|
||||||
Continue
|
{t("policies.wizard.continue", "Continue")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -583,7 +715,9 @@ export function PolicySetupWizard({
|
|||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{isEdit ? "Save Changes" : "Enable Policy"}
|
{isEdit
|
||||||
|
? t("policies.wizard.saveChanges", "Save Changes")
|
||||||
|
: t("policies.wizard.enablePolicy", "Enable Policy")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Loader } from "@mantine/core";
|
import { Loader } from "@mantine/core";
|
||||||
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
|
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
|
||||||
import { Card } from "@shared/components/Card";
|
import { Card } from "@shared/components/Card";
|
||||||
@@ -9,13 +10,24 @@ import { PolicyWatermarkConfig } from "@app/components/policies/PolicyWatermarkC
|
|||||||
import type { ToolRegistry } from "@app/data/toolsTaxonomy";
|
import type { ToolRegistry } from "@app/data/toolsTaxonomy";
|
||||||
import type { ToolId } from "@app/types/toolId";
|
import type { ToolId } from "@app/types/toolId";
|
||||||
|
|
||||||
/** Plain-language, non-technical descriptions shown by each tool's info button. */
|
/**
|
||||||
const TOOL_PLAIN_INFO: Record<string, string> = {
|
* Plain-language, non-technical descriptions shown by each tool's info button.
|
||||||
redact:
|
* Stored as [i18n key, English default] pairs so they can be resolved with `t`
|
||||||
|
* at render (the map lives at module scope, outside the component).
|
||||||
|
*/
|
||||||
|
const TOOL_PLAIN_INFO: Record<string, readonly [key: string, en: string]> = {
|
||||||
|
redact: [
|
||||||
|
"policies.toolConfig.info.redact",
|
||||||
"Automatically finds and blacks out sensitive details — like Social Security and card numbers — so they can't be read in the document.",
|
"Automatically finds and blacks out sensitive details — like Social Security and card numbers — so they can't be read in the document.",
|
||||||
sanitize:
|
],
|
||||||
|
sanitize: [
|
||||||
|
"policies.toolConfig.info.sanitize",
|
||||||
"Removes hidden JavaScript from the file, so nothing can run automatically when someone opens it.",
|
"Removes hidden JavaScript from the file, so nothing can run automatically when someone opens it.",
|
||||||
watermark: "Stamps a visible mark (e.g. “Confidential”) across every page.",
|
],
|
||||||
|
watermark: [
|
||||||
|
"policies.toolConfig.info.watermark",
|
||||||
|
"Stamps a visible mark (e.g. “Confidential”) across every page.",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** One tool in a policy's fixed chain: whether it runs + its configured params. */
|
/** One tool in a policy's fixed chain: whether it runs + its configured params. */
|
||||||
@@ -51,6 +63,7 @@ export function PolicyToolConfig({
|
|||||||
onChange,
|
onChange,
|
||||||
editable = true,
|
editable = true,
|
||||||
}: PolicyToolConfigProps) {
|
}: PolicyToolConfigProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const patchTool = (index: number, patch: Partial<PolicyToolState>) =>
|
const patchTool = (index: number, patch: Partial<PolicyToolState>) =>
|
||||||
onChange(tools.map((t, i) => (i === index ? { ...t, ...patch } : t)));
|
onChange(tools.map((t, i) => (i === index ? { ...t, ...patch } : t)));
|
||||||
|
|
||||||
@@ -59,23 +72,27 @@ export function PolicyToolConfig({
|
|||||||
{tools.map((tool, index) => {
|
{tools.map((tool, index) => {
|
||||||
const entry = toolRegistry[tool.operation as ToolId];
|
const entry = toolRegistry[tool.operation as ToolId];
|
||||||
const Settings = entry?.automationSettings ?? null;
|
const Settings = entry?.automationSettings ?? null;
|
||||||
|
const toolName = entry?.name ?? tool.operation;
|
||||||
|
const plainInfo = TOOL_PLAIN_INFO[tool.operation];
|
||||||
return (
|
return (
|
||||||
<Card key={tool.operation} padding="none">
|
<Card key={tool.operation} padding="none">
|
||||||
<div className="pol-tool-head">
|
<div className="pol-tool-head">
|
||||||
<span className="pol-tool-icon">{entry?.icon}</span>
|
<span className="pol-tool-icon">{entry?.icon}</span>
|
||||||
<span className="pol-tool-name">
|
<span className="pol-tool-name">{toolName}</span>
|
||||||
{entry?.name ?? tool.operation}
|
{plainInfo && (
|
||||||
</span>
|
|
||||||
{TOOL_PLAIN_INFO[tool.operation] && (
|
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
content={TOOL_PLAIN_INFO[tool.operation]}
|
content={t(plainInfo[0], plainInfo[1])}
|
||||||
sidebarTooltip
|
sidebarTooltip
|
||||||
pinOnClick
|
pinOnClick
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="pol-info-btn"
|
className="pol-info-btn"
|
||||||
aria-label={`What does ${entry?.name ?? tool.operation} do?`}
|
aria-label={t(
|
||||||
|
"policies.toolConfig.infoAriaLabel",
|
||||||
|
"What does {{tool}} do?",
|
||||||
|
{ tool: toolName },
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<LocalIcon
|
<LocalIcon
|
||||||
icon="info-outline-rounded"
|
icon="info-outline-rounded"
|
||||||
@@ -91,7 +108,9 @@ export function PolicyToolConfig({
|
|||||||
checked={tool.enabled}
|
checked={tool.enabled}
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
onChange={(checked) => patchTool(index, { enabled: checked })}
|
onChange={(checked) => patchTool(index, { enabled: checked })}
|
||||||
aria-label={`Enable ${entry?.name ?? tool.operation}`}
|
aria-label={t("policies.toolConfig.enableAriaLabel", "Enable {{tool}}", {
|
||||||
|
tool: toolName,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{tool.enabled &&
|
{tool.enabled &&
|
||||||
|
|||||||
Reference in New Issue
Block a user