- 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
128 lines
3.2 KiB
TypeScript
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
|
|
}
|
|
}
|