2026-07-15 12:04:30 +01:00
|
|
|
// The Files-sidebar category picker: a "tune" button opening a modal that lists the fixed, shared
|
|
|
|
|
// categories and lets the user show or hide each one in their own sidebar. Device-local and
|
|
|
|
|
// presentational only — it never changes the shared categories or the label vocabulary; files in a
|
|
|
|
|
// hidden (or no) category fall back to "Other".
|
2026-07-09 12:47:37 +01:00
|
|
|
|
|
|
|
|
import { useMemo, useState, useSyncExternalStore } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import TuneIcon from "@mui/icons-material/Tune";
|
|
|
|
|
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|
|
|
|
import { Modal } from "@app/ui/Modal";
|
|
|
|
|
import { Button } from "@app/ui/Button";
|
|
|
|
|
import { ActionIcon } from "@app/ui/ActionIcon";
|
2026-07-15 12:04:30 +01:00
|
|
|
import { ClassificationCategoryManager } from "@app/components/policies/ClassificationCategoryManager";
|
2026-07-09 12:47:37 +01:00
|
|
|
import { useClassificationEnabled } from "@app/hooks/useClassificationEnabled";
|
|
|
|
|
import { bucketStubsByLabel } from "@app/components/shared/fileSidebarGroupingLogic";
|
|
|
|
|
import {
|
|
|
|
|
getSidebarCategories,
|
2026-07-15 12:04:30 +01:00
|
|
|
resetHiddenCategories,
|
2026-07-09 12:47:37 +01:00
|
|
|
setCategoryHidden,
|
|
|
|
|
subscribeSidebarCategories,
|
|
|
|
|
} from "@app/services/fileSidebarCategories";
|
|
|
|
|
import type { StirlingFileStub } from "@app/types/fileContext";
|
|
|
|
|
import "@app/components/shared/FileSidebarGroupControls.css";
|
|
|
|
|
|
|
|
|
|
interface FileSidebarGroupControlsProps {
|
2026-07-15 12:04:30 +01:00
|
|
|
/** The files currently listed, for live per-category counts. */
|
2026-07-09 12:47:37 +01:00
|
|
|
stubs: StirlingFileStub[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function FileSidebarGroupControls({
|
|
|
|
|
stubs,
|
|
|
|
|
}: FileSidebarGroupControlsProps) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const enabled = useClassificationEnabled();
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const categories = useSyncExternalStore(
|
|
|
|
|
subscribeSidebarCategories,
|
|
|
|
|
getSidebarCategories,
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-15 12:04:30 +01:00
|
|
|
// Files per category (deduped across its labels), from the same bucketing the sidebar groups use.
|
2026-07-09 12:47:37 +01:00
|
|
|
const categoryCounts = useMemo(() => {
|
2026-07-15 12:04:30 +01:00
|
|
|
const byLabel = bucketStubsByLabel(stubs);
|
2026-07-09 12:47:37 +01:00
|
|
|
const counts = new Map<string, number>();
|
|
|
|
|
for (const category of categories) {
|
|
|
|
|
const ids = new Set<string>();
|
|
|
|
|
for (const key of category.labelKeys) {
|
|
|
|
|
for (const stub of byLabel.get(key)?.stubs ?? []) {
|
2026-08-26 14:09:55 +00:00
|
|
|
ids.add(stub.id);
|
2026-07-09 12:47:37 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
counts.set(category.id, ids.size);
|
|
|
|
|
}
|
|
|
|
|
return counts;
|
2026-07-15 12:04:30 +01:00
|
|
|
}, [stubs, categories]);
|
2026-07-09 12:47:37 +01:00
|
|
|
|
|
|
|
|
// Classification off (non-AI SaaS tenant) → no "customize groups" affordance,
|
|
|
|
|
// matching the flat, ungrouped list. (All hooks above run unconditionally.)
|
|
|
|
|
if (!enabled) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* -external: revealed on section-header hover, like the Browse button. */}
|
|
|
|
|
<ActionIcon
|
|
|
|
|
variant="quiet"
|
|
|
|
|
className="file-sidebar-section-btn file-sidebar-section-btn-external"
|
2026-07-15 12:04:30 +01:00
|
|
|
onClick={() => setOpen(true)}
|
2026-07-09 12:47:37 +01:00
|
|
|
aria-label={t("fileSidebar.customizeGroups", "Customize groups")}
|
|
|
|
|
data-testid="customize-groups"
|
|
|
|
|
>
|
|
|
|
|
<TuneIcon sx={{ fontSize: "1rem" }} />
|
|
|
|
|
</ActionIcon>
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
open={open}
|
|
|
|
|
onClose={() => setOpen(false)}
|
2026-07-15 12:04:30 +01:00
|
|
|
width="md"
|
2026-07-09 12:47:37 +01:00
|
|
|
title={t("fileSidebar.groupsModal.title", "Sidebar categories")}
|
|
|
|
|
subtitle={t(
|
|
|
|
|
"fileSidebar.groupsModal.subtitle",
|
2026-07-15 12:04:30 +01:00
|
|
|
"Show or hide categories in the files sidebar.",
|
2026-07-09 12:47:37 +01:00
|
|
|
)}
|
|
|
|
|
footer={
|
|
|
|
|
<div className="fsg-footer">
|
|
|
|
|
<Button
|
|
|
|
|
variant="tertiary"
|
|
|
|
|
size="sm"
|
|
|
|
|
leftSection={<RestartAltIcon sx={{ fontSize: "1rem" }} />}
|
2026-07-15 12:04:30 +01:00
|
|
|
onClick={resetHiddenCategories}
|
2026-07-09 12:47:37 +01:00
|
|
|
>
|
2026-07-15 12:04:30 +01:00
|
|
|
{t("fileSidebar.groupsModal.reset", "Show all")}
|
2026-07-09 12:47:37 +01:00
|
|
|
</Button>
|
|
|
|
|
<Button variant="primary" size="sm" onClick={() => setOpen(false)}>
|
|
|
|
|
{t("fileSidebar.groupsModal.done", "Done")}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<div className="fsg-body">
|
2026-07-15 12:04:30 +01:00
|
|
|
<ClassificationCategoryManager
|
|
|
|
|
categories={categories}
|
|
|
|
|
onToggleHidden={setCategoryHidden}
|
|
|
|
|
counts={categoryCounts}
|
2026-07-09 12:47:37 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|