feat: improve app ui and project metadata
This commit is contained in:
@@ -4,9 +4,12 @@ import type { ForeshadowView } from "@/lib/api/types";
|
||||
import {
|
||||
buildRegisterRequest,
|
||||
buildTransitionRequest,
|
||||
countByStatus,
|
||||
extractReason,
|
||||
groupByStatus,
|
||||
isWindowApproaching,
|
||||
LANE_LABELS,
|
||||
nextFreeForeshadowCode,
|
||||
suggestForeshadowCode,
|
||||
validationReasonMessage,
|
||||
} from "./board";
|
||||
@@ -24,6 +27,24 @@ describe("suggestForeshadowCode", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("nextFreeForeshadowCode", () => {
|
||||
it("keeps the preferred code when it is free", () => {
|
||||
expect(nextFreeForeshadowCode("FS-04", new Set(["FS-01", "FS-02"]))).toBe(
|
||||
"FS-04",
|
||||
);
|
||||
});
|
||||
|
||||
it("skips taken codes and returns the first free FS-NN", () => {
|
||||
const taken = new Set(["FS-01", "FS-02", "FS-03", "FS-05", "FS-OLD"]);
|
||||
// preferred FS-01 已占 → 取首个空闲 FS-04
|
||||
expect(nextFreeForeshadowCode("FS-01", taken)).toBe("FS-04");
|
||||
});
|
||||
|
||||
it("returns FS-01 when nothing is taken", () => {
|
||||
expect(nextFreeForeshadowCode("FS-01", new Set())).toBe("FS-01");
|
||||
});
|
||||
});
|
||||
|
||||
const view = (over: Partial<ForeshadowView>): ForeshadowView => ({
|
||||
code: "F-001",
|
||||
title: "线索",
|
||||
@@ -53,6 +74,32 @@ describe("groupByStatus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("countByStatus", () => {
|
||||
it("counts items in the same lane model used by the board", () => {
|
||||
expect(
|
||||
countByStatus([
|
||||
view({ status: "OPEN" }),
|
||||
view({ status: "OPEN" }),
|
||||
view({ status: "PARTIAL" }),
|
||||
view({ status: "CLOSED" }),
|
||||
view({ status: "OVERDUE" }),
|
||||
view({ status: "WEIRD" }),
|
||||
]),
|
||||
).toEqual({ OPEN: 3, PARTIAL: 1, CLOSED: 1, OVERDUE: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("LANE_LABELS", () => {
|
||||
it("keeps author-facing labels distinct from status codes", () => {
|
||||
expect(LANE_LABELS).toEqual({
|
||||
OPEN: "待推进",
|
||||
PARTIAL: "推进中",
|
||||
CLOSED: "已回收",
|
||||
OVERDUE: "已逾期",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowApproaching", () => {
|
||||
it("is true when current chapter is inside [from,to] for OPEN/PARTIAL", () => {
|
||||
const f = view({
|
||||
|
||||
@@ -18,13 +18,14 @@ export const LANES: readonly ForeshadowStatus[] = [
|
||||
] as const;
|
||||
|
||||
export const LANE_LABELS: Record<ForeshadowStatus, string> = {
|
||||
OPEN: "OPEN",
|
||||
PARTIAL: "PARTIAL",
|
||||
CLOSED: "CLOSED",
|
||||
OVERDUE: "OVERDUE",
|
||||
OPEN: "待推进",
|
||||
PARTIAL: "推进中",
|
||||
CLOSED: "已回收",
|
||||
OVERDUE: "已逾期",
|
||||
};
|
||||
|
||||
export type ForeshadowLanes = Record<ForeshadowStatus, ForeshadowView[]>;
|
||||
export type ForeshadowLaneCounts = Record<ForeshadowStatus, number>;
|
||||
|
||||
function emptyLanes(): ForeshadowLanes {
|
||||
return { OPEN: [], PARTIAL: [], CLOSED: [], OVERDUE: [] };
|
||||
@@ -46,6 +47,18 @@ export function groupByStatus(
|
||||
return lanes;
|
||||
}
|
||||
|
||||
export function countByStatus(
|
||||
items: readonly ForeshadowView[] | undefined,
|
||||
): ForeshadowLaneCounts {
|
||||
const lanes = groupByStatus(items);
|
||||
return {
|
||||
OPEN: lanes.OPEN.length,
|
||||
PARTIAL: lanes.PARTIAL.length,
|
||||
CLOSED: lanes.CLOSED.length,
|
||||
OVERDUE: lanes.OVERDUE.length,
|
||||
};
|
||||
}
|
||||
|
||||
// 接近回收窗口判定:current 在 [from, to] 内,或已超 from 但尚未 CLOSED(提示安排回收)。
|
||||
// 仅对 OPEN/PARTIAL 提示;CLOSED 已回收、OVERDUE 已逾期另有强调。
|
||||
export function isWindowApproaching(
|
||||
@@ -70,6 +83,21 @@ export function suggestForeshadowCode(
|
||||
return `FS-${String(index + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// 在已占用代号(taken,区分大小写按原样)中,为预填代号挑一个不冲突的值:
|
||||
// preferred 未被占用就用它;否则从 FS-01 起取首个空闲的 FS-<两位序号>。
|
||||
// 纯函数(可单测)。避免登记时与库里已存代号撞码 → 422 duplicate。
|
||||
export function nextFreeForeshadowCode(
|
||||
preferred: string,
|
||||
taken: ReadonlySet<string>,
|
||||
): string {
|
||||
if (preferred && !taken.has(preferred)) return preferred;
|
||||
for (let n = 1; n < 1000; n++) {
|
||||
const candidate = `FS-${String(n).padStart(2, "0")}`;
|
||||
if (!taken.has(candidate)) return candidate;
|
||||
}
|
||||
return preferred; // 理论不可达:1000 个代号全占用时退回原值
|
||||
}
|
||||
|
||||
export interface RegisterInput {
|
||||
projectId: string;
|
||||
code: string;
|
||||
|
||||
Reference in New Issue
Block a user