## Overview Adds a **Classification policy** to the processor's policy catalogue, set up the same way as the Security policy. This moves classifier configuration out of the editor (where the labels UI landed in #6898 and was then removed with the rest of the editor's policy-management surface in #6932) and into the processor, which is now the single place policies are configured. ## What it does - **Classification card** in the processor policy catalogue. Always shown, but **setup is locked until the backend reports the AI engine is on** — so admins can see the capability they're missing rather than it being hidden entirely. - **Setup wizard** mirrors Security: the workflow step shows the team's **classification label editor** (reused `LabelsEditor`/`LabelsEditorModal` — add box, chip grid, per-label icon picker, import/export, reset) instead of tool toggles, since classify is a single non-configurable step. - On enable, the team's label vocabulary is **seeded with the 268 built-in defaults** (clobber-safe: only when the team has none). On upload the document is classified against the team's labels and tagged; on SaaS with the engine on, files group by category in the editor sidebar. ## Reuse & consolidation - Reuses the existing labels table, `labelsFile` helpers, and default vocabulary. Labels read/write through the processor's own `apiClient.local` (not the editor's axios client) so auth/base routing stays explicit; the wire shape is shared. - Consolidates policy-category icons into a shared, **id-keyed** `policyCategoryIcon` util (outline glyphs) used by both the editor and the processor, replacing the processor's emoji-glyph map (and the stray `schedule` key that rendered a bare dot). ## Testing - `task frontend:typecheck:{core,proprietary,portal}`, `frontend:lint:eslint`, `frontend:test` (156 files / 1305 tests) — all green. - Verified in Storybook: the Classification card renders, the setup wizard shows the label editor (268 defaults), and the full labels editor opens with icons/import/export/reset. Added an MSW handler for the app-config + labels endpoints and a `Classification` wizard story. ## Notes for reviewers - The AI-engine gate reads the public `/api/v1/config/app-config`; classification labels use `/api/v1/classification/labels` (team-scoped, team-lead/admin-gated, `policies.enabled`); the classify step hits `/api/v1/ai/tools/classify-and-label` — all pre-existing backend from #6898. - Known parity behavior (matches the editor hook): a transient failure loading team labels falls back to showing the defaults; not changed here to avoid diverging the two hooks.
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
// Classification labels. The SOURCE OF TRUTH is the co-located static JSON
|
|
// (`classificationLabels.json`), imported here and shaped into typed objects —
|
|
// edit THAT file, not this one. It's a fixed, built-in vocabulary shared by
|
|
// everyone (no per-team customization); it drives the sidebar's grouping, icons,
|
|
// and display names.
|
|
//
|
|
// The backend keeps a SECOND copy of this list
|
|
// (`app/proprietary/src/main/resources/classification/classification-labels.json`)
|
|
// which it sends to the engine per classify request (the engine holds no
|
|
// vocabulary of its own). The two copies must not drift — `classificationLabels.drift.test.ts`
|
|
// guards that. When you edit the labels here, update the backend copy too.
|
|
//
|
|
// `labels` is the flat set: each has a stable `id` (slug — the value on the wire,
|
|
// in storage and keyed on) and a human `name` (display, translatable via
|
|
// `classification.labels.<id>`); `icon` is presentational only, the engine never
|
|
// sees it. `families` are presentational sidebar roll-ups (referencing labels by
|
|
// id) the classifier never sees.
|
|
|
|
import labelsData from "@app/data/classificationLabels.json";
|
|
|
|
export interface ClassificationLabel {
|
|
/** Stable identity (slug) — the value on the wire, stored on the doc, and
|
|
* keyed on. Independent of the (translatable) display name. */
|
|
id: string;
|
|
/** Human display name; the en-US default for `classification.labels.<id>`. */
|
|
name: string;
|
|
/** Material Symbols icon key (see `labelIcons.ts`). */
|
|
icon?: string;
|
|
}
|
|
|
|
export interface LabelFamily {
|
|
/** Stable identity for sidebar prefs — never rename once shipped. */
|
|
id: string;
|
|
/** Group header text shown in the sidebar and the group picker. */
|
|
name: string;
|
|
/** Material Symbols icon key (see `labelIcons.ts`). */
|
|
icon: string;
|
|
/** The built-in labels this family rolls up in the sidebar. */
|
|
labels: ClassificationLabel[];
|
|
}
|
|
|
|
/** Shape of `classificationLabels.json` — the flat label set plus the
|
|
* presentational family grouping (which references labels by id). */
|
|
interface LabelsFile {
|
|
labels: ClassificationLabel[];
|
|
families: { id: string; name: string; icon: string; labelIds: string[] }[];
|
|
}
|
|
|
|
const data = labelsData as LabelsFile;
|
|
|
|
/** Flat default label set — file order, as the classifier/team-seed sees it. */
|
|
export const DEFAULT_CLASSIFICATION_LABELS: ClassificationLabel[] = data.labels;
|
|
|
|
const LABEL_BY_ID = new Map(data.labels.map((label) => [label.id, label]));
|
|
|
|
/**
|
|
* Built-in families with their labels resolved from the flat set by id. Throws
|
|
* at module load if a family references an unknown id, so a bad hand-edit of the
|
|
* JSON fails fast rather than silently dropping a label from its group.
|
|
*/
|
|
export const LABEL_FAMILIES: LabelFamily[] = data.families.map((family) => ({
|
|
id: family.id,
|
|
name: family.name,
|
|
icon: family.icon,
|
|
labels: family.labelIds.map((id) => {
|
|
const label = LABEL_BY_ID.get(id);
|
|
if (!label) {
|
|
throw new Error(
|
|
`Classification family "${family.id}" references unknown label id "${id}"`,
|
|
);
|
|
}
|
|
return label;
|
|
}),
|
|
}));
|
|
|
|
/**
|
|
* Stable slug id from a label's canonical (English) name — used to derive an id
|
|
* for a NEW custom label the user types. Built-in ids are authored in the JSON;
|
|
* this must stay in sync with the slug rule used to generate them.
|
|
*/
|
|
export function labelId(name: string): string {
|
|
return name
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
}
|