mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
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:
commit
aa3ee7e963
4 changed files with 130 additions and 2 deletions
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue