mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
feat(processing-folders): My Files filters by classification category
The categories existed everywhere except anywhere you could act on them: the sidebar groups by them and cards wear their labels, but the files page had no way to see "just the Finance documents". The toolbar now carries a category filter beside the source and type filters, offering the same visible families in the same order the sidebar groups by, and keeping the files whose labels fall under the chosen family. The options come through the grouping seam (useCategoryFilterOptions): core, which has no classification, offers none and the dropdown never renders.
This commit is contained in:
@@ -4037,6 +4037,10 @@ icon = "Icon"
|
||||
title = "Appearance"
|
||||
useColour = "Use color {{c}}"
|
||||
|
||||
[filesPage.categoryFilter]
|
||||
all = "All categories"
|
||||
label = "Filter by category"
|
||||
|
||||
[filesPage.column]
|
||||
modified = "Modified"
|
||||
name = "Name"
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
useProcessingFolders,
|
||||
type ProcessingRunInfo,
|
||||
} from "@app/hooks/useProcessingFolders";
|
||||
import { useCategoryFilterOptions } from "@app/components/shared/fileSidebarGrouping";
|
||||
import SuperSearch from "@app/components/shared/superSearch/SuperSearch";
|
||||
import { useEditorSearchScopes } from "@app/hooks/useSuperSearch";
|
||||
import { FileDetailsPanel } from "@app/components/filesPage/FileDetailsPanel";
|
||||
@@ -385,6 +386,16 @@ export default function FileManagerView() {
|
||||
}
|
||||
}, [availableTypes, typeFilter, setTypeFilter]);
|
||||
|
||||
// Category filter over the classification families the sidebar groups by;
|
||||
// empty (core, classification off) means the dropdown never renders.
|
||||
const categoryOptions = useCategoryFilterOptions();
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>("all");
|
||||
const categoryLabelKeys = useMemo(() => {
|
||||
if (categoryFilter === "all") return null;
|
||||
const option = categoryOptions.find((c) => c.id === categoryFilter);
|
||||
return option ? new Set(option.labelKeys) : null;
|
||||
}, [categoryFilter, categoryOptions]);
|
||||
|
||||
const visibleFiles = useMemo(() => {
|
||||
const filtered = filesInCurrentFolder
|
||||
.filter((f) =>
|
||||
@@ -397,7 +408,14 @@ export default function FileManagerView() {
|
||||
if (typeFilter.length === 0) return true;
|
||||
const ext = (f.name.split(".").pop() ?? "").toUpperCase();
|
||||
return typeFilter.includes(ext);
|
||||
});
|
||||
})
|
||||
.filter(
|
||||
(f) =>
|
||||
!categoryLabelKeys ||
|
||||
(f.classificationLabels ?? []).some((label) =>
|
||||
categoryLabelKeys.has(label),
|
||||
),
|
||||
);
|
||||
const sorted = [...filtered];
|
||||
sorted.sort((a, b) => {
|
||||
switch (sortMode) {
|
||||
@@ -417,7 +435,14 @@ export default function FileManagerView() {
|
||||
}
|
||||
});
|
||||
return sorted;
|
||||
}, [filesInCurrentFolder, search, sortMode, originFilter, typeFilter]);
|
||||
}, [
|
||||
filesInCurrentFolder,
|
||||
search,
|
||||
sortMode,
|
||||
originFilter,
|
||||
typeFilter,
|
||||
categoryLabelKeys,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Resolve a folder id to its breadcrumb path (e.g. "Receipts / 2024 / Q1").
|
||||
@@ -1746,6 +1771,31 @@ export default function FileManagerView() {
|
||||
style={{ width: 140 }}
|
||||
aria-label={t("filesPage.originFilter", "Filter by source")}
|
||||
/>
|
||||
{categoryOptions.length > 0 && (
|
||||
<Select
|
||||
size="xs"
|
||||
value={categoryFilter}
|
||||
onChange={(value) => setCategoryFilter(value ?? "all")}
|
||||
data={[
|
||||
{
|
||||
value: "all",
|
||||
label: t(
|
||||
"filesPage.categoryFilter.all",
|
||||
"All categories",
|
||||
),
|
||||
},
|
||||
...categoryOptions.map((category) => ({
|
||||
value: category.id,
|
||||
label: category.name,
|
||||
})),
|
||||
]}
|
||||
style={{ width: 150 }}
|
||||
aria-label={t(
|
||||
"filesPage.categoryFilter.label",
|
||||
"Filter by category",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{availableTypes.length > 1 && (
|
||||
<MultiSelect
|
||||
size="xs"
|
||||
|
||||
@@ -43,6 +43,21 @@ export function useLabelBadges(_labels?: string[] | null): LabelBadge[] {
|
||||
return NO_BADGES;
|
||||
}
|
||||
|
||||
/** One category (label family) as a files-page filter offers it. */
|
||||
export interface CategoryFilterOption {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Label ids the category rolls up — a file matches if it carries any. */
|
||||
labelKeys: string[];
|
||||
}
|
||||
|
||||
const NO_CATEGORIES: CategoryFilterOption[] = [];
|
||||
|
||||
/** Categories to filter by; core (no classification) offers none. */
|
||||
export function useCategoryFilterOptions(): CategoryFilterOption[] {
|
||||
return NO_CATEGORIES;
|
||||
}
|
||||
|
||||
// Header control for customizing the grouping; core has none, an override renders a group picker.
|
||||
export function FileSidebarGroupControls(_props: {
|
||||
stubs: StirlingFileStub[];
|
||||
|
||||
@@ -23,6 +23,7 @@ import { scheduleIdle } from "@app/utils/scheduleIdle";
|
||||
import type { FileId } from "@app/types/file";
|
||||
import type { StirlingFileStub } from "@app/types/fileContext";
|
||||
import type {
|
||||
CategoryFilterOption,
|
||||
FileSidebarGroup,
|
||||
LabelBadge,
|
||||
} from "@core/components/shared/fileSidebarGrouping";
|
||||
@@ -31,7 +32,10 @@ import { DEFAULT_LABEL_ICON } from "@app/data/labelIcons";
|
||||
import { accentColor, accentCycleColor } from "@app/utils/accentColors";
|
||||
|
||||
export type { FileSidebarGroup };
|
||||
export type { LabelBadge } from "@core/components/shared/fileSidebarGrouping";
|
||||
export type {
|
||||
CategoryFilterOption,
|
||||
LabelBadge,
|
||||
} from "@core/components/shared/fileSidebarGrouping";
|
||||
// Pure grouping logic lives in a component-free module so tests don't drag in the picker's UI deps.
|
||||
export {
|
||||
buildLabelGroups,
|
||||
@@ -115,6 +119,32 @@ export function useFileSidebarGroups(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The visible categories as filter options, in the sidebar's own display
|
||||
* order — the files-page category filter and the sidebar groups must name
|
||||
* and order the world identically.
|
||||
*/
|
||||
export function useCategoryFilterOptions(): CategoryFilterOption[] {
|
||||
const categories = useSyncExternalStore(
|
||||
subscribeSidebarCategories,
|
||||
getSidebarCategories,
|
||||
);
|
||||
return useMemo(
|
||||
() =>
|
||||
categories
|
||||
.filter((category) => !category.hidden)
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
|
||||
)
|
||||
.map((category) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
labelKeys: [...category.labelKeys],
|
||||
})),
|
||||
[categories],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge descriptors for a file's labels: each label's own icon from the
|
||||
* classification vocabulary, coloured with the accent its category cycles to
|
||||
|
||||
Reference in New Issue
Block a user