Scrum4Me/stores/sprint-workspace/restore.ts
Madhura68 fdd83005a8 feat(PBI-74): sprint-workspace-store skelet (Story 9 / T-879)
- stores/sprint-workspace/{types,store,selectors,restore}.ts conform
  product-workspace blueprint
- ContextSlice: activeProduct, activeSprintId, activeStoryId, activeTaskId
- EntitiesSlice: sprintsById, storiesById, tasksById
- RelationsSlice: sprintIdsByProduct, storyIdsBySprint, taskIdsByStory
- LoadingSlice met activeRequestId voor race-safe ensure*Loaded
- SyncSlice: realtimeStatus, lastResyncAt, resyncReason
- Realtime applyRealtimeEvent voor sprint/story/task entities + unknown-event
  fallback, parent-move handling, child-cleanup bij D op sprint/story
- Optimistic mutations: sprint-story-order, sprint-task-order, entity-patch
- LocalStorage hints (storage key sprint-workspace-hints) per product/sprint
- 45 unit-tests groen — verplicht 13 cases uit workspace-store.md §Tests
2026-05-10 06:34:05 +02:00

128 lines
3.2 KiB
TypeScript

const STORAGE_KEY = 'sprint-workspace-hints'
interface PerProductHint {
lastActiveSprintId?: string | null
}
interface PerSprintHint {
lastActiveStoryId?: string | null
lastActiveTaskId?: string | null
}
export interface SprintWorkspaceHints {
lastActiveProductId: string | null
perProduct: Record<string, PerProductHint>
perSprint: Record<string, PerSprintHint>
}
const EMPTY_HINTS: SprintWorkspaceHints = {
lastActiveProductId: null,
perProduct: {},
perSprint: {},
}
function safeStorage(): Storage | null {
if (typeof globalThis === 'undefined') return null
try {
const ls = (globalThis as { localStorage?: Storage }).localStorage
return ls ?? null
} catch {
return null
}
}
export function readHints(): SprintWorkspaceHints {
const storage = safeStorage()
if (!storage) return { ...EMPTY_HINTS, perProduct: {}, perSprint: {} }
try {
const raw = storage.getItem(STORAGE_KEY)
if (!raw) return { ...EMPTY_HINTS, perProduct: {}, perSprint: {} }
const parsed = JSON.parse(raw) as Partial<SprintWorkspaceHints> | null
if (!parsed || typeof parsed !== 'object') {
return { ...EMPTY_HINTS, perProduct: {}, perSprint: {} }
}
return {
lastActiveProductId: parsed.lastActiveProductId ?? null,
perProduct:
parsed.perProduct && typeof parsed.perProduct === 'object'
? parsed.perProduct
: {},
perSprint:
parsed.perSprint && typeof parsed.perSprint === 'object'
? parsed.perSprint
: {},
}
} catch {
return { ...EMPTY_HINTS, perProduct: {}, perSprint: {} }
}
}
function writeHints(hints: SprintWorkspaceHints): void {
const storage = safeStorage()
if (!storage) return
try {
storage.setItem(STORAGE_KEY, JSON.stringify(hints))
} catch {
// ignore quota or serialization errors
}
}
export function writeProductHint(productId: string | null): void {
const hints = readHints()
hints.lastActiveProductId = productId
writeHints(hints)
}
function ensurePerProduct(
hints: SprintWorkspaceHints,
productId: string,
): PerProductHint {
if (!hints.perProduct[productId]) {
hints.perProduct[productId] = {}
}
return hints.perProduct[productId]
}
function ensurePerSprint(
hints: SprintWorkspaceHints,
sprintId: string,
): PerSprintHint {
if (!hints.perSprint[sprintId]) {
hints.perSprint[sprintId] = {}
}
return hints.perSprint[sprintId]
}
export function writeSprintHint(productId: string, sprintId: string | null): void {
const hints = readHints()
const entry = ensurePerProduct(hints, productId)
entry.lastActiveSprintId = sprintId
writeHints(hints)
}
export function writeStoryHint(sprintId: string, storyId: string | null): void {
const hints = readHints()
const entry = ensurePerSprint(hints, sprintId)
entry.lastActiveStoryId = storyId
if (storyId === null) {
entry.lastActiveTaskId = null
}
writeHints(hints)
}
export function writeTaskHint(sprintId: string, taskId: string | null): void {
const hints = readHints()
const entry = ensurePerSprint(hints, sprintId)
entry.lastActiveTaskId = taskId
writeHints(hints)
}
export function clearHints(): void {
const storage = safeStorage()
if (!storage) return
try {
storage.removeItem(STORAGE_KEY)
} catch {
// ignore
}
}