Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/store/cmsStore.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
185 changes: 103 additions & 82 deletions src/store/cmsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { CMSCourse, MediaUploadTask, ContentTemplate } from '../types/cms';

interface CMSState {
Expand Down Expand Up @@ -29,95 +30,115 @@ interface CMSState {
setTemplates: (templates: ContentTemplate[]) => void;
}

export const useCMSStore = create<CMSState>((set) => ({
course: {
id: '',
title: '',
description: '',
modules: [],
},
history: [],
historyIndex: -1,
mediaQueue: [],
templates: [],
isSaving: false,
export const useCMSStore = create<CMSState>()(
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,
}),
}
)
);
Loading