Merge pull request #68 from devonthesofa/feature/status-selector-ui

[feature] Add template-based status grouping with switchable views in status picker
This commit is contained in:
Aleix Soler 2025-11-18 17:45:00 +01:00 committed by GitHub
commit aa3ee7e963
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 130 additions and 2 deletions

View file

@ -4,11 +4,13 @@ import {
StatusSelectorGroup,
Props as StatusSelectorGroupProps,
} from "./StatusSelectorGroup";
import { StatusTemplate } from "@/types/pluginSettings";
export interface Props {
currentStatuses: GroupedStatuses;
filesQuantity: number;
availableStatuses: NoteStatus[];
templates: StatusTemplate[];
onRemoveStatus: (
frontmatterTagName: string,
status: NoteStatus,
@ -23,6 +25,7 @@ export const ChangeStatusModal: React.FC<Props> = ({
currentStatuses: initialStatuses,
filesQuantity,
availableStatuses,
templates,
onRemoveStatus,
onSelectStatus,
}) => {
@ -53,6 +56,7 @@ export const ChangeStatusModal: React.FC<Props> = ({
frontmatterTagName={frontmatterTagName}
availableStatuses={availableStatuses}
currentStatuses={statusList}
templates={templates}
onSelectedState={handleSelectedState}
/>
))}

View file

@ -1,15 +1,17 @@
import React, { useCallback } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { NoteStatus } from "@/types/noteStatus";
import { SearchFilter } from "../atoms/SearchFilter";
import { StatusSelector } from "../atoms/StatusSelector";
import { SettingItem } from "../SettingsUI/SettingItem";
import { CurrentStatusChips } from "./CurrentStatusChips";
import { useKeyboardNavigation } from "./useKeyboardNavigation";
import { StatusTemplate } from "@/types/pluginSettings";
export interface Props {
frontmatterTagName: string;
currentStatuses: NoteStatus[];
availableStatuses: NoteStatus[];
templates: StatusTemplate[];
onSelectedState: (
frontmatterTagName: string,
status: NoteStatus,
@ -20,9 +22,13 @@ export interface Props {
export const StatusSelectorGroup: React.FC<Props> = ({
currentStatuses,
availableStatuses,
templates,
frontmatterTagName,
onSelectedState,
}) => {
const TEMPLATE_ALL_VIEW = "__all__";
const TEMPLATE_CUSTOM_VIEW = "__custom__";
const handleRemoveStatus = useCallback(
(status: NoteStatus) => {
onSelectedState(frontmatterTagName, status, "unselected");
@ -37,6 +43,92 @@ export const StatusSelectorGroup: React.FC<Props> = ({
[onSelectedState, frontmatterTagName],
);
type TemplateViewOption = {
id: string;
label: string;
statuses: NoteStatus[];
};
const templateViewOptions = useMemo<TemplateViewOption[]>(() => {
const statusesByTemplate = new Map<string, NoteStatus[]>();
const customStatuses: NoteStatus[] = [];
availableStatuses.forEach((status) => {
if (status.templateId) {
const bucket = statusesByTemplate.get(status.templateId) ?? [];
bucket.push(status);
statusesByTemplate.set(status.templateId, bucket);
} else {
customStatuses.push(status);
}
});
const templateOrder = templates.map((template) => template.id);
const templateNameMap = new Map(
templates.map((template) => [template.id, template.name]),
);
const views: TemplateViewOption[] = [
{
id: TEMPLATE_ALL_VIEW,
label: "All",
statuses: availableStatuses,
},
];
templateOrder.forEach((templateId) => {
const statuses = statusesByTemplate.get(templateId);
if (statuses?.length) {
views.push({
id: templateId,
label: templateNameMap.get(templateId) ?? templateId,
statuses,
});
statusesByTemplate.delete(templateId);
}
});
// Include any template-defined statuses that aren't present in the settings order
statusesByTemplate.forEach((statuses, templateId) => {
if (statuses.length) {
views.push({
id: templateId,
label: templateNameMap.get(templateId) ?? templateId,
statuses,
});
}
});
if (customStatuses.length) {
views.push({
id: TEMPLATE_CUSTOM_VIEW,
label: "Custom",
statuses: customStatuses,
});
}
return views;
}, [availableStatuses, templates]);
const [selectedTemplateId, setSelectedTemplateId] =
useState<string>(TEMPLATE_ALL_VIEW);
useEffect(() => {
const exists = templateViewOptions.some(
(option) => option.id === selectedTemplateId,
);
if (!exists) {
setSelectedTemplateId(TEMPLATE_ALL_VIEW);
}
}, [templateViewOptions, selectedTemplateId]);
const activeTemplate =
templateViewOptions.find(
(option) => option.id === selectedTemplateId,
) ?? templateViewOptions[0];
const statusesForView = activeTemplate?.statuses ?? [];
const {
focusedIndex,
searchFilter,
@ -46,7 +138,7 @@ export const StatusSelectorGroup: React.FC<Props> = ({
handleKeyDown,
setSearchFilter,
} = useKeyboardNavigation({
availableStatuses,
availableStatuses: statusesForView,
currentStatuses,
onSelectStatus: handleSelectStatus,
onRemoveStatus: handleRemoveStatus,
@ -59,6 +151,31 @@ export const StatusSelectorGroup: React.FC<Props> = ({
onKeyDown={handleKeyDown}
style={{ outline: "none" }}
>
{templateViewOptions.length > 1 && (
<div
className="note-status-template-tabs"
role="tablist"
aria-label="Status templates"
>
{templateViewOptions.map((option) => (
<button
type="button"
role="tab"
key={option.id}
className={`note-status-template-tab${
option.id === selectedTemplateId
? " is-active"
: ""
}`}
onClick={() => setSelectedTemplateId(option.id)}
aria-selected={option.id === selectedTemplateId}
>
{option.label}
</button>
))}
</div>
)}
<SearchFilter
ref={searchRef}
value={searchFilter}

View file

@ -11,6 +11,7 @@ import {
BaseNoteStatusService,
} from "@/core/noteStatusService";
import eventBus from "@/core/eventBus";
import settingsService from "@/core/settingsService";
export class StatusModalIntegration extends Modal {
private root: Root | null = null;
@ -116,6 +117,7 @@ export class StatusModalIntegration extends Modal {
this.selectorService.noteStatusService.statuses ?? {}
}
filesQuantity={filesQuantity}
templates={settingsService.settings.templates}
onRemoveStatus={this.onRemoveStatus}
onSelectStatus={this.onSelectStatus}
/>,

File diff suppressed because one or more lines are too long