feat: update dashboard view
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { err } from "neverthrow";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { Task } from "@/model/task";
|
||||
import { TaskStatus } from "@/model/taskStatus";
|
||||
import { serializeTask } from "@/utils/markdown";
|
||||
import { applyRawTaskContent } from "./applyRawTaskContent";
|
||||
|
||||
const previous: Task = {
|
||||
id: "keep-me",
|
||||
title: "Old title",
|
||||
description: "old body",
|
||||
status: TaskStatus.TODO,
|
||||
priority: "medium",
|
||||
tags: [],
|
||||
assignee: "",
|
||||
created: "2026-01-01T00:00:00.000Z",
|
||||
updated: "2026-01-02T00:00:00.000Z",
|
||||
};
|
||||
|
||||
describe("applyRawTaskContent", () => {
|
||||
it("parses raw file and applies fields", () => {
|
||||
const raw = serializeTask({
|
||||
...previous,
|
||||
id: "ignored-id",
|
||||
title: "New title",
|
||||
description: "new body",
|
||||
status: TaskStatus.DONE,
|
||||
priority: "high",
|
||||
tags: ["a"],
|
||||
assignee: "bob",
|
||||
created: "2099-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const result = applyRawTaskContent(raw, previous);
|
||||
expect(result.isOk()).toBe(true);
|
||||
if (result.isErr()) return;
|
||||
|
||||
expect(result.value.id).toBe(previous.id);
|
||||
expect(result.value.created).toBe(previous.created);
|
||||
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.tags).toEqual(["a"]);
|
||||
expect(result.value.assignee).toBe("bob");
|
||||
});
|
||||
|
||||
it("rejects empty title", () => {
|
||||
const raw = `---
|
||||
id: "x"
|
||||
title: " "
|
||||
status: todo
|
||||
priority: medium
|
||||
---
|
||||
|
||||
body
|
||||
`;
|
||||
const result = applyRawTaskContent(raw, previous);
|
||||
expect(result).toEqual(err(appError(AppErrorVariant.EMPTY_TITLE)));
|
||||
});
|
||||
|
||||
it("rejects unparseable content", () => {
|
||||
// Invalid YAML inside frontmatter — front-matter / yaml throws.
|
||||
const raw = "---\n:\n---\n";
|
||||
const result = applyRawTaskContent(raw, previous);
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_TASK_FILE);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { err, ok, Result } from "neverthrow";
|
||||
import { appError, AppError, AppErrorVariant } from "@/error";
|
||||
import { Task } from "@/model/task";
|
||||
import { parseTaskFile } from "@/utils/markdown";
|
||||
|
||||
/**
|
||||
* Parse full task file text into a Task, keeping stable id/created from previous.
|
||||
*/
|
||||
export function applyRawTaskContent(
|
||||
raw: string,
|
||||
previous: Task,
|
||||
): Result<Task, AppError> {
|
||||
let parsed: Task;
|
||||
try {
|
||||
parsed = parseTaskFile(raw);
|
||||
} catch (cause) {
|
||||
const detail =
|
||||
cause instanceof Error ? cause.message : "unable to parse markdown";
|
||||
return err(appError(AppErrorVariant.INVALID_TASK_FILE, { detail }));
|
||||
}
|
||||
|
||||
const title = parsed.title?.trim() ?? "";
|
||||
if (!title) {
|
||||
return err(appError(AppErrorVariant.EMPTY_TITLE));
|
||||
}
|
||||
|
||||
return ok({
|
||||
...parsed,
|
||||
title,
|
||||
id: previous.id,
|
||||
created: previous.created,
|
||||
});
|
||||
}
|
||||
@@ -44,20 +44,13 @@ describe("parseDashboardInboundMessage", () => {
|
||||
).toEqual(ok({ variant: "setGroupBy", groupBy: GroupBy.STATUS }));
|
||||
});
|
||||
|
||||
it("parses saveDescription", () => {
|
||||
it("parses editTask", () => {
|
||||
expect(
|
||||
parseDashboardInboundMessage({
|
||||
variant: "saveDescription",
|
||||
variant: "editTask",
|
||||
id: "abc",
|
||||
description: "updated body",
|
||||
}),
|
||||
).toEqual(
|
||||
ok({
|
||||
variant: "saveDescription",
|
||||
id: "abc",
|
||||
description: "updated body",
|
||||
}),
|
||||
);
|
||||
).toEqual(ok({ variant: "editTask", id: "abc" }));
|
||||
});
|
||||
|
||||
it("parses refresh", () => {
|
||||
@@ -137,12 +130,8 @@ describe("parseDashboardInboundMessage", () => {
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_MESSAGE);
|
||||
});
|
||||
|
||||
it("rejects saveDescription with non-string description", () => {
|
||||
const result = parseDashboardInboundMessage({
|
||||
variant: "saveDescription",
|
||||
id: "abc",
|
||||
description: 42,
|
||||
});
|
||||
it("rejects editTask without id", () => {
|
||||
const result = parseDashboardInboundMessage({ variant: "editTask" });
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_MESSAGE);
|
||||
@@ -157,4 +146,25 @@ describe("parseDashboardInboundMessage", () => {
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_MESSAGE);
|
||||
});
|
||||
|
||||
it("rejects saveDescription (view-only dashboard)", () => {
|
||||
const result = parseDashboardInboundMessage({
|
||||
variant: "saveDescription",
|
||||
id: "abc",
|
||||
description: "x",
|
||||
});
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_MESSAGE);
|
||||
});
|
||||
|
||||
it("rejects setDetailMode (no raw mode)", () => {
|
||||
const result = parseDashboardInboundMessage({
|
||||
variant: "setDetailMode",
|
||||
mode: "raw",
|
||||
});
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export type DashboardInboundMessage =
|
||||
| { variant: "selectTask"; id: string }
|
||||
| { variant: "setFilter"; filter: TaskFilter }
|
||||
| { variant: "setGroupBy"; groupBy: GroupBy }
|
||||
| { variant: "saveDescription"; id: string; description: string }
|
||||
| { variant: "editTask"; id: string }
|
||||
| { variant: "refresh" }
|
||||
| { variant: "createTask" }
|
||||
| { variant: "setScopeFilter"; scope: DashboardScopeFilter }
|
||||
@@ -78,18 +78,11 @@ export function parseDashboardInboundMessage(
|
||||
return ok({ variant: "setGroupBy", groupBy: raw.groupBy });
|
||||
}
|
||||
|
||||
case "saveDescription": {
|
||||
case "editTask": {
|
||||
if (typeof raw.id !== "string" || raw.id.length === 0) {
|
||||
return invalid(variant);
|
||||
}
|
||||
if (typeof raw.description !== "string") {
|
||||
return invalid(variant);
|
||||
}
|
||||
return ok({
|
||||
variant: "saveDescription",
|
||||
id: raw.id,
|
||||
description: raw.description,
|
||||
});
|
||||
return ok({ variant: "editTask", id: raw.id });
|
||||
}
|
||||
|
||||
case "setScopeFilter": {
|
||||
|
||||
@@ -30,5 +30,4 @@ export type DashboardOutboundMessage =
|
||||
sortDirection: TaskSortDirection;
|
||||
availableScopes: TaskScope[];
|
||||
}
|
||||
| { variant: "error"; error: AppError }
|
||||
| { variant: "saved"; task: Task };
|
||||
| { variant: "error"; error: AppError };
|
||||
|
||||
Reference in New Issue
Block a user