Remove settings from templates which don't work

This commit is contained in:
James Brunton
2026-09-01 16:03:37 +01:00
parent f18b60e513
commit 2c5fd906c9
4 changed files with 109 additions and 260 deletions
@@ -8849,9 +8849,6 @@ labelsHeading = "Classification labels"
noTools = "Enable at least one tool in the workflow first."
saveFailed = "Couldn't save the pipeline. Please try again."
[portal.policies.wizard.output]
heading = "Output & run"
[portal.policies.wizard.output.filenameRule]
autoNumber = "Auto-number"
label = "Filename rule"
@@ -8873,10 +8870,6 @@ upload = "Upload"
[portal.policies.wizard.settings]
heading = "Settings"
[portal.policies.wizard.sources]
heading = "Sources"
loading = "Loading sources…"
[portal.policies.wizard.tabs]
ariaLabel = "Setup steps"
settings = "Settings"
@@ -32,11 +32,6 @@ vi.mock("react-i18next", () => ({
initReactI18next: { type: "3rdParty", init: vi.fn() },
}));
const fetchSources = vi.fn();
vi.mock("@portal/api/sources", () => ({
fetchSources: () => fetchSources(),
}));
const fetchIntegrations = vi.fn();
vi.mock("@portal/api/integrations", () => ({
fetchIntegrations: () => fetchIntegrations(),
@@ -89,7 +84,6 @@ async function submitWizard(saveLabel: string) {
describe("PolicySetupWizard", () => {
beforeEach(() => {
fetchSources.mockResolvedValue({ sources: [] });
fetchIntegrations.mockResolvedValue([]);
});
@@ -1,11 +1,6 @@
import { useMemo, useState, type ReactNode } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import CheckIcon from "@mui/icons-material/Check";
import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined";
import CloudOutlinedIcon from "@mui/icons-material/CloudOutlined";
import StorageOutlinedIcon from "@mui/icons-material/StorageOutlined";
import {
Banner,
Button,
@@ -34,7 +29,6 @@ import {
type PolicyToolStep,
} from "@app/policies/operations";
import { resolveRunOn } from "@app/policies/runOn";
import { useSources } from "@portal/queries/sources";
import { fetchIntegrations } from "@portal/api/integrations";
import { errorMessage } from "@portal/api/http";
import { useAsync } from "@portal/hooks/useAsync";
@@ -46,21 +40,6 @@ import { PolicyPurviewConfig } from "@portal/components/policies/PolicyPurviewCo
import { ClassificationLabelsSection } from "@portal/components/policies/ClassificationLabelsSection";
import "@portal/views/Policies.css";
/** Outline icon for a source tile, keyed by the backend source `type`. */
function sourceIcon(type: string): ReactNode {
const sx = { fontSize: "1.1rem" } as const;
switch (type) {
case "editor":
return <EditOutlinedIcon sx={sx} />;
case "folder":
return <FolderOutlinedIcon sx={sx} />;
case "s3":
return <CloudOutlinedIcon sx={sx} />;
default:
return <StorageOutlinedIcon sx={sx} />;
}
}
interface PolicySetupWizardProps {
/** The category being configured, or null when closed. */
entry: CatalogueEntry | null;
@@ -280,36 +259,6 @@ function PolicySetupWizardBody({
const [fieldValues, setFieldValues] = useState(() =>
resolveFieldValues(entry),
);
// Real sources only; editor participation is its own flag, not an entry here.
const [sources, setSources] = useState<string[]>(() =>
(policy?.state.sources ?? []).filter((s) => s !== "editor"),
);
// Whether the policy runs in the editor. Defaults on for a new policy (the common case);
// on edit it comes straight from the stored flag, never re-derived from the sources list.
const [runsOnEditor, setRunsOnEditor] = useState<boolean>(
policy?.state.runsOnEditor ?? true,
);
const sourcesAsync = useSources();
const availableSources = useMemo(() => {
const backendSources = (sourcesAsync.data?.sources ?? []).filter(
(s) => s.status !== "disabled",
);
// The editor is always an available source. The backend now returns it as a
// virtual source too, so take that when present (avoids a duplicate tile) and
// otherwise fall back to a synthetic one; keep it first, selected by default.
const editorSource = backendSources.find((s) => s.id === "editor") ?? {
id: "editor",
name: t("portal.sources.types.editor.label"),
type: "editor",
status: "active" as const,
referenceCount: 0,
referencingPolicies: [],
config: [],
docsTotal: null,
};
return [editorSource, ...backendSources.filter((s) => s.id !== "editor")];
}, [sourcesAsync.data, t]);
// Document-type scoping has no UI; preserve any saved scope on edit and
// default new policies to all document types.
const [scopeTypes] = useState<string[]>(policy?.state.scopeTypes ?? []);
@@ -381,18 +330,6 @@ function PolicySetupWizardBody({
);
}
function toggleSource(id: string) {
// The editor is not a real source: its tile toggles the runsOnEditor flag instead of
// adding "editor" to the sources list.
if (id === "editor") {
setRunsOnEditor((on) => !on);
return;
}
setSources((prev) =>
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
);
}
/** The wizard's current state as a submit result: shared by Save and Customise. */
function collectResult(): PolicySetupResult {
const steps: PipelineStep[] = enabledTools.map((tl) =>
@@ -400,9 +337,12 @@ function PolicySetupWizardBody({
);
return {
required,
runsOnEditor,
// A template always runs in the editor. A pipeline takes exactly one input, and the simple
// wizard has no field to bind a stored source, so real sources never applied here; anyone
// wanting a folder or bucket source uses Customise, which drops into the full builder.
runsOnEditor: true,
fieldValues,
sources,
sources: [],
scopeTypes,
reviewerEmail,
outputMode,
@@ -634,170 +574,112 @@ function PolicySetupWizardBody({
</>
)}
<h3 className="portal-policies__wizard-heading">
{t("portal.policies.wizard.sources.heading")}
</h3>
{sourcesAsync.loading && !sourcesAsync.data ? (
<p className="portal-policies__sources-loading">
{t("portal.policies.wizard.sources.loading")}
</p>
) : (
// The backend always returns the editor as a virtual source, so the
// loaded list is never empty - no "no sources" state exists.
<div className="portal-policies__sources">
{availableSources.map((src) => {
const on =
src.id === "editor" ? runsOnEditor : sources.includes(src.id);
return (
<Button
key={src.id}
variant={on ? "secondary" : "quiet"}
justify="between"
fullWidth
className={
"portal-policies__source" +
(on ? " portal-policies__source--on" : "")
}
// The check keeps its slot when unselected (hidden) so the
// icon + name stay put whether or not the tile is selected.
rightSection={
<CheckIcon
sx={{
fontSize: "1.1rem",
visibility: on ? "visible" : "hidden",
}}
/>
}
onClick={() => toggleSource(src.id)}
aria-pressed={on}
>
<span className="portal-policies__source-label">
{sourceIcon(src.type)}
{src.name}
</span>
</Button>
);
})}
</div>
)}
<h3 className="portal-policies__wizard-heading">
{t("portal.policies.wizard.output.heading")}
</h3>
<div className="portal-policies__fields">
{runsOnEditor && (
<>
<FormField
label={t("portal.policies.wizard.output.runOn.label")}
helperText={t("portal.policies.wizard.output.runOn.helper")}
>
<Select
<FormField
label={t("portal.policies.wizard.output.runOn.label")}
helperText={t("portal.policies.wizard.output.runOn.helper")}
>
<Select
inputSize="sm"
value={runOn}
onChange={(value) => setRunOn(resolveRunOn(value, category.id))}
options={[
{
value: "upload",
label: t("portal.policies.wizard.output.runOn.upload"),
},
{
value: "export",
label: t("portal.policies.wizard.output.runOn.export"),
},
]}
/>
</FormField>
<FormField
label={t("portal.policies.wizard.output.outputAs.label")}
>
<Select
inputSize="sm"
value={outputMode}
onChange={(value) => {
const mode = (value ?? "new_file") as
| "new_file"
| "new_version";
setOutputMode(mode);
// Auto-number only applies to separate new files.
if (
mode === "new_version" &&
outputNamePosition === "auto-number"
) {
setOutputNamePosition("suffix");
}
}}
options={[
{
value: "new_version",
label: t(
"portal.policies.wizard.output.outputAs.newVersion",
),
},
{
value: "new_file",
label: t("portal.policies.wizard.output.outputAs.newFile"),
},
]}
/>
</FormField>
<FormField
label={t("portal.policies.wizard.output.filenameRule.label")}
>
<div className="portal-policies__name-row">
<Select
inputSize="sm"
value={outputNamePosition}
onChange={(value) =>
setOutputNamePosition(
(value ?? "suffix") as
| "prefix"
| "suffix"
| "auto-number",
)
}
options={[
{
value: "prefix",
label: t(
"portal.policies.wizard.output.filenameRule.prefix",
),
},
{
value: "suffix",
label: t(
"portal.policies.wizard.output.filenameRule.suffix",
),
},
...(outputMode === "new_file"
? [
{
value: "auto-number",
label: t(
"portal.policies.wizard.output.filenameRule.autoNumber",
),
},
]
: []),
]}
/>
{outputNamePosition !== "auto-number" && (
<Input
inputSize="sm"
value={runOn}
onChange={(value) =>
setRunOn(resolveRunOn(value, category.id))
}
options={[
{
value: "upload",
label: t("portal.policies.wizard.output.runOn.upload"),
},
{
value: "export",
label: t("portal.policies.wizard.output.runOn.export"),
},
]}
/>
</FormField>
<FormField
label={t("portal.policies.wizard.output.outputAs.label")}
>
<Select
inputSize="sm"
value={outputMode}
onChange={(value) => {
const mode = (value ?? "new_file") as
| "new_file"
| "new_version";
setOutputMode(mode);
// Auto-number only applies to separate new files.
if (
mode === "new_version" &&
outputNamePosition === "auto-number"
) {
setOutputNamePosition("suffix");
}
}}
options={[
{
value: "new_version",
label: t(
"portal.policies.wizard.output.outputAs.newVersion",
),
},
{
value: "new_file",
label: t(
"portal.policies.wizard.output.outputAs.newFile",
),
},
]}
/>
</FormField>
<FormField
label={t("portal.policies.wizard.output.filenameRule.label")}
>
<div className="portal-policies__name-row">
<Select
inputSize="sm"
value={outputNamePosition}
onChange={(value) =>
setOutputNamePosition(
(value ?? "suffix") as
| "prefix"
| "suffix"
| "auto-number",
)
}
options={[
{
value: "prefix",
label: t(
"portal.policies.wizard.output.filenameRule.prefix",
),
},
{
value: "suffix",
label: t(
"portal.policies.wizard.output.filenameRule.suffix",
),
},
...(outputMode === "new_file"
? [
{
value: "auto-number",
label: t(
"portal.policies.wizard.output.filenameRule.autoNumber",
),
},
]
: []),
]}
/>
{outputNamePosition !== "auto-number" && (
<Input
inputSize="sm"
value={outputName}
placeholder={t(
"portal.policies.wizard.output.filenameRule.placeholder",
)}
onChange={(e) => setOutputName(e.target.value)}
/>
value={outputName}
placeholder={t(
"portal.policies.wizard.output.filenameRule.placeholder",
)}
</div>
</FormField>
</>
)}
onChange={(e) => setOutputName(e.target.value)}
/>
)}
</div>
</FormField>
{/* TODO: reviewer user-picker goes here */}
</div>
@@ -281,26 +281,6 @@
margin-top: 0.75rem;
}
/* Sources picker */
/* Selectable source tiles: a vertical stack of full-width shared Buttons
(icon · name · check). Layout is the Button's own leftSection/label/
rightSection — only the selected border/tint is added here. */
.portal-policies__sources {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.portal-policies__source--on {
border-color: var(--c-primary);
}
.portal-policies__source-label {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.portal-policies__link {
border: none;
background: none;