mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Change to have template cards
This commit is contained in:
@@ -7837,11 +7837,11 @@ platform = "PDF Platform"
|
||||
processor = "PDF Processor"
|
||||
|
||||
[portal.pipelines]
|
||||
subtitle = "Pipelines and the policies your organisation requires. Start from a suggested policy, or build any pipeline and enforce it. Click a row to edit, run, or pause it."
|
||||
subtitle = "Automate your document workflows. Start from a template for a simple, guided setup, or build a custom pipeline from scratch. Enforce any pipeline as a policy to run it on every document."
|
||||
title = "Pipelines"
|
||||
|
||||
[portal.pipelines.actions]
|
||||
newPipeline = "New pipeline"
|
||||
newCustomPipeline = "New custom pipeline"
|
||||
|
||||
[portal.pipelines.all]
|
||||
title = "All pipelines"
|
||||
@@ -7912,8 +7912,8 @@ invalid = "Not valid JSON. The last valid version is kept until this is fixed."
|
||||
label = "Policy metadata (advanced)"
|
||||
|
||||
[portal.pipelines.builder.required]
|
||||
desc = "Members can't pause or delete a required policy, and it enforces on their documents."
|
||||
label = "Required by your organisation"
|
||||
desc = "Members can't skip, pause, or delete it, and it runs on their documents automatically."
|
||||
label = "Enforce as policy"
|
||||
|
||||
[portal.pipelines.composer]
|
||||
addTool = "Add a tool"
|
||||
@@ -8017,10 +8017,6 @@ timeout = "Run is taking longer than expected; it may still finish in the backgr
|
||||
active = "Active"
|
||||
paused = "Paused"
|
||||
|
||||
[portal.pipelines.suggested]
|
||||
subtitle = "Ready-made policies with a simple setup. Add one, then customise it into a full pipeline if you need to."
|
||||
title = "Suggested policies"
|
||||
|
||||
[portal.pipelines.table]
|
||||
name = "Pipeline"
|
||||
sources = "Sources"
|
||||
@@ -8029,6 +8025,11 @@ steps = "Steps"
|
||||
trigger = "Trigger"
|
||||
type = "Type"
|
||||
|
||||
[portal.pipelines.templates]
|
||||
setUp = "Set up"
|
||||
subtitle = "Ready-made starting points with a simple, guided setup. Pick one, then customise it into a full pipeline if you need more control."
|
||||
title = "Templates"
|
||||
|
||||
[portal.pipelines.trigger]
|
||||
folder-watch = "Folder watch"
|
||||
manual = "Manual"
|
||||
@@ -8036,7 +8037,7 @@ schedule = "Scheduled"
|
||||
|
||||
[portal.pipelines.type]
|
||||
pipeline = "Pipeline"
|
||||
required = "Required"
|
||||
policy = "Policy"
|
||||
|
||||
[portal.policies]
|
||||
defaultName = "{{category}} Policy"
|
||||
@@ -8475,9 +8476,8 @@ description = "Every uploaded document is classified against the built-in labels
|
||||
labelsHeading = "Classification labels"
|
||||
|
||||
[portal.policies.wizard.enforcement]
|
||||
heading = "Enforcement"
|
||||
requiredDesc = "Members can't pause or delete a required policy, and it runs on their documents automatically."
|
||||
requiredLabel = "Required by your organisation"
|
||||
requiredDesc = "Run this on every document automatically. Members can't skip, pause, or delete it."
|
||||
requiredLabel = "Enforce as policy"
|
||||
|
||||
[portal.policies.wizard.errors]
|
||||
noTools = "Enable at least one tool in the workflow first."
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.portal-pipelines__template-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.portal-pipelines__template-card--locked {
|
||||
/* Recede via surface + ink rather than opacity, which would fade text below the contrast floor. */
|
||||
background: var(--c-surface-sunken);
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
.portal-pipelines__template-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--c-primary-subtle);
|
||||
color: var(--c-accent-fg, var(--c-primary));
|
||||
}
|
||||
|
||||
.portal-pipelines__template-card--locked .portal-pipelines__template-icon {
|
||||
background: var(--c-surface-raised);
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
.portal-pipelines__template-title {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 640;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.portal-pipelines__template-blurb {
|
||||
/* Push the CTA to the card foot so cards in a row line their footers up. */
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
color: var(--c-text-subtle);
|
||||
}
|
||||
|
||||
.portal-pipelines__template-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 620;
|
||||
color: var(--c-accent-fg, var(--c-primary));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import {
|
||||
POLICY_CATEGORIES,
|
||||
POLICY_CONFIG,
|
||||
} from "@portal/components/policies/storyFixtures";
|
||||
import { PipelineTemplateCard } from "@portal/components/pipelines/PipelineTemplateCard";
|
||||
|
||||
const security = POLICY_CATEGORIES.find((c) => c.id === "security")!;
|
||||
const compliance = POLICY_CATEGORIES.find((c) => c.id === "compliance")!;
|
||||
|
||||
const meta: Meta<typeof PipelineTemplateCard> = {
|
||||
title: "Portal/Pipelines/PipelineTemplateCard",
|
||||
component: PipelineTemplateCard,
|
||||
parameters: { layout: "padded" },
|
||||
args: { onOpen: () => {} },
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PipelineTemplateCard>;
|
||||
|
||||
/** An available template — opens the simple guided setup. */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
entry: { category: security, config: POLICY_CONFIG.security, policy: null },
|
||||
},
|
||||
};
|
||||
|
||||
/** Setup unavailable (e.g. the AI engine is off) — shown but inert. */
|
||||
export const Locked: Story = {
|
||||
args: {
|
||||
entry: { category: security, config: POLICY_CONFIG.security, policy: null },
|
||||
locked: true,
|
||||
lockedLabel: "Requires AI engine",
|
||||
},
|
||||
};
|
||||
|
||||
/** Coming-soon category — locked and inert. */
|
||||
export const ComingSoon: Story = {
|
||||
args: {
|
||||
entry: {
|
||||
category: compliance,
|
||||
config: POLICY_CONFIG.compliance,
|
||||
policy: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
import { Card, Chip } from "@app/ui";
|
||||
import type { CatalogueEntry } from "@portal/api/policies";
|
||||
import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon";
|
||||
import "@portal/components/pipelines/PipelineTemplateCard.css";
|
||||
|
||||
interface PipelineTemplateCardProps {
|
||||
entry: CatalogueEntry;
|
||||
/** Open the simple setup wizard seeded from this template. */
|
||||
onOpen: (entry: CatalogueEntry) => void;
|
||||
/** Setup is unavailable (e.g. the AI engine is off): shown, but not openable. */
|
||||
locked?: boolean;
|
||||
/** Chip text explaining why setup is locked (e.g. "Requires AI engine"). */
|
||||
lockedLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A template in the Pipelines gallery: a ready-made starting point that opens the simple, guided
|
||||
* setup. Vertical card (icon, title, blurb, a "Set up" affordance) so the gallery reads as a set of
|
||||
* choices, distinct from the pipelines table below. Locked/coming-soon templates show a chip and
|
||||
* don't open.
|
||||
*/
|
||||
export function PipelineTemplateCard({
|
||||
entry,
|
||||
onOpen,
|
||||
locked = false,
|
||||
lockedLabel,
|
||||
}: PipelineTemplateCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { category } = entry;
|
||||
const comingSoon = category.comingSoon === true;
|
||||
const openable = !comingSoon && !locked;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={
|
||||
"portal-pipelines__template-card" +
|
||||
(openable ? "" : " portal-pipelines__template-card--locked")
|
||||
}
|
||||
interactive={openable}
|
||||
onClick={openable ? () => onOpen(entry) : undefined}
|
||||
role={openable ? "button" : undefined}
|
||||
tabIndex={openable ? 0 : undefined}
|
||||
onKeyDown={
|
||||
openable
|
||||
? (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onOpen(entry);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span className="portal-pipelines__template-icon" aria-hidden>
|
||||
{policyCategoryIcon(category.id)}
|
||||
</span>
|
||||
|
||||
<h3 className="portal-pipelines__template-title">{t(category.label)}</h3>
|
||||
<p className="portal-pipelines__template-blurb">{t(category.desc)}</p>
|
||||
|
||||
{comingSoon ? (
|
||||
<Chip accent="neutral" size="sm">
|
||||
{t("portal.policies.card.comingSoon")}
|
||||
</Chip>
|
||||
) : locked ? (
|
||||
<Chip accent="neutral" size="sm">
|
||||
{lockedLabel ?? t("portal.policies.card.requiresAiEngine")}
|
||||
</Chip>
|
||||
) : (
|
||||
<span className="portal-pipelines__template-cta">
|
||||
{t("portal.pipelines.templates.setUp")}
|
||||
<ArrowForwardRoundedIcon style={{ fontSize: "1rem" }} />
|
||||
</span>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -35,11 +35,10 @@ export function PipelinesTable({ pipelines, onRowClick }: PipelinesTableProps) {
|
||||
key: "type",
|
||||
header: t("portal.pipelines.table.type", "Type"),
|
||||
sortable: true,
|
||||
// A required pipeline IS a policy (the org mandates it); an ordinary one is just an
|
||||
// automation.
|
||||
// A pipeline enforced as a policy shows as "Policy"; an ordinary one as "Pipeline".
|
||||
get: (p) =>
|
||||
p.required
|
||||
? { tone: "purple", label: t("portal.pipelines.type.required") }
|
||||
? { tone: "purple", label: t("portal.pipelines.type.policy") }
|
||||
: { tone: "neutral", label: t("portal.pipelines.type.pipeline") },
|
||||
}),
|
||||
column.text({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Banner,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
FormField,
|
||||
Input,
|
||||
Modal,
|
||||
@@ -600,24 +601,6 @@ function PolicySetupWizardBody({
|
||||
|
||||
{step === "settings" && (
|
||||
<div className="portal-policies__wizard-section">
|
||||
<h3 className="portal-policies__wizard-heading">
|
||||
{t("portal.policies.wizard.enforcement.heading")}
|
||||
</h3>
|
||||
<Card padding="none">
|
||||
<SettingsRow
|
||||
label={t("portal.policies.wizard.enforcement.requiredLabel")}
|
||||
description={t("portal.policies.wizard.enforcement.requiredDesc")}
|
||||
control={
|
||||
<ToggleSwitch
|
||||
size="sm"
|
||||
checked={required}
|
||||
onChange={setRequired}
|
||||
label=""
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{config.fields.length > 0 && (
|
||||
<>
|
||||
<h3 className="portal-policies__wizard-heading">
|
||||
@@ -803,6 +786,15 @@ function PolicySetupWizardBody({
|
||||
)}
|
||||
{/* TODO: reviewer user-picker goes here */}
|
||||
</div>
|
||||
|
||||
<div className="portal-policies__wizard-enforce">
|
||||
<Checkbox
|
||||
checked={required}
|
||||
onChange={(e) => setRequired(e.target.checked)}
|
||||
label={t("portal.policies.wizard.enforcement.requiredLabel")}
|
||||
description={t("portal.policies.wizard.enforcement.requiredDesc")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -31,14 +31,21 @@
|
||||
max-width: 46rem;
|
||||
}
|
||||
|
||||
/* Sections: the suggested-policy gallery and the full list. */
|
||||
.portal-pipelines__suggested,
|
||||
/* Sections: the template gallery and the full pipelines list. */
|
||||
.portal-pipelines__templates,
|
||||
.portal-pipelines__all {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Template gallery: a responsive row of cards that wraps to fit. */
|
||||
.portal-pipelines__templates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.portal-pipelines__section-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -32,8 +32,8 @@ vi.mock("@portal/api/pipelines", () => ({
|
||||
fetchPipeline: (id: string) => fetchPipeline(id),
|
||||
}));
|
||||
|
||||
// The suggested-policy gallery is out of scope here: keep the catalogue empty so the test focuses on
|
||||
// the pipelines list.
|
||||
// The template gallery is out of scope here: keep the catalogue empty so the test focuses on the
|
||||
// pipelines list.
|
||||
vi.mock("@portal/queries/policies", () => ({
|
||||
usePoliciesOverview: () => ({ data: null, loading: false, error: null }),
|
||||
}));
|
||||
@@ -99,7 +99,9 @@ describe("Pipelines view", () => {
|
||||
it("opens the builder when creating a pipeline", async () => {
|
||||
renderView();
|
||||
await screen.findByText("Redaction sweep");
|
||||
fireEvent.click(screen.getByText("portal.pipelines.actions.newPipeline"));
|
||||
fireEvent.click(
|
||||
screen.getByText("portal.pipelines.actions.newCustomPipeline"),
|
||||
);
|
||||
expect(await screen.findByText("builder new")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext";
|
||||
import { PipelinesIcon } from "@portal/components/icons";
|
||||
import { KpiStrip } from "@portal/components/pipelines/KpiStrip";
|
||||
import { PipelinesTable } from "@portal/components/pipelines/PipelinesTable";
|
||||
import { PolicyCatalogueTable } from "@portal/components/policies/PolicyCatalogueTable";
|
||||
import { PipelineTemplateCard } from "@portal/components/pipelines/PipelineTemplateCard";
|
||||
import { PolicyDetailPanel } from "@portal/components/policies/PolicyDetailPanel";
|
||||
import { PolicySetupWizard } from "@portal/components/policies/PolicySetupWizard";
|
||||
import { useAiEngineEnabled } from "@portal/hooks/useAiEngineEnabled";
|
||||
@@ -252,26 +252,31 @@ export function Pipelines() {
|
||||
onClick={openCreate}
|
||||
leftSection={<AddRoundedIcon style={{ fontSize: "1.125rem" }} />}
|
||||
>
|
||||
{t("portal.pipelines.actions.newPipeline")}
|
||||
{t("portal.pipelines.actions.newCustomPipeline")}
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{pageError && <Banner tone="danger" description={pageError} />}
|
||||
|
||||
{galleryEntries.length > 0 && (
|
||||
<section className="portal-pipelines__suggested">
|
||||
<section className="portal-pipelines__templates">
|
||||
<h2 className="portal-pipelines__section-title">
|
||||
{t("portal.pipelines.suggested.title")}
|
||||
{t("portal.pipelines.templates.title")}
|
||||
</h2>
|
||||
<p className="portal-pipelines__section-sub">
|
||||
{t("portal.pipelines.suggested.subtitle")}
|
||||
{t("portal.pipelines.templates.subtitle")}
|
||||
</p>
|
||||
<PolicyCatalogueTable
|
||||
entries={galleryEntries}
|
||||
onOpen={openTemplate}
|
||||
isLocked={isLocked}
|
||||
lockedLabel={t("portal.policies.card.requiresAiEngine")}
|
||||
/>
|
||||
<div className="portal-pipelines__templates-grid">
|
||||
{galleryEntries.map((entry) => (
|
||||
<PipelineTemplateCard
|
||||
key={entry.category.id}
|
||||
entry={entry}
|
||||
onOpen={openTemplate}
|
||||
locked={isLocked(entry)}
|
||||
lockedLabel={t("portal.policies.card.requiresAiEngine")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@@ -234,6 +234,13 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* "Enforce as policy" — the last choice in the simple wizard, set off from the settings above. */
|
||||
.portal-policies__wizard-enforce {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.875rem;
|
||||
border-top: 1px solid var(--c-border-subtle);
|
||||
}
|
||||
|
||||
.portal-policies__fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user