read the task category order from the items store
CATEGORY_ORDER hardcoded Video/Audio/Cleanup in the frontend, so adding a category meant a code change. The order now lives in categories.yaml at the root of the items store and reaches the client via GET /tasks/categories — the platform no longer knows any category by name. The endpoint is declared before /:name, which would otherwise match "categories". Categories used by a task but absent from the file still work: they sort alphabetically after the listed ones, and Other stays last. Menus consume grouped tasks rather than grouping them per row. Groups are built from the tasks and the file only ranks them, so a category listed with no matching tasks cannot produce an empty submenu — locked in by tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a77ec22df
commit
5f7d574dec
@@ -1,6 +1,6 @@
|
|||||||
import { readdir, mkdir, rm } from 'node:fs/promises';
|
import { readdir, mkdir, rm } from 'node:fs/promises';
|
||||||
import { join, dirname } from 'node:path';
|
import { join, dirname } from 'node:path';
|
||||||
import { itemsDir } from '../../data-path';
|
import { itemsDir, OFFICER_ITEMS_DIR } from '../../data-path';
|
||||||
|
|
||||||
// File-backed task store. Every task is a directory under $OFFICER_ITEMS_DIR/tasks/<dirName>/ with a
|
// File-backed task store. Every task is a directory under $OFFICER_ITEMS_DIR/tasks/<dirName>/ with a
|
||||||
// TASK.md (metadata + prose body) and, for script-mode tasks, a sibling implementation file. There are
|
// TASK.md (metadata + prose body) and, for script-mode tasks, a sibling implementation file. There are
|
||||||
@@ -149,6 +149,25 @@ async function readImplementation(dirName: string, language: string | null): Pro
|
|||||||
return (await file.exists()) ? file.text() : null;
|
return (await file.exists()) ? file.text() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// categories.yaml at the root of the items store lists the task categories in the order they should
|
||||||
|
// appear in the file browser's Run Task submenu. It is optional and purely presentational: the
|
||||||
|
// platform never decides a task's category, only how known ones sort. A missing, empty or malformed
|
||||||
|
// file leaves everything to alphabetical ordering.
|
||||||
|
export async function readCategoryOrder(): Promise<string[]> {
|
||||||
|
const file = Bun.file(join(OFFICER_ITEMS_DIR, 'categories.yaml'));
|
||||||
|
if (!(await file.exists())) return [];
|
||||||
|
try {
|
||||||
|
const parsed = YAML.parse(await file.text());
|
||||||
|
if (!Array.isArray(parsed)) return [];
|
||||||
|
return parsed
|
||||||
|
.filter((entry): entry is string => typeof entry === 'string')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function listTasks(): Promise<TaskSummary[]> {
|
export async function listTasks(): Promise<TaskSummary[]> {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createRouter } from '../../create-router';
|
import { createRouter } from '../../create-router';
|
||||||
import { listTasks, getTaskByDirName, createTask, deleteTask } from './task-files';
|
import { listTasks, getTaskByDirName, createTask, deleteTask, readCategoryOrder } from './task-files';
|
||||||
|
|
||||||
type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' };
|
type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' };
|
||||||
|
|
||||||
@@ -20,6 +20,11 @@ tasksRouter.get('/', async (ctx) => {
|
|||||||
return ctx.json(tasks);
|
return ctx.json(tasks);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Must be declared before '/:name', which would otherwise match "categories".
|
||||||
|
tasksRouter.get('/categories', async (ctx) => {
|
||||||
|
return ctx.json(await readCategoryOrder());
|
||||||
|
});
|
||||||
|
|
||||||
tasksRouter.get('/:name', async (ctx) => {
|
tasksRouter.get('/:name', async (ctx) => {
|
||||||
const name = ctx.req.param('name');
|
const name = ctx.req.param('name');
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -66,7 +66,7 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
|||||||
handleExtractAudio,
|
handleExtractAudio,
|
||||||
handleExtract,
|
handleExtract,
|
||||||
handlePlay,
|
handlePlay,
|
||||||
getMatchingTasks,
|
getMatchingTaskGroups,
|
||||||
handleRunTask,
|
handleRunTask,
|
||||||
handleCreateDashboard,
|
handleCreateDashboard,
|
||||||
fileScrollRef: scrollRef,
|
fileScrollRef: scrollRef,
|
||||||
@@ -185,7 +185,7 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
|||||||
onExtractAudio={handleExtractAudio}
|
onExtractAudio={handleExtractAudio}
|
||||||
onExtract={handleExtract}
|
onExtract={handleExtract}
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
matchingTasks={getMatchingTasks(entry.name, entry.type)}
|
taskGroups={getMatchingTaskGroups(entry.name, entry.type)}
|
||||||
onRunTask={handleRunTask}
|
onRunTask={handleRunTask}
|
||||||
onCreateDashboard={handleCreateDashboard}
|
onCreateDashboard={handleCreateDashboard}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+21
-31
@@ -42,7 +42,7 @@ import {
|
|||||||
} from '@/components/ui/context-menu';
|
} from '@/components/ui/context-menu';
|
||||||
import { cardStyle } from '@/components/Card';
|
import { cardStyle } from '@/components/Card';
|
||||||
import type { DirEntry } from '../../../../hooks/useFilesAPI';
|
import type { DirEntry } from '../../../../hooks/useFilesAPI';
|
||||||
import { groupTasksByCategory, type TaskSummary } from '../../useTasks';
|
import type { TaskGroup, TaskSummary } from '../../useTasks';
|
||||||
import { getFileType } from '../../../FileViewer';
|
import { getFileType } from '../../../FileViewer';
|
||||||
|
|
||||||
export type FileItemProps = {
|
export type FileItemProps = {
|
||||||
@@ -69,7 +69,7 @@ export type FileItemProps = {
|
|||||||
onExtractAudio: (entry: DirEntry) => void;
|
onExtractAudio: (entry: DirEntry) => void;
|
||||||
onExtract: (entry: DirEntry) => void;
|
onExtract: (entry: DirEntry) => void;
|
||||||
onPlay: (entry: DirEntry) => void;
|
onPlay: (entry: DirEntry) => void;
|
||||||
matchingTasks: TaskSummary[];
|
taskGroups: TaskGroup[];
|
||||||
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
||||||
onCreateDashboard: (entry: DirEntry) => void;
|
onCreateDashboard: (entry: DirEntry) => void;
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,7 @@ type MenuItemsProps = {
|
|||||||
onPlay: (e: DirEntry) => void;
|
onPlay: (e: DirEntry) => void;
|
||||||
onCut: () => void;
|
onCut: () => void;
|
||||||
onCopy: () => void;
|
onCopy: () => void;
|
||||||
matchingTasks: TaskSummary[];
|
taskGroups: TaskGroup[];
|
||||||
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
||||||
onCreateDashboard: (e: DirEntry) => void;
|
onCreateDashboard: (e: DirEntry) => void;
|
||||||
};
|
};
|
||||||
@@ -125,7 +125,7 @@ const DropdownMenuItems = ({
|
|||||||
onPlay,
|
onPlay,
|
||||||
onCut,
|
onCut,
|
||||||
onCopy,
|
onCopy,
|
||||||
matchingTasks,
|
taskGroups,
|
||||||
onRunTask,
|
onRunTask,
|
||||||
onCreateDashboard,
|
onCreateDashboard,
|
||||||
}: MenuItemsProps) => {
|
}: MenuItemsProps) => {
|
||||||
@@ -137,18 +137,13 @@ const DropdownMenuItems = ({
|
|||||||
const showExtract = fileType === 'archive';
|
const showExtract = fileType === 'archive';
|
||||||
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
||||||
|
|
||||||
|
const hasTasks = taskGroups.length > 0;
|
||||||
const hasActions =
|
const hasActions =
|
||||||
showPlay ||
|
showPlay || showReadAloud || showOcr || showTranscribe || showExtractAudio || showExtract || hasTasks;
|
||||||
showReadAloud ||
|
// Only nest when more than one category matched — a file usually matches a single category, and
|
||||||
showOcr ||
|
// Run Task > Video > Convert would just add a hop.
|
||||||
showTranscribe ||
|
|
||||||
showExtractAudio ||
|
|
||||||
showExtract ||
|
|
||||||
matchingTasks.length > 0;
|
|
||||||
// Only nest when more than one category is present — a file usually matches a single category,
|
|
||||||
// and Run Task > Video > Convert would just add a hop.
|
|
||||||
const taskGroups = groupTasksByCategory(matchingTasks);
|
|
||||||
const nestTasks = taskGroups.length > 1;
|
const nestTasks = taskGroups.length > 1;
|
||||||
|
const flatTasks = taskGroups[0]?.tasks ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -188,7 +183,7 @@ const DropdownMenuItems = ({
|
|||||||
Extract
|
Extract
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{matchingTasks.length > 0 && (
|
{hasTasks && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger className="cursor-pointer">
|
<DropdownMenuSubTrigger className="cursor-pointer">
|
||||||
<Play className="mr-2 h-4 w-4" />
|
<Play className="mr-2 h-4 w-4" />
|
||||||
@@ -212,7 +207,7 @@ const DropdownMenuItems = ({
|
|||||||
</DropdownMenuSubContent>
|
</DropdownMenuSubContent>
|
||||||
</DropdownMenuSub>
|
</DropdownMenuSub>
|
||||||
))
|
))
|
||||||
: matchingTasks.map((task) => (
|
: flatTasks.map((task) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={task.dirName}
|
key={task.dirName}
|
||||||
onClick={() => onRunTask(task, entry)}
|
onClick={() => onRunTask(task, entry)}
|
||||||
@@ -283,7 +278,7 @@ const ContextMenuItems = ({
|
|||||||
onPlay,
|
onPlay,
|
||||||
onCut,
|
onCut,
|
||||||
onCopy,
|
onCopy,
|
||||||
matchingTasks,
|
taskGroups,
|
||||||
onRunTask,
|
onRunTask,
|
||||||
onCreateDashboard,
|
onCreateDashboard,
|
||||||
}: MenuItemsProps) => {
|
}: MenuItemsProps) => {
|
||||||
@@ -295,18 +290,13 @@ const ContextMenuItems = ({
|
|||||||
const showExtract = fileType === 'archive';
|
const showExtract = fileType === 'archive';
|
||||||
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
||||||
|
|
||||||
|
const hasTasks = taskGroups.length > 0;
|
||||||
const hasActions =
|
const hasActions =
|
||||||
showPlay ||
|
showPlay || showReadAloud || showOcr || showTranscribe || showExtractAudio || showExtract || hasTasks;
|
||||||
showReadAloud ||
|
// Only nest when more than one category matched — a file usually matches a single category, and
|
||||||
showOcr ||
|
// Run Task > Video > Convert would just add a hop.
|
||||||
showTranscribe ||
|
|
||||||
showExtractAudio ||
|
|
||||||
showExtract ||
|
|
||||||
matchingTasks.length > 0;
|
|
||||||
// Only nest when more than one category is present — a file usually matches a single category,
|
|
||||||
// and Run Task > Video > Convert would just add a hop.
|
|
||||||
const taskGroups = groupTasksByCategory(matchingTasks);
|
|
||||||
const nestTasks = taskGroups.length > 1;
|
const nestTasks = taskGroups.length > 1;
|
||||||
|
const flatTasks = taskGroups[0]?.tasks ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -346,7 +336,7 @@ const ContextMenuItems = ({
|
|||||||
Extract
|
Extract
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)}
|
)}
|
||||||
{matchingTasks.length > 0 && (
|
{hasTasks && (
|
||||||
<ContextMenuSub>
|
<ContextMenuSub>
|
||||||
<ContextMenuSubTrigger className="cursor-pointer">
|
<ContextMenuSubTrigger className="cursor-pointer">
|
||||||
<Play className="mr-2 h-4 w-4" />
|
<Play className="mr-2 h-4 w-4" />
|
||||||
@@ -370,7 +360,7 @@ const ContextMenuItems = ({
|
|||||||
</ContextMenuSubContent>
|
</ContextMenuSubContent>
|
||||||
</ContextMenuSub>
|
</ContextMenuSub>
|
||||||
))
|
))
|
||||||
: matchingTasks.map((task) => (
|
: flatTasks.map((task) => (
|
||||||
<ContextMenuItem key={task.dirName} onClick={() => onRunTask(task, entry)} className="cursor-pointer">
|
<ContextMenuItem key={task.dirName} onClick={() => onRunTask(task, entry)} className="cursor-pointer">
|
||||||
{task.name}
|
{task.name}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
@@ -548,7 +538,7 @@ export const FileItem = ({
|
|||||||
onExtractAudio,
|
onExtractAudio,
|
||||||
onExtract,
|
onExtract,
|
||||||
onPlay,
|
onPlay,
|
||||||
matchingTasks,
|
taskGroups,
|
||||||
onRunTask,
|
onRunTask,
|
||||||
onCreateDashboard,
|
onCreateDashboard,
|
||||||
}: FileItemProps) => {
|
}: FileItemProps) => {
|
||||||
@@ -633,7 +623,7 @@ export const FileItem = ({
|
|||||||
onPlay,
|
onPlay,
|
||||||
onCut,
|
onCut,
|
||||||
onCopy,
|
onCopy,
|
||||||
matchingTasks,
|
taskGroups,
|
||||||
onRunTask,
|
onRunTask,
|
||||||
onCreateDashboard,
|
onCreateDashboard,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const useFileBrowserApp = (
|
|||||||
const [audioOnly, setAudioOnly] = useState(false);
|
const [audioOnly, setAudioOnly] = useState(false);
|
||||||
const [showDictate, setShowDictate] = useState(false);
|
const [showDictate, setShowDictate] = useState(false);
|
||||||
const dragCounter = useRef(0);
|
const dragCounter = useRef(0);
|
||||||
const { getMatchingTasks } = useTasks();
|
const { getMatchingTasks, getMatchingTaskGroups } = useTasks();
|
||||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const fileScrollRef = useRef<HTMLDivElement | null>(null);
|
const fileScrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -745,6 +745,7 @@ export const useFileBrowserApp = (
|
|||||||
runningTask,
|
runningTask,
|
||||||
setRunningTask,
|
setRunningTask,
|
||||||
getMatchingTasks,
|
getMatchingTasks,
|
||||||
|
getMatchingTaskGroups,
|
||||||
// Video download
|
// Video download
|
||||||
showVideoDownload,
|
showVideoDownload,
|
||||||
setShowVideoDownload,
|
setShowVideoDownload,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { test, expect } from 'bun:test';
|
||||||
|
import { groupTasksByCategory, type TaskSummary } from './useTasks';
|
||||||
|
|
||||||
|
const task = (name: string, category: string | null): TaskSummary => ({
|
||||||
|
id: 0,
|
||||||
|
dirName: name.toLowerCase().replace(/\s+/g, '-'),
|
||||||
|
name,
|
||||||
|
description: '',
|
||||||
|
scope: '',
|
||||||
|
category,
|
||||||
|
triggers: [{ type: 'directory' }],
|
||||||
|
mode: 'script',
|
||||||
|
userId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const shape = (tasks: TaskSummary[], order: string[]) =>
|
||||||
|
groupTasksByCategory(tasks, order).map((g) => `${g.category}(${g.tasks.length})`);
|
||||||
|
|
||||||
|
// The point of the grouping: categories.yaml is a sort key, never a source of menu entries. A
|
||||||
|
// category listed there with nothing in it must not render an empty submenu.
|
||||||
|
test('categories with no matching tasks do not appear', () => {
|
||||||
|
const order = ['Video', 'Photos', 'Audio', 'Documents', 'Cleanup'];
|
||||||
|
const groups = groupTasksByCategory([task('Convert Video', 'Video'), task('Tag Album', 'Audio')], order);
|
||||||
|
|
||||||
|
expect(groups.map((g) => g.category)).toEqual(['Video', 'Audio']);
|
||||||
|
expect(groups.every((g) => g.tasks.length > 0)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an empty task list produces no groups at all', () => {
|
||||||
|
expect(groupTasksByCategory([], ['Video', 'Audio'])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('order follows categories.yaml', () => {
|
||||||
|
const tasks = [task('A', 'Audio'), task('V', 'Video'), task('C', 'Cleanup')];
|
||||||
|
expect(shape(tasks, ['Video', 'Audio', 'Cleanup'])).toEqual(['Video(1)', 'Audio(1)', 'Cleanup(1)']);
|
||||||
|
expect(shape(tasks, ['Cleanup', 'Audio', 'Video'])).toEqual(['Cleanup(1)', 'Audio(1)', 'Video(1)']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a category missing from the file still renders, after the listed ones', () => {
|
||||||
|
const tasks = [task('Crop', 'Photos'), task('V', 'Video')];
|
||||||
|
expect(shape(tasks, ['Video'])).toEqual(['Video(1)', 'Photos(1)']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uncategorized tasks fall into Other, which sorts last', () => {
|
||||||
|
const tasks = [task('Mystery', null), task('Blank', ' '), task('V', 'Video')];
|
||||||
|
expect(shape(tasks, ['Video'])).toEqual(['Video(1)', 'Other(2)']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with no order at all, categories sort alphabetically and Other stays last', () => {
|
||||||
|
const tasks = [task('V', 'Video'), task('A', 'Audio'), task('M', null)];
|
||||||
|
expect(shape(tasks, [])).toEqual(['Audio(1)', 'Video(1)', 'Other(1)']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tasks within a group are sorted by display name', () => {
|
||||||
|
const tasks = [task('Zebra', 'Video'), task('Alpha', 'Video')];
|
||||||
|
expect(groupTasksByCategory(tasks, ['Video'])[0]!.tasks.map((t) => t.name)).toEqual(['Alpha', 'Zebra']);
|
||||||
|
});
|
||||||
@@ -20,17 +20,18 @@ export type TaskGroup = { category: string; tasks: TaskSummary[] };
|
|||||||
const UNCATEGORIZED = 'Other';
|
const UNCATEGORIZED = 'Other';
|
||||||
|
|
||||||
// Every task carries a `directory` trigger, so right-clicking a folder listed all of them at once.
|
// Every task carries a `directory` trigger, so right-clicking a folder listed all of them at once.
|
||||||
// Grouping by category turns that into Run Task > Video > … Known categories lead in this order;
|
// Grouping by category turns that into Run Task > Video > …
|
||||||
// anything else follows alphabetically, with Other always last.
|
//
|
||||||
const CATEGORY_ORDER = ['Video', 'Audio', 'Cleanup'];
|
// The order comes from categories.yaml in the items store, not from here — adding a category is a
|
||||||
|
// change to that file plus the tasks using it, never to this code. Categories absent from the list
|
||||||
const categoryRank = (category: string): number => {
|
// sort alphabetically after the listed ones, and Other is always last.
|
||||||
const known = CATEGORY_ORDER.indexOf(category);
|
const categoryRank = (category: string, order: string[]): number => {
|
||||||
|
const known = order.indexOf(category);
|
||||||
if (known !== -1) return known;
|
if (known !== -1) return known;
|
||||||
return category === UNCATEGORIZED ? Number.MAX_SAFE_INTEGER : CATEGORY_ORDER.length;
|
return category === UNCATEGORIZED ? Number.MAX_SAFE_INTEGER : order.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const groupTasksByCategory = (tasks: TaskSummary[]): TaskGroup[] => {
|
export const groupTasksByCategory = (tasks: TaskSummary[], order: string[] = []): TaskGroup[] => {
|
||||||
const groups = new Map<string, TaskSummary[]>();
|
const groups = new Map<string, TaskSummary[]>();
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
const category = task.category?.trim() || UNCATEGORIZED;
|
const category = task.category?.trim() || UNCATEGORIZED;
|
||||||
@@ -41,7 +42,10 @@ export const groupTasksByCategory = (tasks: TaskSummary[]): TaskGroup[] => {
|
|||||||
|
|
||||||
return Array.from(groups.entries())
|
return Array.from(groups.entries())
|
||||||
.map(([category, list]) => ({ category, tasks: [...list].sort((a, b) => a.name.localeCompare(b.name)) }))
|
.map(([category, list]) => ({ category, tasks: [...list].sort((a, b) => a.name.localeCompare(b.name)) }))
|
||||||
.sort((a, b) => categoryRank(a.category) - categoryRank(b.category) || a.category.localeCompare(b.category));
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
categoryRank(a.category, order) - categoryRank(b.category, order) || a.category.localeCompare(b.category),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTasks = () => {
|
export const useTasks = () => {
|
||||||
@@ -53,6 +57,12 @@ export const useTasks = () => {
|
|||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: categoryOrder = [] } = useQuery<string[]>({
|
||||||
|
queryKey: ['task-categories'],
|
||||||
|
queryFn: () => client.get('/tasks/categories'),
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
const getMatchingTasks = (fileName: string, entryType: 'file' | 'directory'): TaskSummary[] => {
|
const getMatchingTasks = (fileName: string, entryType: 'file' | 'directory'): TaskSummary[] => {
|
||||||
if (entryType === 'directory') {
|
if (entryType === 'directory') {
|
||||||
return tasks.filter((t) => t.triggers.some((tr) => tr.type === 'directory'));
|
return tasks.filter((t) => t.triggers.some((tr) => tr.type === 'directory'));
|
||||||
@@ -62,5 +72,9 @@ export const useTasks = () => {
|
|||||||
return tasks.filter((t) => t.triggers.some((tr) => tr.type === 'file' && tr.extensions.includes(ext)));
|
return tasks.filter((t) => t.triggers.some((tr) => tr.type === 'file' && tr.extensions.includes(ext)));
|
||||||
};
|
};
|
||||||
|
|
||||||
return { tasks, getMatchingTasks };
|
// Grouped for the context menu, ordered by categories.yaml.
|
||||||
|
const getMatchingTaskGroups = (fileName: string, entryType: 'file' | 'directory'): TaskGroup[] =>
|
||||||
|
groupTasksByCategory(getMatchingTasks(fileName, entryType), categoryOrder);
|
||||||
|
|
||||||
|
return { tasks, categoryOrder, getMatchingTasks, getMatchingTaskGroups };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user