From 75b63c94fde972133fc4cc16a22a8586017eb2d0 Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Sat, 27 Jun 2026 18:33:29 +0100 Subject: [PATCH] fix(cms): persist undo/redo history in sessionStorage --- src/store/cmsStore.test.ts | 83 +++++++++++++++++ src/store/cmsStore.ts | 185 +++++++++++++++++++++---------------- 2 files changed, 186 insertions(+), 82 deletions(-) create mode 100644 src/store/cmsStore.test.ts diff --git a/src/store/cmsStore.test.ts b/src/store/cmsStore.test.ts new file mode 100644 index 00000000..7db5f566 --- /dev/null +++ b/src/store/cmsStore.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { useCMSStore } from './cmsStore'; + +describe('cmsStore persist middleware', () => { + beforeEach(() => { + // Clear storage and reset store + sessionStorage.clear(); + useCMSStore.persist.clearStorage(); + useCMSStore.setState({ + course: { id: '', title: '', description: '', modules: [] }, + history: [], + historyIndex: -1, + mediaQueue: [], + templates: [], + isSaving: false, + }); + }); + + it('persists history, historyIndex, and course state to sessionStorage', () => { + const course = { id: '1', title: 'Test Course', description: 'Test', modules: [] }; + + useCMSStore.getState().setCourse(course); + + // Check sessionStorage + const stored = JSON.parse(sessionStorage.getItem('cms-storage') || '{}'); + expect(stored.state).toBeDefined(); + expect(stored.state.course).toEqual(course); + expect(stored.state.history.length).toBe(1); + expect(stored.state.historyIndex).toBe(0); + }); + + it('rehydrates correctly and allows undo after refresh', async () => { + const course1 = { id: '1', title: 'Course v1', description: '', modules: [] }; + const course2 = { id: '1', title: 'Course v2', description: '', modules: [] }; + + useCMSStore.getState().setCourse(course1); + useCMSStore.getState().updateCourse({ title: 'Course v2' }); + + // Store state before rehydration + const stateBefore = useCMSStore.getState(); + expect(stateBefore.course.title).toBe('Course v2'); + expect(stateBefore.historyIndex).toBe(1); + expect(stateBefore.history.length).toBe(2); + + // Simulate page refresh by resetting store state to defaults + useCMSStore.setState({ + course: { id: '', title: '', description: '', modules: [] }, + history: [], + historyIndex: -1, + }); + + // Rehydrate store from sessionStorage + await useCMSStore.persist.rehydrate(); + + // Verify state is restored + const stateAfterRehydrate = useCMSStore.getState(); + expect(stateAfterRehydrate.course.title).toBe('Course v2'); + expect(stateAfterRehydrate.historyIndex).toBe(1); + expect(stateAfterRehydrate.history.length).toBe(2); + + // Perform undo + useCMSStore.getState().undo(); + + // Verify undo worked + const stateAfterUndo = useCMSStore.getState(); + expect(stateAfterUndo.course.title).toBe('Course v1'); + expect(stateAfterUndo.historyIndex).toBe(0); + }); + + it('limits history to 20 items to save sessionStorage quota', () => { + for (let i = 0; i < 25; i++) { + useCMSStore.getState().updateCourse({ title: `Course v${i}` }); + } + + const state = useCMSStore.getState(); + expect(state.history.length).toBe(20); + expect(state.historyIndex).toBe(19); + + // The first 5 should be dropped, so the oldest item is v5 + expect(state.history[0].title).toBe('Course v5'); + expect(state.history[19].title).toBe('Course v24'); + }); +}); diff --git a/src/store/cmsStore.ts b/src/store/cmsStore.ts index 37b49278..87ab148d 100644 --- a/src/store/cmsStore.ts +++ b/src/store/cmsStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; import { CMSCourse, MediaUploadTask, ContentTemplate } from '../types/cms'; interface CMSState { @@ -29,95 +30,115 @@ interface CMSState { setTemplates: (templates: ContentTemplate[]) => void; } -export const useCMSStore = create((set) => ({ - course: { - id: '', - title: '', - description: '', - modules: [], - }, - history: [], - historyIndex: -1, - mediaQueue: [], - templates: [], - isSaving: false, +export const useCMSStore = create()( + persist( + (set) => ({ + course: { + id: '', + title: '', + description: '', + modules: [], + }, + history: [], + historyIndex: -1, + mediaQueue: [], + templates: [], + isSaving: false, - setCourse: (course) => { - set((state) => { - const newHistory = state.history.slice(0, state.historyIndex + 1); - newHistory.push(course); - return { - course, - history: newHistory, - historyIndex: newHistory.length - 1, - }; - }); - }, + setCourse: (course) => { + set((state) => { + let newHistory = state.history.slice(0, state.historyIndex + 1); + newHistory.push(course); + + if (newHistory.length > 20) { + newHistory = newHistory.slice(newHistory.length - 20); + } + + return { + course, + history: newHistory, + historyIndex: newHistory.length - 1, + }; + }); + }, - updateCourse: (updates) => { - set((state) => { - const updatedCourse = { ...state.course, ...updates }; - const newHistory = state.history.slice(0, state.historyIndex + 1); + updateCourse: (updates) => { + set((state) => { + const updatedCourse = { ...state.course, ...updates }; + let newHistory = state.history.slice(0, state.historyIndex + 1); - // Limit history to 50 items - if (newHistory.length > 50) newHistory.shift(); + newHistory.push(updatedCourse); + + if (newHistory.length > 20) { + newHistory = newHistory.slice(newHistory.length - 20); + } - newHistory.push(updatedCourse); - return { - course: updatedCourse, - history: newHistory, - historyIndex: newHistory.length - 1, - }; - }); - }, + return { + course: updatedCourse, + history: newHistory, + historyIndex: newHistory.length - 1, + }; + }); + }, - undo: () => { - set((state) => { - if (state.historyIndex > 0) { - const prevIndex = state.historyIndex - 1; - return { - course: state.history[prevIndex], - historyIndex: prevIndex, - }; - } - return state; - }); - }, + undo: () => { + set((state) => { + if (state.historyIndex > 0) { + const prevIndex = state.historyIndex - 1; + return { + course: state.history[prevIndex], + historyIndex: prevIndex, + }; + } + return state; + }); + }, - redo: () => { - set((state) => { - if (state.historyIndex < state.history.length - 1) { - const nextIndex = state.historyIndex + 1; - return { - course: state.history[nextIndex], - historyIndex: nextIndex, - }; - } - return state; - }); - }, + redo: () => { + set((state) => { + if (state.historyIndex < state.history.length - 1) { + const nextIndex = state.historyIndex + 1; + return { + course: state.history[nextIndex], + historyIndex: nextIndex, + }; + } + return state; + }); + }, - addToQueue: (tasks) => { - set((state) => ({ - mediaQueue: [...state.mediaQueue, ...tasks], - })); - }, + addToQueue: (tasks) => { + set((state) => ({ + mediaQueue: [...state.mediaQueue, ...tasks], + })); + }, - updateUploadProgress: (id, progress) => { - set((state) => ({ - mediaQueue: state.mediaQueue.map((task) => (task.id === id ? { ...task, progress } : task)), - })); - }, + updateUploadProgress: (id, progress) => { + set((state) => ({ + mediaQueue: state.mediaQueue.map((task) => (task.id === id ? { ...task, progress } : task)), + })); + }, - setUploadStatus: (id, status, url, error) => { - set((state) => ({ - mediaQueue: state.mediaQueue.map((task) => - task.id === id ? { ...task, status, url, error } : task, - ), - })); - }, + setUploadStatus: (id, status, url, error) => { + set((state) => ({ + mediaQueue: state.mediaQueue.map((task) => + task.id === id ? { ...task, status, url, error } : task, + ), + })); + }, - setTemplates: (templates) => { - set({ templates }); - }, -})); + setTemplates: (templates) => { + set({ templates }); + }, + }), + { + name: 'cms-storage', + storage: createJSONStorage(() => sessionStorage), + partialize: (state) => ({ + course: state.course, + history: state.history, + historyIndex: state.historyIndex, + }), + } + ) +);