mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: status group with template filtering
This commit is contained in:
parent
5863ab6f51
commit
0f6499907c
8 changed files with 190 additions and 19 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue