feat: add unknown status and priority, change todo status to open
This commit is contained in:
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { changeStatus } from "./changeStatus";
|
||||
|
||||
@@ -18,8 +20,8 @@ describe("changeStatus", () => {
|
||||
const created = await repo.create(folder, {
|
||||
title: "Work item",
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
@@ -27,23 +29,23 @@ describe("changeStatus", () => {
|
||||
const result = await changeStatus(repo, {
|
||||
folderPath: folder,
|
||||
id: created.id,
|
||||
status: "in-progress",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
if (result.isErr()) return;
|
||||
expect(result.value.status).toBe("in-progress");
|
||||
expect(result.value.status).toBe(TaskStatus.IN_PROGRESS);
|
||||
expect(result.value.id).toBe(created.id);
|
||||
|
||||
const stored = await repo.getById(folder, created.id);
|
||||
expect(stored?.status).toBe("in-progress");
|
||||
expect(stored?.status).toBe(TaskStatus.IN_PROGRESS);
|
||||
});
|
||||
|
||||
it("returns not-found for unknown id", async () => {
|
||||
const result = await changeStatus(repo, {
|
||||
folderPath: folder,
|
||||
id: "missing",
|
||||
status: "done",
|
||||
status: TaskStatus.DONE,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })),
|
||||
@@ -54,7 +56,7 @@ describe("changeStatus", () => {
|
||||
const result = await changeStatus(repo, {
|
||||
folderPath: undefined,
|
||||
id: "x",
|
||||
status: "done",
|
||||
status: TaskStatus.DONE,
|
||||
});
|
||||
expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER)));
|
||||
});
|
||||
@@ -63,7 +65,7 @@ describe("changeStatus", () => {
|
||||
const result = await changeStatus(repo, {
|
||||
folderPath: folder,
|
||||
id: undefined,
|
||||
status: "done",
|
||||
status: TaskStatus.DONE,
|
||||
});
|
||||
expect(result).toEqual(err(appError(AppErrorVariant.NO_ID)));
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { createTask } from "./createTask";
|
||||
@@ -15,7 +16,7 @@ describe("createTask", () => {
|
||||
repo = new FsTaskRepository(new InMemoryFileSystem(), TASK_FILE_EXTENSION);
|
||||
});
|
||||
|
||||
it("creates a todo/medium task from title", async () => {
|
||||
it("creates an open/medium task from title", async () => {
|
||||
const result = await createTask(repo, {
|
||||
folderPath: folder,
|
||||
title: " New feature ",
|
||||
@@ -25,8 +26,8 @@ describe("createTask", () => {
|
||||
if (result.isErr()) return;
|
||||
|
||||
expect(result.value.title).toBe("New feature");
|
||||
expect(result.value.status).toBe("todo");
|
||||
expect(result.value.priority).toBe("medium");
|
||||
expect(result.value.status).toBe(TaskStatus.OPEN);
|
||||
expect(result.value.priority).toBe(TaskPriority.MEDIUM);
|
||||
expect(result.value.description).toBe("");
|
||||
expect(result.value.tags).toEqual([]);
|
||||
expect(result.value.assignee).toBe("");
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ITaskRepository } from "@/ports";
|
||||
export type CreateTaskInput = {
|
||||
folderPath?: string;
|
||||
title?: string;
|
||||
/** Defaults to TODO when omitted. */
|
||||
/** Defaults to OPEN when omitted. */
|
||||
status?: TaskStatus;
|
||||
};
|
||||
|
||||
@@ -27,10 +27,10 @@ export function createTask(
|
||||
|
||||
const status =
|
||||
input.status === undefined
|
||||
? TaskStatus.TODO
|
||||
? TaskStatus.OPEN
|
||||
: isTaskStatus(input.status)
|
||||
? input.status
|
||||
: TaskStatus.TODO;
|
||||
: TaskStatus.OPEN;
|
||||
|
||||
return ResultAsync.fromSafePromise(
|
||||
repo.create(input.folderPath, {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err, ok } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { deleteTask } from "./deleteTask";
|
||||
|
||||
@@ -18,8 +20,8 @@ describe("deleteTask", () => {
|
||||
const created = await repo.create(folder, {
|
||||
title: "To delete",
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { listLocatedTasks } from "./listLocatedTasks";
|
||||
|
||||
@@ -19,16 +21,16 @@ describe("listLocatedTasks", () => {
|
||||
const p = await repo.create(project, {
|
||||
title: "Project task",
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
const g = await repo.create(global, {
|
||||
title: "Global task",
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { loadDashboard } from "./loadDashboard";
|
||||
|
||||
@@ -18,16 +20,16 @@ describe("loadDashboard", () => {
|
||||
const a = await repo.create(folder, {
|
||||
title: "First",
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
const b = await repo.create(folder, {
|
||||
title: "Second",
|
||||
description: "body",
|
||||
status: "done",
|
||||
priority: "high",
|
||||
status: TaskStatus.DONE,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: ["x"],
|
||||
assignee: "",
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { openTask } from "./openTask";
|
||||
|
||||
@@ -18,8 +20,8 @@ describe("openTask", () => {
|
||||
const created = await repo.create(folder, {
|
||||
title: "Open me",
|
||||
description: "body",
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { saveTaskDescription } from "./saveTaskDescription";
|
||||
|
||||
@@ -18,8 +20,8 @@ describe("saveTaskDescription", () => {
|
||||
const created = await repo.create(folder, {
|
||||
title: "Editable",
|
||||
description: "old body",
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: ["keep"],
|
||||
assignee: "alice",
|
||||
});
|
||||
@@ -36,8 +38,8 @@ describe("saveTaskDescription", () => {
|
||||
expect(result.value.id).toBe(created.id);
|
||||
expect(result.value.description).toBe("new markdown body");
|
||||
expect(result.value.title).toBe("Editable");
|
||||
expect(result.value.status).toBe("todo");
|
||||
expect(result.value.priority).toBe("high");
|
||||
expect(result.value.status).toBe(TaskStatus.OPEN);
|
||||
expect(result.value.priority).toBe(TaskPriority.HIGH);
|
||||
expect(result.value.tags).toEqual(["keep"]);
|
||||
expect(result.value.assignee).toBe("alice");
|
||||
|
||||
@@ -49,8 +51,8 @@ describe("saveTaskDescription", () => {
|
||||
const created = await repo.create(folder, {
|
||||
title: "Clear me",
|
||||
description: "something",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { err } from "neverthrow";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||
import { serializeTask } from "@/utils/markdown";
|
||||
@@ -20,8 +21,8 @@ describe("saveTaskRaw", () => {
|
||||
const created = await repo.create(folder, {
|
||||
title: "Raw me",
|
||||
description: "old",
|
||||
status: TaskStatus.TODO,
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { err } from "neverthrow";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { serializeTask } from "@/utils/markdown";
|
||||
import { applyRawTaskContent } from "./applyRawTaskContent";
|
||||
@@ -10,8 +11,8 @@ const previous: Task = {
|
||||
id: "keep-me",
|
||||
title: "Old title",
|
||||
description: "old body",
|
||||
status: TaskStatus.TODO,
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
created: "2026-01-01T00:00:00.000Z",
|
||||
@@ -26,7 +27,7 @@ describe("applyRawTaskContent", () => {
|
||||
title: "New title",
|
||||
description: "new body",
|
||||
status: TaskStatus.DONE,
|
||||
priority: "high",
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: ["a"],
|
||||
assignee: "bob",
|
||||
created: "2099-01-01T00:00:00.000Z",
|
||||
@@ -41,7 +42,7 @@ describe("applyRawTaskContent", () => {
|
||||
expect(result.value.title).toBe("New title");
|
||||
expect(result.value.description).toBe("new body");
|
||||
expect(result.value.status).toBe(TaskStatus.DONE);
|
||||
expect(result.value.priority).toBe("high");
|
||||
expect(result.value.priority).toBe(TaskPriority.HIGH);
|
||||
expect(result.value.tags).toEqual(["a"]);
|
||||
expect(result.value.assignee).toBe("bob");
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { GroupBy } from "./groupBy";
|
||||
import { buildDashboardView } from "./buildDashboardView";
|
||||
|
||||
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
|
||||
return {
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
created: "2026-01-01T00:00:00.000Z",
|
||||
@@ -21,22 +23,22 @@ describe("buildDashboardView", () => {
|
||||
task({
|
||||
id: "1",
|
||||
title: "Alpha",
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
description: "first",
|
||||
}),
|
||||
task({
|
||||
id: "2",
|
||||
title: "Beta",
|
||||
status: "in-progress",
|
||||
priority: "low",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.LOW,
|
||||
description: "second",
|
||||
}),
|
||||
task({
|
||||
id: "3",
|
||||
title: "Gamma",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
description: "third",
|
||||
}),
|
||||
];
|
||||
@@ -52,12 +54,12 @@ describe("buildDashboardView", () => {
|
||||
|
||||
it("applies filter then groups", () => {
|
||||
const view = buildDashboardView(tasks, {
|
||||
filter: { statuses: ["todo"] },
|
||||
filter: { statuses: [TaskStatus.OPEN] },
|
||||
groupBy: GroupBy.PRIORITY,
|
||||
});
|
||||
|
||||
expect(view.visibleTasks.map((t) => t.id)).toEqual(["1", "3"]);
|
||||
expect(view.groups.map((g) => g.key)).toEqual(["high", "medium"]);
|
||||
expect(view.groups.map((g) => g.key)).toEqual([TaskPriority.HIGH, TaskPriority.MEDIUM]);
|
||||
});
|
||||
|
||||
it("resolves selected task by id from the full task list", () => {
|
||||
@@ -68,7 +70,7 @@ describe("buildDashboardView", () => {
|
||||
|
||||
it("keeps selection even when the task is filtered out of the list", () => {
|
||||
const view = buildDashboardView(tasks, {
|
||||
filter: { statuses: ["todo"] },
|
||||
filter: { statuses: [TaskStatus.OPEN] },
|
||||
selectedId: "2",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Task } from "@/model/task";
|
||||
import { LocatedTask, TaskScope } from "@/model/taskLocation";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { filterLocatedByScope } from "./filterLocatedByScope";
|
||||
|
||||
@@ -13,8 +14,8 @@ function located(
|
||||
id,
|
||||
title: id,
|
||||
description: "",
|
||||
status: TaskStatus.TODO,
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
created: "2026-01-01T00:00:00.000Z",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { filterTasks } from "./filterTasks";
|
||||
|
||||
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
|
||||
return {
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
created: "2026-01-01T00:00:00.000Z",
|
||||
@@ -21,32 +23,32 @@ describe("filterTasks", () => {
|
||||
id: "1",
|
||||
title: "Fix login",
|
||||
description: "Auth token expired",
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: ["bug", "frontend"],
|
||||
}),
|
||||
task({
|
||||
id: "2",
|
||||
title: "Add dark mode",
|
||||
description: "Theme toggle",
|
||||
status: "in-progress",
|
||||
priority: "medium",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: ["feature", "frontend"],
|
||||
}),
|
||||
task({
|
||||
id: "3",
|
||||
title: "Write docs",
|
||||
description: "API reference",
|
||||
status: "done",
|
||||
priority: "low",
|
||||
status: TaskStatus.DONE,
|
||||
priority: TaskPriority.LOW,
|
||||
tags: ["docs"],
|
||||
}),
|
||||
task({
|
||||
id: "4",
|
||||
title: "Deploy hotfix",
|
||||
description: "Production login outage",
|
||||
status: "todo",
|
||||
priority: "critical",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.CRITICAL,
|
||||
tags: ["bug", "ops"],
|
||||
}),
|
||||
];
|
||||
@@ -56,17 +58,17 @@ describe("filterTasks", () => {
|
||||
});
|
||||
|
||||
it("filters by a single status", () => {
|
||||
const result = filterTasks(tasks, { statuses: ["todo"] });
|
||||
const result = filterTasks(tasks, { statuses: [TaskStatus.OPEN] });
|
||||
expect(result.map((t) => t.id)).toEqual(["1", "4"]);
|
||||
});
|
||||
|
||||
it("filters by multiple statuses", () => {
|
||||
const result = filterTasks(tasks, { statuses: ["todo", "done"] });
|
||||
const result = filterTasks(tasks, { statuses: [TaskStatus.OPEN, TaskStatus.DONE] });
|
||||
expect(result.map((t) => t.id)).toEqual(["1", "3", "4"]);
|
||||
});
|
||||
|
||||
it("filters by priority", () => {
|
||||
const result = filterTasks(tasks, { priorities: ["high", "critical"] });
|
||||
const result = filterTasks(tasks, { priorities: [TaskPriority.HIGH, TaskPriority.CRITICAL] });
|
||||
expect(result.map((t) => t.id)).toEqual(["1", "4"]);
|
||||
});
|
||||
|
||||
@@ -92,8 +94,8 @@ describe("filterTasks", () => {
|
||||
|
||||
it("combines status, priority and query with AND", () => {
|
||||
const result = filterTasks(tasks, {
|
||||
statuses: ["todo"],
|
||||
priorities: ["critical"],
|
||||
statuses: [TaskStatus.OPEN],
|
||||
priorities: [TaskPriority.CRITICAL],
|
||||
query: "login",
|
||||
});
|
||||
expect(result.map((t) => t.id)).toEqual(["4"]);
|
||||
@@ -101,6 +103,6 @@ describe("filterTasks", () => {
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(filterTasks([], {})).toEqual([]);
|
||||
expect(filterTasks([], { statuses: ["todo"] })).toEqual([]);
|
||||
expect(filterTasks([], { statuses: [TaskStatus.OPEN] })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ import { groupTasks } from "./groupTasks";
|
||||
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
|
||||
return {
|
||||
description: "",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
@@ -37,13 +37,13 @@ describe("groupTasks", () => {
|
||||
task({
|
||||
id: "b",
|
||||
title: "B",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.CRITICAL,
|
||||
}),
|
||||
task({
|
||||
id: "c",
|
||||
title: "C",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
}),
|
||||
task({
|
||||
@@ -97,7 +97,7 @@ describe("groupTasks", () => {
|
||||
it("silently drops tasks with unknown status from groups", () => {
|
||||
const tasks: Task[] = [
|
||||
task({ id: "a", title: "A", status: "__garbage__" as TaskStatus }),
|
||||
task({ id: "b", title: "B", status: TaskStatus.TODO }),
|
||||
task({ id: "b", title: "B", status: TaskStatus.OPEN }),
|
||||
];
|
||||
const groups = groupTasks(tasks, GroupBy.STATUS);
|
||||
const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { sortTasks, TaskSortDirection } from "./sortTasks";
|
||||
|
||||
@@ -8,8 +9,8 @@ function task(
|
||||
): Task {
|
||||
return {
|
||||
description: "",
|
||||
status: TaskStatus.TODO,
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
updated: "2026-01-01T00:00:00.000Z",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import {
|
||||
extractFrontmatterBlock,
|
||||
findFieldRange,
|
||||
@@ -20,7 +22,7 @@ Body
|
||||
|
||||
describe("replaceFrontmatterField", () => {
|
||||
it("replaces status value", () => {
|
||||
const result = replaceFrontmatterField(sample, "status", "done");
|
||||
const result = replaceFrontmatterField(sample, "status", TaskStatus.DONE);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.content).toContain("status: done");
|
||||
@@ -29,7 +31,7 @@ describe("replaceFrontmatterField", () => {
|
||||
});
|
||||
|
||||
it("replaces priority value", () => {
|
||||
const result = replaceFrontmatterField(sample, "priority", "critical");
|
||||
const result = replaceFrontmatterField(sample, "priority", TaskPriority.CRITICAL);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.content).toContain("priority: critical");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { validateFrontmatter } from "./validateFrontmatter";
|
||||
const valid = `---
|
||||
id: "550e8400-e29b-41d4-a716-446655440000"
|
||||
title: Fix login
|
||||
status: todo
|
||||
status: open
|
||||
priority: high
|
||||
tags: [bug]
|
||||
assignee: ""
|
||||
@@ -34,7 +34,7 @@ describe("validateFrontmatter", () => {
|
||||
});
|
||||
|
||||
it("errors on invalid status", () => {
|
||||
const content = valid.replace("status: todo", "status: nope");
|
||||
const content = valid.replace("status: open", "status: nope");
|
||||
const issues = validateFrontmatter(content);
|
||||
const statusIssue = issues.find((i) => i.field === "status");
|
||||
expect(statusIssue).toBeDefined();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import {
|
||||
TASK_STATUS_META,
|
||||
TASK_STATUS_ORDER,
|
||||
@@ -10,8 +11,8 @@ import { buildKanbanBoard } from "./buildKanbanBoard";
|
||||
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
|
||||
return {
|
||||
description: "",
|
||||
status: TaskStatus.TODO,
|
||||
priority: "medium",
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
tags: [],
|
||||
assignee: "",
|
||||
created: "2026-01-01T00:00:00.000Z",
|
||||
@@ -35,13 +36,13 @@ describe("buildKanbanBoard", () => {
|
||||
task({
|
||||
id: "b",
|
||||
title: "Todo older",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
updated: "2026-01-01T00:00:01.000Z",
|
||||
}),
|
||||
task({
|
||||
id: "c",
|
||||
title: "Todo newer",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
updated: "2026-01-01T00:00:03.000Z",
|
||||
}),
|
||||
task({
|
||||
@@ -96,7 +97,7 @@ describe("buildKanbanBoard", () => {
|
||||
const byStatus = Object.fromEntries(
|
||||
board.columns.map((col) => [col.status, col.tasks.map((t) => t.id)]),
|
||||
);
|
||||
expect(byStatus[TaskStatus.TODO]).toEqual(["c", "b"]);
|
||||
expect(byStatus[TaskStatus.OPEN]).toEqual(["c", "b"]);
|
||||
expect(byStatus[TaskStatus.IN_PROGRESS]).toEqual(["d"]);
|
||||
expect(byStatus[TaskStatus.DONE]).toEqual(["a"]);
|
||||
expect(byStatus[TaskStatus.BACKLOG]).toEqual(["e"]);
|
||||
@@ -105,7 +106,7 @@ describe("buildKanbanBoard", () => {
|
||||
|
||||
it("sorts tasks within a column by updated desc", () => {
|
||||
const board = buildKanbanBoard(tasks);
|
||||
const todo = board.columns.find((col) => col.status === TaskStatus.TODO);
|
||||
const todo = board.columns.find((col) => col.status === TaskStatus.OPEN);
|
||||
expect(todo?.tasks.map((t) => t.id)).toEqual(["c", "b"]);
|
||||
});
|
||||
|
||||
@@ -124,24 +125,24 @@ describe("buildKanbanBoard", () => {
|
||||
task({
|
||||
id: "a",
|
||||
title: "A",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
updated: sameUpdated,
|
||||
}),
|
||||
task({
|
||||
id: "b",
|
||||
title: "B",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
updated: sameUpdated,
|
||||
}),
|
||||
task({
|
||||
id: "c",
|
||||
title: "C",
|
||||
status: TaskStatus.TODO,
|
||||
status: TaskStatus.OPEN,
|
||||
updated: sameUpdated,
|
||||
}),
|
||||
];
|
||||
const board = buildKanbanBoard(tasks);
|
||||
const todo = board.columns.find((col) => col.status === TaskStatus.TODO);
|
||||
const todo = board.columns.find((col) => col.status === TaskStatus.OPEN);
|
||||
expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,5 +6,6 @@ exports[`TaskPriority > exposes wire values on const keys 1`] = `
|
||||
"HIGH": "high",
|
||||
"LOW": "low",
|
||||
"MEDIUM": "medium",
|
||||
"UNKNOWN": "unknown",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -6,6 +6,7 @@ exports[`TaskStatus > exposes wire values on const keys 1`] = `
|
||||
"CANCELLED": "cancelled",
|
||||
"DONE": "done",
|
||||
"IN_PROGRESS": "in-progress",
|
||||
"TODO": "todo",
|
||||
"OPEN": "open",
|
||||
"UNKNOWN": "unknown",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("TaskPriority", () => {
|
||||
|
||||
it("isTaskPriority accepts known values only", () => {
|
||||
expect(isTaskPriority(TaskPriority.HIGH)).toBe(true);
|
||||
expect(isTaskPriority(TaskPriority.UNKNOWN)).toBe(true);
|
||||
expect(isTaskPriority("__not_a_priority__")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ export const TaskPriority = {
|
||||
HIGH: "high",
|
||||
MEDIUM: "medium",
|
||||
LOW: "low",
|
||||
UNKNOWN: "unknown",
|
||||
} as const;
|
||||
|
||||
export type TaskPriority = (typeof TaskPriority)[keyof typeof TaskPriority];
|
||||
@@ -11,12 +12,13 @@ export type TaskPriorityMeta = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
/** Explicit order for filters / grouping (critical → low). */
|
||||
/** Explicit order for filters / grouping (critical → low → unknown). */
|
||||
export const TASK_PRIORITY_ORDER: TaskPriority[] = [
|
||||
TaskPriority.CRITICAL,
|
||||
TaskPriority.HIGH,
|
||||
TaskPriority.MEDIUM,
|
||||
TaskPriority.LOW,
|
||||
TaskPriority.UNKNOWN,
|
||||
];
|
||||
|
||||
export const TASK_PRIORITY_META: Record<TaskPriority, TaskPriorityMeta> = {
|
||||
@@ -24,6 +26,7 @@ export const TASK_PRIORITY_META: Record<TaskPriority, TaskPriorityMeta> = {
|
||||
[TaskPriority.HIGH]: { label: "High" },
|
||||
[TaskPriority.MEDIUM]: { label: "Medium" },
|
||||
[TaskPriority.LOW]: { label: "Low" },
|
||||
[TaskPriority.UNKNOWN]: { label: "Unknown" },
|
||||
};
|
||||
|
||||
export function isTaskPriority(value: unknown): value is TaskPriority {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isTaskStatus,
|
||||
TaskStatus,
|
||||
TASK_STATUS_META,
|
||||
TASK_STATUS_ORDER,
|
||||
TaskStatus,
|
||||
} from "./taskStatus";
|
||||
|
||||
describe("TaskStatus", () => {
|
||||
@@ -23,8 +23,9 @@ describe("TaskStatus", () => {
|
||||
});
|
||||
|
||||
it("isTaskStatus accepts known values only", () => {
|
||||
expect(isTaskStatus(TaskStatus.TODO)).toBe(true);
|
||||
expect(isTaskStatus(TaskStatus.OPEN)).toBe(true);
|
||||
expect(isTaskStatus(TaskStatus.BACKLOG)).toBe(true);
|
||||
expect(isTaskStatus(TaskStatus.UNKNOWN)).toBe(true);
|
||||
expect(isTaskStatus("__not_a_status__")).toBe(false);
|
||||
expect(isTaskStatus(null)).toBe(false);
|
||||
});
|
||||
|
||||
+11
-4
@@ -1,9 +1,10 @@
|
||||
export const TaskStatus = {
|
||||
BACKLOG: "backlog",
|
||||
TODO: "todo",
|
||||
OPEN: "open",
|
||||
IN_PROGRESS: "in-progress",
|
||||
DONE: "done",
|
||||
CANCELLED: "cancelled",
|
||||
UNKNOWN: "unknown",
|
||||
} as const;
|
||||
|
||||
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];
|
||||
@@ -19,10 +20,11 @@ export type TaskStatusMeta = {
|
||||
/** Explicit display / column order (not Object.values). */
|
||||
export const TASK_STATUS_ORDER: TaskStatus[] = [
|
||||
TaskStatus.BACKLOG,
|
||||
TaskStatus.TODO,
|
||||
TaskStatus.OPEN,
|
||||
TaskStatus.IN_PROGRESS,
|
||||
TaskStatus.DONE,
|
||||
TaskStatus.CANCELLED,
|
||||
TaskStatus.UNKNOWN,
|
||||
];
|
||||
|
||||
export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
|
||||
@@ -31,8 +33,8 @@ export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
|
||||
defaultHidden: true,
|
||||
icon: "inbox",
|
||||
},
|
||||
[TaskStatus.TODO]: {
|
||||
label: "To Do",
|
||||
[TaskStatus.OPEN]: {
|
||||
label: "Open",
|
||||
defaultHidden: false,
|
||||
icon: "circle-outline",
|
||||
},
|
||||
@@ -51,6 +53,11 @@ export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
|
||||
defaultHidden: true,
|
||||
icon: "close",
|
||||
},
|
||||
[TaskStatus.UNKNOWN]: {
|
||||
label: "Unknown",
|
||||
defaultHidden: false,
|
||||
icon: "question",
|
||||
},
|
||||
};
|
||||
|
||||
export function isTaskStatus(value: unknown): value is TaskStatus {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Task } from "@/model/task";
|
||||
import { ITaskRepository } from "@/ports";
|
||||
import { TASK_FILE_EXTENSION } from "@/model/taskFile";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||
import { FsTaskRepository } from "./FsTaskRepository";
|
||||
@@ -21,8 +23,8 @@ describe("FsTaskRepository", () => {
|
||||
const sampleTask = {
|
||||
title: "Fix login bug",
|
||||
description: "Token validation error",
|
||||
status: "todo" as const,
|
||||
priority: "high" as const,
|
||||
status: TaskStatus.OPEN,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: ["bug", "frontend"],
|
||||
assignee: "ivanov",
|
||||
};
|
||||
@@ -53,7 +55,7 @@ describe("FsTaskRepository", () => {
|
||||
`${folder}/fix-login-bug${TASK_FILE_EXTENSION}`,
|
||||
);
|
||||
expect(content).toContain("Fix login bug");
|
||||
expect(content).toContain("status: todo");
|
||||
expect(content).toContain("status: open");
|
||||
});
|
||||
|
||||
it("list returns created tasks sorted by updated desc", async () => {
|
||||
@@ -81,12 +83,12 @@ describe("FsTaskRepository", () => {
|
||||
|
||||
it("update modifies fields and bumps updated timestamp", async () => {
|
||||
const task = await repo.create(folder, sampleTask);
|
||||
const updated: Task = { ...task, status: "done" };
|
||||
const updated: Task = { ...task, status: TaskStatus.DONE };
|
||||
|
||||
await repo.update(folder, updated);
|
||||
const found = await repo.getById(folder, task.id);
|
||||
|
||||
expect(found!.status).toBe("done");
|
||||
expect(found!.status).toBe(TaskStatus.DONE);
|
||||
expect(found!.title).toBe(task.title);
|
||||
expect(found!.updated).toBe(t1);
|
||||
expect(found!.created).toBe(t0);
|
||||
@@ -220,7 +222,7 @@ describe("FsTaskRepository", () => {
|
||||
});
|
||||
|
||||
it("creates task with tags containing special characters", async () => {
|
||||
const task = await repo.create(folder, {
|
||||
await repo.create(folder, {
|
||||
...sampleTask,
|
||||
tags: ["c#", "c++", "tag name", "tag:value"],
|
||||
});
|
||||
@@ -236,7 +238,7 @@ describe("FsTaskRepository", () => {
|
||||
});
|
||||
|
||||
it("getFilePath produces consistent slug regardless of special characters", async () => {
|
||||
const task = await repo.create(folder, {
|
||||
await repo.create(folder, {
|
||||
...sampleTask,
|
||||
title: "Fix___bug...",
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ export function resolveTaskFolderForPath(
|
||||
): string | undefined {
|
||||
const normalized = path.resolve(filePath).replace(/\\/g, "/");
|
||||
let best: string | undefined;
|
||||
let bestLen = -1;
|
||||
const NO_MATCH = -1;
|
||||
let bestLen = NO_MATCH;
|
||||
|
||||
for (const location of locations) {
|
||||
const folder = path.resolve(location.folderPath).replace(/\\/g, "/");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskPriority } from "@/model/taskPriority";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseTaskFile, serializeTask } from "./markdown";
|
||||
|
||||
@@ -8,8 +10,8 @@ const fullTask: Task = {
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
title: "Пофиксить баг логина",
|
||||
description: "Ошибка валидации токена",
|
||||
status: "in-progress",
|
||||
priority: "high",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.HIGH,
|
||||
tags: ["bug", "frontend"],
|
||||
assignee: "ivanov",
|
||||
created: "2026-07-12T10:00:00.000Z",
|
||||
@@ -45,8 +47,8 @@ describe("markdown", () => {
|
||||
expect(task.id).toBe("abc");
|
||||
expect(task.title).toBe("Minimal");
|
||||
expect(task.description).toBe("");
|
||||
expect(task.status).toBe("todo");
|
||||
expect(task.priority).toBe("medium");
|
||||
expect(task.status).toBe(TaskStatus.UNKNOWN);
|
||||
expect(task.priority).toBe(TaskPriority.UNKNOWN);
|
||||
expect(task.tags).toEqual([]);
|
||||
expect(task.assignee).toBe("");
|
||||
expect(task.created).toMatch(ISO_RE);
|
||||
|
||||
+12
-4
@@ -10,6 +10,16 @@ function toISO(value: unknown): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function normalizeTaskStatus(value: unknown): TaskStatus {
|
||||
if (isTaskStatus(value)) return value;
|
||||
return TaskStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
function normalizeTaskPriority(value: unknown): TaskPriority {
|
||||
if (isTaskPriority(value)) return value;
|
||||
return TaskPriority.UNKNOWN;
|
||||
}
|
||||
|
||||
export function parseTaskFile(content: string): Task {
|
||||
const parsed = fm<Record<string, unknown>>(content);
|
||||
const data = parsed.attributes;
|
||||
@@ -17,10 +27,8 @@ export function parseTaskFile(content: string): Task {
|
||||
id: data.id as string,
|
||||
title: data.title as string,
|
||||
description: parsed.body.trim(),
|
||||
status: isTaskStatus(data.status) ? data.status : TaskStatus.TODO,
|
||||
priority: isTaskPriority(data.priority)
|
||||
? data.priority
|
||||
: TaskPriority.MEDIUM,
|
||||
status: normalizeTaskStatus(data.status),
|
||||
priority: normalizeTaskPriority(data.priority),
|
||||
tags: (data.tags as string[]) ?? [],
|
||||
assignee: (data.assignee as string) ?? "",
|
||||
created: toISO(data.created),
|
||||
|
||||
Reference in New Issue
Block a user