feat(FN-936): add modal description persistence for re-entry
- Add modalPersistence hook for saving/restoring modal state (description, goal) via localStorage - Update PlanningModeModal to persist and restore description across re-opens - Update SubtaskBreakdownModal to persist and clear description on resume flow - Update MissionInterviewModal to clear goal after starting interview - Add unit tests for modalPersistence hook - Add integration tests for modal re-entry behavior across all three modals - Add changeset for published package patch bump
This commit is contained in:
144
packages/dashboard/app/hooks/__tests__/modalPersistence.test.ts
Normal file
144
packages/dashboard/app/hooks/__tests__/modalPersistence.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import {
|
||||
STORED_PLANNING_KEY,
|
||||
STORED_SUBTASK_KEY,
|
||||
STORED_MISSION_KEY,
|
||||
savePlanningDescription,
|
||||
getPlanningDescription,
|
||||
clearPlanningDescription,
|
||||
saveSubtaskDescription,
|
||||
getSubtaskDescription,
|
||||
clearSubtaskDescription,
|
||||
saveMissionGoal,
|
||||
getMissionGoal,
|
||||
clearMissionGoal,
|
||||
} from "../modalPersistence";
|
||||
|
||||
describe("modalPersistence", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("Storage keys are exported", () => {
|
||||
it("exports planning key", () => {
|
||||
expect(STORED_PLANNING_KEY).toBe("kb-planning-last-description");
|
||||
});
|
||||
|
||||
it("exports subtask key", () => {
|
||||
expect(STORED_SUBTASK_KEY).toBe("kb-subtask-last-description");
|
||||
});
|
||||
|
||||
it("exports mission key", () => {
|
||||
expect(STORED_MISSION_KEY).toBe("kb-mission-last-goal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Planning persistence", () => {
|
||||
it("saves and retrieves planning description", () => {
|
||||
savePlanningDescription("Build authentication");
|
||||
expect(getPlanningDescription()).toBe("Build authentication");
|
||||
});
|
||||
|
||||
it("returns empty string when nothing saved", () => {
|
||||
expect(getPlanningDescription()).toBe("");
|
||||
});
|
||||
|
||||
it("clears correctly", () => {
|
||||
savePlanningDescription("Test");
|
||||
clearPlanningDescription();
|
||||
expect(getPlanningDescription()).toBe("");
|
||||
});
|
||||
|
||||
it("returns empty string when localStorage returns null", () => {
|
||||
vi.spyOn(Storage.prototype, "getItem").mockReturnValue(null);
|
||||
expect(getPlanningDescription()).toBe("");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("overwrites previous value", () => {
|
||||
savePlanningDescription("First");
|
||||
savePlanningDescription("Second");
|
||||
expect(getPlanningDescription()).toBe("Second");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Subtask persistence", () => {
|
||||
it("saves and retrieves subtask description", () => {
|
||||
saveSubtaskDescription("Implement login feature");
|
||||
expect(getSubtaskDescription()).toBe("Implement login feature");
|
||||
});
|
||||
|
||||
it("returns empty string when nothing saved", () => {
|
||||
expect(getSubtaskDescription()).toBe("");
|
||||
});
|
||||
|
||||
it("clears correctly", () => {
|
||||
saveSubtaskDescription("Test");
|
||||
clearSubtaskDescription();
|
||||
expect(getSubtaskDescription()).toBe("");
|
||||
});
|
||||
|
||||
it("overwrites previous value", () => {
|
||||
saveSubtaskDescription("First");
|
||||
saveSubtaskDescription("Second");
|
||||
expect(getSubtaskDescription()).toBe("Second");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mission persistence", () => {
|
||||
it("saves and retrieves mission goal", () => {
|
||||
saveMissionGoal("Build a SaaS platform");
|
||||
expect(getMissionGoal()).toBe("Build a SaaS platform");
|
||||
});
|
||||
|
||||
it("returns empty string when nothing saved", () => {
|
||||
expect(getMissionGoal()).toBe("");
|
||||
});
|
||||
|
||||
it("clears correctly", () => {
|
||||
saveMissionGoal("Test");
|
||||
clearMissionGoal();
|
||||
expect(getMissionGoal()).toBe("");
|
||||
});
|
||||
|
||||
it("overwrites previous value", () => {
|
||||
saveMissionGoal("First");
|
||||
saveMissionGoal("Second");
|
||||
expect(getMissionGoal()).toBe("Second");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Storage keys are independent", () => {
|
||||
it("planning and subtask do not interfere", () => {
|
||||
savePlanningDescription("planning desc");
|
||||
saveSubtaskDescription("subtask desc");
|
||||
expect(getPlanningDescription()).toBe("planning desc");
|
||||
expect(getSubtaskDescription()).toBe("subtask desc");
|
||||
});
|
||||
|
||||
it("planning and mission do not interfere", () => {
|
||||
savePlanningDescription("planning desc");
|
||||
saveMissionGoal("mission goal");
|
||||
expect(getPlanningDescription()).toBe("planning desc");
|
||||
expect(getMissionGoal()).toBe("mission goal");
|
||||
});
|
||||
|
||||
it("subtask and mission do not interfere", () => {
|
||||
saveSubtaskDescription("subtask desc");
|
||||
saveMissionGoal("mission goal");
|
||||
expect(getSubtaskDescription()).toBe("subtask desc");
|
||||
expect(getMissionGoal()).toBe("mission goal");
|
||||
});
|
||||
|
||||
it("clearing one does not affect others", () => {
|
||||
savePlanningDescription("planning");
|
||||
saveSubtaskDescription("subtask");
|
||||
saveMissionGoal("mission");
|
||||
|
||||
clearSubtaskDescription();
|
||||
expect(getPlanningDescription()).toBe("planning");
|
||||
expect(getSubtaskDescription()).toBe("");
|
||||
expect(getMissionGoal()).toBe("mission");
|
||||
});
|
||||
});
|
||||
});
|
||||
61
packages/dashboard/app/hooks/modalPersistence.ts
Normal file
61
packages/dashboard/app/hooks/modalPersistence.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Storage keys — each modal type has independent storage
|
||||
export const STORED_PLANNING_KEY = "kb-planning-last-description";
|
||||
export const STORED_SUBTASK_KEY = "kb-subtask-last-description";
|
||||
export const STORED_MISSION_KEY = "kb-mission-last-goal";
|
||||
|
||||
// Planning persistence
|
||||
|
||||
export function savePlanningDescription(description: string): void {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORED_PLANNING_KEY, description);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlanningDescription(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return localStorage.getItem(STORED_PLANNING_KEY) || "";
|
||||
}
|
||||
|
||||
export function clearPlanningDescription(): void {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORED_PLANNING_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// Subtask persistence
|
||||
|
||||
export function saveSubtaskDescription(description: string): void {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORED_SUBTASK_KEY, description);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubtaskDescription(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return localStorage.getItem(STORED_SUBTASK_KEY) || "";
|
||||
}
|
||||
|
||||
export function clearSubtaskDescription(): void {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORED_SUBTASK_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// Mission persistence
|
||||
|
||||
export function saveMissionGoal(goal: string): void {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORED_MISSION_KEY, goal);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMissionGoal(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return localStorage.getItem(STORED_MISSION_KEY) || "";
|
||||
}
|
||||
|
||||
export function clearMissionGoal(): void {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORED_MISSION_KEY);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user