feat: status group with template filtering

This commit is contained in:
Aleix Soler 2025-07-20 11:00:00 +02:00
parent 5863ab6f51
commit 0f6499907c
8 changed files with 190 additions and 19 deletions

View file

@ -6,6 +6,7 @@ import {
GroupedDataProvider,
useGroupedDataContext,
} from "./context/GroupedDataProvider";
import { NoteStatus } from "@/types/noteStatus";
export type FileItem = {
id: string;
@ -33,6 +34,7 @@ export type GroupedStatusViewProps = {
onFileClick: (file: FileItem) => void;
subscribeToEvents: (onDataChange: () => void) => () => void;
getAvailableStatuses: () => StatusItem[];
getAvailableStatusesWithTemplateInfo: () => NoteStatus[];
};
const GroupedStatusViewContent = () => {
@ -44,7 +46,7 @@ const GroupedStatusViewContent = () => {
toggleGroup,
toggleFiles,
onFileClick,
getAvailableStatuses,
getAvailableStatusesWithTemplateInfo,
getLoadedCount,
loadMoreItems,
handleScroll,
@ -57,14 +59,26 @@ const GroupedStatusViewContent = () => {
[onFileClick],
);
const availableStatuses = useMemo(
() => getAvailableStatuses(),
[getAvailableStatuses],
);
const statusMap = useMemo(
() => new Map(availableStatuses.map((s) => [s.name, s])),
[availableStatuses],
);
const statusMap = useMemo(() => {
const map = new Map();
const noteStatuses = getAvailableStatusesWithTemplateInfo();
// Build map using scoped identifiers as keys
noteStatuses.forEach((noteStatus) => {
const statusItem: StatusItem = {
name: noteStatus.name,
color: noteStatus.color || "white",
icon: noteStatus.icon,
};
const scopedIdentifier = noteStatus.templateId
? `${noteStatus.templateId}:${noteStatus.name}`
: noteStatus.name;
map.set(scopedIdentifier, statusItem);
});
return map;
}, [getAvailableStatusesWithTemplateInfo]);
if (isLoading) {
return <LoadingSpinner />;
@ -107,9 +121,22 @@ export const GroupedStatusView = ({
onFileClick,
subscribeToEvents,
getAvailableStatuses,
getAvailableStatusesWithTemplateInfo,
}: GroupedStatusViewProps) => {
const [searchFilter, setSearchFilter] = useState("");
const [noteNameFilter, setNoteNameFilter] = useState("");
const [templateFilter, setTemplateFilter] = useState("");
// Get available templates from the statuses
const availableTemplates = useMemo(() => {
const templates = new Set<string>();
getAvailableStatusesWithTemplateInfo().forEach((status) => {
if (status.templateId) {
templates.add(status.templateId);
}
});
return Array.from(templates).sort();
}, [getAvailableStatusesWithTemplateInfo]);
return (
<GroupedDataProvider
@ -118,15 +145,22 @@ export const GroupedStatusView = ({
onFileClick={onFileClick}
subscribeToEvents={subscribeToEvents}
getAvailableStatuses={getAvailableStatuses}
getAvailableStatusesWithTemplateInfo={
getAvailableStatusesWithTemplateInfo
}
searchFilter={searchFilter}
noteNameFilter={noteNameFilter}
templateFilter={templateFilter}
>
<div className="">
<FilterSection
searchFilter={searchFilter}
noteNameFilter={noteNameFilter}
templateFilter={templateFilter}
availableTemplates={availableTemplates}
onSearchFilterChange={setSearchFilter}
onNoteNameFilterChange={setNoteNameFilter}
onTemplateFilterChange={setTemplateFilter}
/>
<GroupedStatusViewContent />
</div>

View file

@ -5,15 +5,21 @@ import { Input } from "@/components/atoms/Input";
interface FilterSectionProps {
searchFilter: string;
noteNameFilter: string;
templateFilter: string;
availableTemplates: string[];
onSearchFilterChange: (value: string) => void;
onNoteNameFilterChange: (value: string) => void;
onTemplateFilterChange: (value: string) => void;
}
export const FilterSection = ({
searchFilter,
noteNameFilter,
templateFilter,
availableTemplates,
onSearchFilterChange,
onNoteNameFilterChange,
onTemplateFilterChange,
}: FilterSectionProps) => {
return (
<div className="grouped-status-header">
@ -23,15 +29,29 @@ export const FilterSection = ({
value={searchFilter}
onFilterChange={onSearchFilterChange}
/>
<div className="grouped-status-note-filter">
<div className="grouped-status-filters__note">
<Input
variant="text"
value={noteNameFilter}
onChange={onNoteNameFilterChange}
placeholder="Filter by note name..."
className="grouped-status-note-input"
className="grouped-status-filters__note-input"
/>
</div>
<div className="grouped-status-filters__template">
<select
value={templateFilter}
onChange={(e) => onTemplateFilterChange(e.target.value)}
className="grouped-status-filters__template-select"
>
<option value="">All templates</option>
{availableTemplates.map((template) => (
<option key={template} value={template}>
{template}
</option>
))}
</select>
</div>
</div>
</div>
);

View file

@ -36,6 +36,11 @@ export const StatusGroup = ({
}: StatusGroupProps) => {
const groupKey = `${tag}-${statusName}`;
// Extract template ID from scoped identifier
const templateId = statusName.includes(":")
? statusName.split(":", 2)[0]
: null;
const handleToggle = useCallback(() => {
onToggle();
}, [onToggle]);
@ -43,12 +48,19 @@ export const StatusGroup = ({
return (
<div className="grouped-status-group">
<div className="grouped-status-group-header" onClick={handleToggle}>
<StatusDisplay
status={
{ ...status, icon: status.icon || "" } as NoteStatus
}
variant="badge"
/>
<div className="grouped-status-group__status">
<StatusDisplay
status={
{ ...status, icon: status.icon || "" } as NoteStatus
}
variant="badge"
/>
{templateId && (
<span className="grouped-status-group__template-badge">
{templateId}
</span>
)}
</div>
<div className="grouped-status-group-info">
<CollapsibleCounter
count={files.length}

View file

@ -1,5 +1,6 @@
import React, { createContext, useContext, ReactNode } from "react";
import { FileItem, GroupedByStatus, StatusItem } from "../GroupedStatusView";
import { NoteStatus } from "@/types/noteStatus";
import { useGroupedData } from "../hooks/useGroupedData";
import { usePagination } from "../hooks/usePagination";
import { useExpandedState } from "../hooks/useExpandedState";
@ -29,6 +30,7 @@ type GroupedDataContextType = {
// Props
onFileClick: (file: FileItem) => void;
getAvailableStatuses: () => StatusItem[];
getAvailableStatusesWithTemplateInfo: () => NoteStatus[];
};
const GroupedDataContext = createContext<GroupedDataContextType | undefined>(
@ -52,8 +54,10 @@ type GroupedDataProviderProps = {
onFileClick: (file: FileItem) => void;
subscribeToEvents: (onDataChange: () => void) => () => void;
getAvailableStatuses: () => StatusItem[];
getAvailableStatusesWithTemplateInfo: () => NoteStatus[];
searchFilter: string;
noteNameFilter: string;
templateFilter: string;
};
export const GroupedDataProvider = ({
@ -63,8 +67,10 @@ export const GroupedDataProvider = ({
onFileClick,
subscribeToEvents,
getAvailableStatuses,
getAvailableStatusesWithTemplateInfo,
searchFilter,
noteNameFilter,
templateFilter,
}: GroupedDataProviderProps) => {
const { groupedData, filteredData, isLoading, loadData } = useGroupedData({
getAllFiles,
@ -72,6 +78,7 @@ export const GroupedDataProvider = ({
subscribeToEvents,
searchFilter,
noteNameFilter,
templateFilter,
});
const { getLoadedCount, loadMoreItems, handleScroll } = usePagination();
@ -93,6 +100,7 @@ export const GroupedDataProvider = ({
toggleFiles,
onFileClick,
getAvailableStatuses,
getAvailableStatusesWithTemplateInfo,
};
return (

View file

@ -7,6 +7,7 @@ export type UseGroupedDataProps = {
subscribeToEvents: (onDataChange: () => void) => () => void;
searchFilter: string;
noteNameFilter: string;
templateFilter: string;
};
export const useGroupedData = ({
@ -15,6 +16,7 @@ export const useGroupedData = ({
subscribeToEvents,
searchFilter,
noteNameFilter,
templateFilter,
}: UseGroupedDataProps) => {
const [groupedData, setGroupedData] = useState<GroupedByStatus>({});
const [isLoading, setIsLoading] = useState(true);
@ -43,7 +45,12 @@ export const useGroupedData = ({
}, [loadData, subscribeToEvents]);
const filteredData = useMemo(() => {
if (!searchFilter.trim() && !noteNameFilter.trim()) return groupedData;
if (
!searchFilter.trim() &&
!noteNameFilter.trim() &&
!templateFilter.trim()
)
return groupedData;
const filtered: GroupedByStatus = {};
const searchLower = searchFilter.toLowerCase();
@ -52,6 +59,18 @@ export const useGroupedData = ({
Object.entries(groupedData).forEach(([tag, statusGroups]) => {
filtered[tag] = {};
Object.entries(statusGroups).forEach(([statusName, files]) => {
// Filter by template if templateFilter is provided
if (templateFilter.trim()) {
// Check if statusName contains the template (scoped: "template:status")
const statusTemplate = statusName.includes(":")
? statusName.split(":", 2)[0]
: "";
if (statusTemplate !== templateFilter) {
return; // Skip this status group if it doesn't match the template filter
}
}
let matchingFiles = files;
// Filter by status/tag if searchFilter is provided
@ -79,7 +98,7 @@ export const useGroupedData = ({
});
return filtered;
}, [groupedData, searchFilter, noteNameFilter]);
}, [groupedData, searchFilter, noteNameFilter, templateFilter]);
return {
groupedData,

View file

@ -137,6 +137,10 @@ export class GroupedDashboardView extends ItemView {
return statuses.map(this.convertStatusToStatusItem);
};
private getAvailableStatusesWithTemplateInfo = () => {
return BaseNoteStatusService.getAllAvailableStatuses();
};
private handleFileClick = (file: FileItem) => {
const tFile = BaseNoteStatusService.app.vault.getAbstractFileByPath(
file.path,
@ -201,6 +205,9 @@ export class GroupedDashboardView extends ItemView {
onFileClick={this.handleFileClick}
subscribeToEvents={this.subscribeToEvents}
getAvailableStatuses={this.getAvailableStatuses}
getAvailableStatusesWithTemplateInfo={
this.getAvailableStatusesWithTemplateInfo
}
/>,
);
}

View file

@ -122,6 +122,10 @@ export class GroupedStatusView extends ItemView {
return statuses.map(this.convertStatusToStatusItem);
};
private getAvailableStatusesWithTemplateInfo = () => {
return BaseNoteStatusService.getAllAvailableStatuses();
};
private handleFileClick = (file: FileItem) => {
const tFile = BaseNoteStatusService.app.vault.getAbstractFileByPath(
file.path,
@ -186,6 +190,9 @@ export class GroupedStatusView extends ItemView {
onFileClick={this.handleFileClick}
subscribeToEvents={this.subscribeToEvents}
getAvailableStatuses={this.getAvailableStatuses}
getAvailableStatusesWithTemplateInfo={
this.getAvailableStatusesWithTemplateInfo
}
/>,
);
}

View file

@ -194,3 +194,67 @@
.grouped-status-files-list::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-active-bg);
}
/* Template Filter (BEM) */
.grouped-status-filters__template {
display: flex;
flex-direction: column;
gap: var(--size-2-1);
}
.grouped-status-filters__template-select {
width: 100%;
padding: var(--size-2-2) var(--size-2-3);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-small);
cursor: pointer;
}
.grouped-status-filters__template-select:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--background-modifier-accent);
}
.grouped-status-filters__note {
display: flex;
flex-direction: column;
gap: var(--size-2-1);
}
.grouped-status-filters__note-input {
width: 100%;
padding: var(--size-2-2) var(--size-2-3);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-small);
}
.grouped-status-filters__note-input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--background-modifier-accent);
}
/* Template Badge (BEM) */
.grouped-status-group__status {
display: flex;
align-items: center;
gap: var(--size-2-2);
}
.grouped-status-group__template-badge {
background: var(--background-modifier-border);
color: var(--text-muted);
padding: var(--size-2-1) var(--size-2-2);
border-radius: var(--radius-s);
font-size: var(--font-ui-smaller);
font-weight: var(--font-medium);
text-transform: capitalize;
white-space: nowrap;
}