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