Phase 1 (Foundation): - Project restructure (presenton-main → backend/ + frontend/) - Database schema (8 new models, Alembic config, seed script) - Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware) - RBAC (access_service, rbac_middleware, admin routers) - Audit logging (fire-and-forget, AuditMiddleware, admin router) - i18n (react-i18next with 5 namespace files) Phase 2 (Admin Panel & Client Management): - Admin panel shell (sidebar layout, role guard, 12 pages) - Redux admin slice with 18 async thunks - User management (role changes, deactivation) - Client management (CRUD, brand config, team management) - Brand config editor (colors, fonts, logos, voice rules) - Master deck upload & parser (PPTX → HTML → React pipeline) - Audit log viewer with filters and CSV/JSON export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
No EOL
3.1 KiB
TypeScript
137 lines
No EOL
3.1 KiB
TypeScript
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
import { Slide } from '@/app/(presentation-generator)/types/slide';
|
|
|
|
interface HistoryState {
|
|
slides: Slide[];
|
|
timestamp: number;
|
|
actionType: string;
|
|
}
|
|
|
|
interface UndoRedoState {
|
|
past: HistoryState[];
|
|
present: HistoryState | null;
|
|
future: HistoryState[];
|
|
maxHistorySize: number;
|
|
isUndoRedoInProgress: boolean;
|
|
}
|
|
|
|
// Helper function for deep copy
|
|
const deepCopy = <T>(obj: T): T => {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
};
|
|
|
|
const initialState: UndoRedoState = {
|
|
past: [],
|
|
present: null,
|
|
future: [],
|
|
maxHistorySize: 30,
|
|
isUndoRedoInProgress: false
|
|
};
|
|
|
|
const undoRedoSlice = createSlice({
|
|
name: 'undoRedo',
|
|
initialState,
|
|
reducers: {
|
|
addToHistory: (state, action: PayloadAction<{slides: Slide[], actionType: string}>) => {
|
|
|
|
// Skip if undo/redo is in progress
|
|
if (state.isUndoRedoInProgress) {
|
|
return;
|
|
}
|
|
|
|
// Deep copy the slides to avoid reference issues
|
|
const newSlides = deepCopy(action.payload.slides);
|
|
|
|
// Only add to history if the slides have actually changed
|
|
if (!state.present) {
|
|
state.present = {
|
|
slides: newSlides,
|
|
timestamp: Date.now(),
|
|
actionType: action.payload.actionType
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Skip if slides are identical
|
|
if (JSON.stringify(state.present.slides) === JSON.stringify(newSlides)) {
|
|
return;
|
|
}
|
|
|
|
// Add current state to past
|
|
state.past.push(state.present);
|
|
|
|
// Limit history size
|
|
if (state.past.length > state.maxHistorySize) {
|
|
state.past.shift();
|
|
}
|
|
|
|
// Clear future on new change
|
|
state.future = [];
|
|
|
|
// Set new present
|
|
state.present = {
|
|
slides: newSlides,
|
|
timestamp: Date.now(),
|
|
actionType: action.payload.actionType
|
|
};
|
|
|
|
|
|
},
|
|
|
|
undo: (state) => {
|
|
if (state.past.length === 0) {
|
|
|
|
return;
|
|
}
|
|
|
|
state.isUndoRedoInProgress = true;
|
|
|
|
// Move present to future
|
|
if (state.present) {
|
|
state.future.unshift(deepCopy(state.present));
|
|
}
|
|
|
|
// Get last past state
|
|
const previous = state.past[state.past.length - 1];
|
|
state.past = state.past.slice(0, -1);
|
|
state.present = deepCopy(previous);
|
|
|
|
|
|
},
|
|
|
|
redo: (state) => {
|
|
if (state.future.length === 0) {
|
|
|
|
return;
|
|
}
|
|
|
|
state.isUndoRedoInProgress = true;
|
|
|
|
// Move present to past
|
|
if (state.present) {
|
|
state.past.push(deepCopy(state.present));
|
|
}
|
|
|
|
// Get first future state
|
|
const next = state.future[0];
|
|
state.future = state.future.slice(1);
|
|
state.present = deepCopy(next);
|
|
|
|
|
|
},
|
|
|
|
finishUndoRedo: (state) => {
|
|
state.isUndoRedoInProgress = false;
|
|
},
|
|
|
|
clearHistory: (state) => {
|
|
state.past = [];
|
|
state.future = [];
|
|
state.present = null;
|
|
// Keep present
|
|
}
|
|
}
|
|
});
|
|
|
|
export const { addToHistory, undo, redo, finishUndoRedo, clearHistory } = undoRedoSlice.actions;
|
|
export default undoRedoSlice.reducer;
|