CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
Co-Authored-By: Codex <noreply@anthropic.com>
163 lines
4.5 KiB
TypeScript
163 lines
4.5 KiB
TypeScript
export type PlainleafTask = {
|
|
ref: string;
|
|
page: string;
|
|
name: string;
|
|
state: string;
|
|
done: boolean;
|
|
scheduled?: string;
|
|
deadline?: string;
|
|
completed?: string;
|
|
tags?: string[];
|
|
links?: string[];
|
|
pageLastModified?: string;
|
|
};
|
|
|
|
export type PlainleafTaskPatch = {
|
|
name?: string;
|
|
state?: string;
|
|
scheduled?: string | null;
|
|
deadline?: string | null;
|
|
completed?: string | null;
|
|
project?: string | null;
|
|
tags?: string[];
|
|
};
|
|
|
|
export type PlainleafNewTask = {
|
|
name: string;
|
|
scheduled?: string | null;
|
|
deadline?: string | null;
|
|
project?: string | null;
|
|
tags?: string[];
|
|
};
|
|
|
|
export type PlainleafTaskList =
|
|
"inbox" | "today" | "planned" | "anytime" | "projects" | "logbook";
|
|
|
|
export const plainleafTaskInbox = "任务/收件箱";
|
|
|
|
export function localDateKey(date = new Date()): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
export function taskDate(value?: string): string | undefined {
|
|
const match = value?.match(/^\d{4}-\d{2}-\d{2}/);
|
|
return match?.[0];
|
|
}
|
|
|
|
export function isOverdueTask(
|
|
task: PlainleafTask,
|
|
today = localDateKey(),
|
|
): boolean {
|
|
const deadline = taskDate(task.deadline);
|
|
return !task.done && !!deadline && deadline < today;
|
|
}
|
|
|
|
export function taskBelongsToToday(
|
|
task: PlainleafTask,
|
|
today = localDateKey(),
|
|
): boolean {
|
|
if (task.done) return false;
|
|
const scheduled = taskDate(task.scheduled);
|
|
const deadline = taskDate(task.deadline);
|
|
return scheduled === today || (!!deadline && deadline <= today);
|
|
}
|
|
|
|
export function tasksForInbox(tasks: PlainleafTask[]): PlainleafTask[] {
|
|
return tasks.filter(
|
|
(task) =>
|
|
!task.done && task.page.replace(/\.md$/i, "") === plainleafTaskInbox,
|
|
);
|
|
}
|
|
|
|
export function tasksForAnytime(tasks: PlainleafTask[]): PlainleafTask[] {
|
|
return tasks.filter(
|
|
(task) =>
|
|
!task.done &&
|
|
!taskDate(task.scheduled) &&
|
|
!taskDate(task.deadline) &&
|
|
task.page.replace(/\.md$/i, "") !== plainleafTaskInbox,
|
|
);
|
|
}
|
|
|
|
function futureTaskDate(
|
|
task: PlainleafTask,
|
|
today: string,
|
|
): string | undefined {
|
|
const scheduled = taskDate(task.scheduled);
|
|
const deadline = taskDate(task.deadline);
|
|
if (scheduled && scheduled > today) return scheduled;
|
|
if (deadline && deadline > today) return deadline;
|
|
return undefined;
|
|
}
|
|
|
|
export function groupScheduledTasks(
|
|
tasks: PlainleafTask[],
|
|
today = localDateKey(),
|
|
): Array<{ date: string; tasks: PlainleafTask[] }> {
|
|
const groups = new Map<string, PlainleafTask[]>();
|
|
for (const task of tasks) {
|
|
if (task.done) continue;
|
|
const date = futureTaskDate(task, today);
|
|
if (!date) continue;
|
|
const group = groups.get(date) ?? [];
|
|
group.push(task);
|
|
groups.set(date, group);
|
|
}
|
|
return [...groups.entries()]
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([date, groupedTasks]) => ({ date, tasks: groupedTasks }));
|
|
}
|
|
|
|
export function projectForTask(task: PlainleafTask): string | undefined {
|
|
const explicit = task.links?.find((link) => link.trim());
|
|
if (explicit) return explicit.trim();
|
|
const page = task.page.replace(/\.md$/i, "");
|
|
return page === plainleafTaskInbox ? undefined : page;
|
|
}
|
|
|
|
export function groupTasksByProject(
|
|
tasks: PlainleafTask[],
|
|
): Array<{ project: string; tasks: PlainleafTask[] }> {
|
|
const groups = new Map<string, PlainleafTask[]>();
|
|
for (const task of tasks) {
|
|
if (task.done) continue;
|
|
const project = projectForTask(task);
|
|
if (!project) continue;
|
|
const group = groups.get(project) ?? [];
|
|
group.push(task);
|
|
groups.set(project, group);
|
|
}
|
|
return [...groups.entries()]
|
|
.sort(([a], [b]) => a.localeCompare(b, "zh-CN"))
|
|
.map(([project, groupedTasks]) => ({ project, tasks: groupedTasks }));
|
|
}
|
|
|
|
export function logbookTasks(tasks: PlainleafTask[]): PlainleafTask[] {
|
|
return tasks
|
|
.filter((task) => task.done)
|
|
.sort((a, b) =>
|
|
(taskDate(b.completed) ?? b.pageLastModified ?? "").localeCompare(
|
|
taskDate(a.completed) ?? a.pageLastModified ?? "",
|
|
),
|
|
);
|
|
}
|
|
|
|
export function sortTodayTasks(
|
|
tasks: PlainleafTask[],
|
|
today = localDateKey(),
|
|
): PlainleafTask[] {
|
|
return tasks
|
|
.filter((task) => taskBelongsToToday(task, today))
|
|
.sort((a, b) => {
|
|
const rank = (task: PlainleafTask) => {
|
|
if (isOverdueTask(task, today)) return 0;
|
|
if (taskDate(task.deadline) === today) return 1;
|
|
return 2;
|
|
};
|
|
return rank(a) - rank(b) || a.name.localeCompare(b.name, "zh-CN");
|
|
});
|
|
}
|