feat: update dashboard view

This commit is contained in:
2026-07-16 02:05:36 +05:00
parent 1d1f11c3f8
commit bda6c6b2e0
14 changed files with 338 additions and 136 deletions
+71
View File
@@ -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);
});
});
+33
View 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,
});
}
+26 -16
View File
@@ -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);
});
});
+3 -10
View File
@@ -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": {
+1 -2
View File
@@ -30,5 +30,4 @@ export type DashboardOutboundMessage =
sortDirection: TaskSortDirection;
availableScopes: TaskScope[];
}
| { variant: "error"; error: AppError }
| { variant: "saved"; task: Task };
| { variant: "error"; error: AppError };