memos/web/src/components/MemoEditor/state/reducer.ts
Johnny 1b11e8c841 refactor(editor): complete state machine and services migration
BREAKING CHANGE: MemoEditor internal architecture completely refactored

## Summary

Refactored MemoEditor from hooks-based state management to a three-layer
architecture (Presentation → State → Services) using useReducer pattern.

## Changes

### Architecture
- **State Layer** (5 new files): types, actions, reducer, context, barrel export
- **Service Layer** (6 new files): error, validation, upload, cache, memo services + barrel
- **Component Layer** (3 new files): EditorToolbar, EditorContent, EditorMetadata
- **Simplified Hooks** (3 new files): useMemoInit, useAutoSave, useKeyboard

### Code Reduction
- Main component: ~380 lines → ~140 lines (-63%)
- Hooks removed: 5 old hooks (useMemoEditorState, useMemoSave, etc.)
- Total lines removed: 508 lines of old code
- Utility hooks preserved: 8 hooks still in use (useLocation, useDragAndDrop, etc.)

### Improvements
- ✅ Predictable state transitions with useReducer
- ✅ Testable business logic in pure service functions
- ✅ Cleaner component code (presentation only)
- ✅ Better separation of concerns
- ✅ Type-safe actions with discriminated unions
- ✅ Centralized error handling

## Statistics
- Files changed: 26
- Commits created: 25 (squashed into 1)
- New files: 17
- Removed files: 5
- TypeScript errors: 0
- Lint errors: 0

## Testing
Manual testing required for:
- Editor functionality (create, edit, save)
- Drag and drop
- Focus mode
- Keyboard shortcuts (Cmd/Ctrl + Enter)
- Auto-save to localStorage
2025-12-23 08:38:02 +08:00

130 lines
2.7 KiB
TypeScript

import type { EditorAction, EditorState } from "./types";
import { initialState } from "./types";
export function editorReducer(state: EditorState, action: EditorAction): EditorState {
switch (action.type) {
case "INIT_MEMO":
return {
...state,
content: action.payload.content,
metadata: action.payload.metadata,
timestamps: action.payload.timestamps,
};
case "UPDATE_CONTENT":
return {
...state,
content: action.payload,
};
case "SET_METADATA":
return {
...state,
metadata: {
...state.metadata,
...action.payload,
},
};
case "ADD_ATTACHMENT":
return {
...state,
metadata: {
...state.metadata,
attachments: [...state.metadata.attachments, action.payload],
},
};
case "REMOVE_ATTACHMENT":
return {
...state,
metadata: {
...state.metadata,
attachments: state.metadata.attachments.filter((a) => a.name !== action.payload),
},
};
case "ADD_RELATION":
return {
...state,
metadata: {
...state.metadata,
relations: [...state.metadata.relations, action.payload],
},
};
case "REMOVE_RELATION":
return {
...state,
metadata: {
...state.metadata,
relations: state.metadata.relations.filter((r) => r.relatedMemo?.name !== action.payload),
},
};
case "ADD_LOCAL_FILE":
return {
...state,
localFiles: [...state.localFiles, action.payload],
};
case "REMOVE_LOCAL_FILE":
return {
...state,
localFiles: state.localFiles.filter((f) => f.previewUrl !== action.payload),
};
case "CLEAR_LOCAL_FILES":
return {
...state,
localFiles: [],
};
case "TOGGLE_FOCUS_MODE":
return {
...state,
ui: {
...state.ui,
isFocusMode: !state.ui.isFocusMode,
},
};
case "SET_LOADING":
return {
...state,
ui: {
...state.ui,
isLoading: {
...state.ui.isLoading,
[action.payload.key]: action.payload.value,
},
},
};
case "SET_DRAGGING":
return {
...state,
ui: {
...state.ui,
isDragging: action.payload,
},
};
case "SET_COMPOSING":
return {
...state,
ui: {
...state.ui,
isComposing: action.payload,
},
};
case "RESET":
return {
...initialState,
};
default:
return state;
}
}