Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

584
frontend/js/store.js Normale Datei
Datei anzeigen

@ -0,0 +1,584 @@
/**
* TASKMATE - State Store
* ======================
* Centralized state management
*/
import { deepClone, deepMerge } from './utils.js';
class Store {
constructor() {
this.state = {
// App state
currentView: 'board',
isOnline: navigator.onLine,
isLoading: false,
syncStatus: 'synced', // 'synced', 'syncing', 'offline', 'error'
// User state
currentUser: null,
users: [],
// Project state
projects: [],
currentProjectId: null,
// Board state
columns: [],
tasks: [],
labels: [],
// Filters
filters: {
search: '',
priority: 'all',
assignee: 'all',
label: 'all',
dueDate: 'all',
archived: false
},
// Server search result IDs (bypass client filter for these)
searchResultIds: [],
// Selection
selectedTaskIds: [],
// Modal state
openModals: [],
editingTask: null,
// UI state
dragState: null,
contextMenu: null,
// Undo stack
undoStack: [],
redoStack: [],
maxUndoHistory: 50
};
this.subscribers = new Map();
this.middlewares = [];
}
// Get current state
getState() {
return this.state;
}
// Get specific state path
get(path) {
return path.split('.').reduce((obj, key) => obj?.[key], this.state);
}
// Update state
setState(updates, actionType = 'SET_STATE') {
const prevState = deepClone(this.state);
// Apply middlewares
let processedUpdates = updates;
for (const middleware of this.middlewares) {
processedUpdates = middleware(prevState, processedUpdates, actionType);
}
// Merge updates
this.state = deepMerge(this.state, processedUpdates);
// Notify subscribers
this.notifySubscribers(prevState, this.state, actionType);
return this.state;
}
// Set specific state path
set(path, value, actionType) {
const keys = path.split('.');
const updates = keys.reduceRight((acc, key) => ({ [key]: acc }), value);
return this.setState(updates, actionType || `SET_${path.toUpperCase()}`);
}
// Subscribe to state changes
subscribe(selector, callback, immediate = false) {
const id = Symbol();
this.subscribers.set(id, { selector, callback });
if (immediate) {
const currentValue = typeof selector === 'function'
? selector(this.state)
: this.get(selector);
callback(currentValue, currentValue);
}
// Return unsubscribe function
return () => this.subscribers.delete(id);
}
// Notify subscribers of changes
notifySubscribers(prevState, newState, actionType) {
this.subscribers.forEach(({ selector, callback }) => {
const prevValue = typeof selector === 'function'
? selector(prevState)
: selector.split('.').reduce((obj, key) => obj?.[key], prevState);
const newValue = typeof selector === 'function'
? selector(newState)
: selector.split('.').reduce((obj, key) => obj?.[key], newState);
// Only call if value changed
if (JSON.stringify(prevValue) !== JSON.stringify(newValue)) {
callback(newValue, prevValue, actionType);
}
});
}
// Add middleware
use(middleware) {
this.middlewares.push(middleware);
}
// Reset state
reset() {
this.state = {
currentView: 'board',
isOnline: navigator.onLine,
isLoading: false,
syncStatus: 'synced',
currentUser: null,
users: [],
projects: [],
currentProjectId: null,
columns: [],
tasks: [],
labels: [],
filters: {
search: '',
priority: 'all',
assignee: 'all',
label: 'all',
dueDate: 'all',
archived: false
},
searchResultIds: [],
selectedTaskIds: [],
openModals: [],
editingTask: null,
dragState: null,
contextMenu: null,
undoStack: [],
redoStack: [],
maxUndoHistory: 50
};
}
// =====================
// PROJECT ACTIONS
// =====================
setProjects(projects) {
this.setState({ projects }, 'SET_PROJECTS');
}
addProject(project) {
this.setState({
projects: [...this.state.projects, project]
}, 'ADD_PROJECT');
}
updateProject(projectId, updates) {
this.setState({
projects: this.state.projects.map(p =>
p.id === projectId ? { ...p, ...updates } : p
)
}, 'UPDATE_PROJECT');
}
removeProject(projectId) {
this.setState({
projects: this.state.projects.filter(p => p.id !== projectId)
}, 'REMOVE_PROJECT');
}
setCurrentProject(projectId) {
this.setState({ currentProjectId: projectId }, 'SET_CURRENT_PROJECT');
localStorage.setItem('current_project_id', projectId);
}
getCurrentProject() {
return this.state.projects.find(p => p.id === this.state.currentProjectId);
}
// =====================
// COLUMN ACTIONS
// =====================
setColumns(columns) {
this.setState({ columns }, 'SET_COLUMNS');
}
addColumn(column) {
this.setState({
columns: [...this.state.columns, column]
}, 'ADD_COLUMN');
}
updateColumn(columnId, updates) {
this.setState({
columns: this.state.columns.map(c =>
c.id === columnId ? { ...c, ...updates } : c
)
}, 'UPDATE_COLUMN');
}
removeColumn(columnId) {
this.setState({
columns: this.state.columns.filter(c => c.id !== columnId),
tasks: this.state.tasks.filter(t => t.columnId !== columnId)
}, 'REMOVE_COLUMN');
}
reorderColumns(columnIds) {
const columnsMap = new Map(this.state.columns.map(c => [c.id, c]));
const reordered = columnIds.map((id, index) => ({
...columnsMap.get(id),
position: index
}));
this.setState({ columns: reordered }, 'REORDER_COLUMNS');
}
// =====================
// TASK ACTIONS
// =====================
setTasks(tasks) {
this.setState({ tasks }, 'SET_TASKS');
}
addTask(task) {
this.setState({
tasks: [...this.state.tasks, task]
}, 'ADD_TASK');
}
updateTask(taskId, updates) {
this.setState({
tasks: this.state.tasks.map(t =>
t.id === taskId ? { ...t, ...updates } : t
)
}, 'UPDATE_TASK');
}
removeTask(taskId) {
this.setState({
tasks: this.state.tasks.filter(t => t.id !== taskId),
selectedTaskIds: this.state.selectedTaskIds.filter(id => id !== taskId)
}, 'REMOVE_TASK');
}
moveTask(taskId, columnId, position) {
const tasks = [...this.state.tasks];
const taskIndex = tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) return;
const task = { ...tasks[taskIndex], columnId: columnId, position };
tasks.splice(taskIndex, 1);
// Find insert position
const columnTasks = tasks.filter(t => t.columnId === columnId);
const insertIndex = tasks.findIndex(t => t.columnId === columnId && t.position >= position);
if (insertIndex === -1) {
tasks.push(task);
} else {
tasks.splice(insertIndex, 0, task);
}
// Recalculate positions
let pos = 0;
tasks.forEach(t => {
if (t.columnId === columnId) {
t.position = pos++;
}
});
this.setState({ tasks }, 'MOVE_TASK');
}
getTaskById(taskId) {
return this.state.tasks.find(t => t.id === taskId);
}
getTasksByColumn(columnId) {
// Priority order: high (0) > medium (1) > low (2)
const priorityOrder = { high: 0, medium: 1, low: 2 };
return this.state.tasks
.filter(t => t.columnId === columnId && !t.archived)
.sort((a, b) => {
// 1. First by position (manual drag&drop sorting)
const posA = a.position ?? 999999;
const posB = b.position ?? 999999;
if (posA !== posB) return posA - posB;
// 2. Then by priority (high > medium > low)
const priA = priorityOrder[a.priority] ?? 1;
const priB = priorityOrder[b.priority] ?? 1;
if (priA !== priB) return priA - priB;
// 3. Then by creation date (older first)
const dateA = new Date(a.createdAt || 0).getTime();
const dateB = new Date(b.createdAt || 0).getTime();
return dateA - dateB;
});
}
// =====================
// LABEL ACTIONS
// =====================
setLabels(labels) {
this.setState({ labels }, 'SET_LABELS');
}
addLabel(label) {
this.setState({
labels: [...this.state.labels, label]
}, 'ADD_LABEL');
}
updateLabel(labelId, updates) {
this.setState({
labels: this.state.labels.map(l =>
l.id === labelId ? { ...l, ...updates } : l
)
}, 'UPDATE_LABEL');
}
removeLabel(labelId) {
this.setState({
labels: this.state.labels.filter(l => l.id !== labelId)
}, 'REMOVE_LABEL');
}
// =====================
// FILTER ACTIONS
// =====================
setFilter(key, value) {
this.setState({
filters: { ...this.state.filters, [key]: value }
}, 'SET_FILTER');
}
setFilters(filters) {
this.setState({
filters: { ...this.state.filters, ...filters }
}, 'SET_FILTERS');
}
resetFilters() {
this.setState({
filters: {
search: '',
priority: 'all',
assignee: 'all',
label: 'all',
dueDate: 'all',
archived: false
}
}, 'RESET_FILTERS');
}
// =====================
// SELECTION ACTIONS
// =====================
selectTask(taskId, multi = false) {
if (multi) {
const selected = this.state.selectedTaskIds.includes(taskId)
? this.state.selectedTaskIds.filter(id => id !== taskId)
: [...this.state.selectedTaskIds, taskId];
this.setState({ selectedTaskIds: selected }, 'SELECT_TASK');
} else {
this.setState({ selectedTaskIds: [taskId] }, 'SELECT_TASK');
}
}
deselectTask(taskId) {
this.setState({
selectedTaskIds: this.state.selectedTaskIds.filter(id => id !== taskId)
}, 'DESELECT_TASK');
}
clearSelection() {
this.setState({ selectedTaskIds: [] }, 'CLEAR_SELECTION');
}
selectAllInColumn(columnId) {
const taskIds = this.getTasksByColumn(columnId).map(t => t.id);
this.setState({ selectedTaskIds: taskIds }, 'SELECT_ALL_IN_COLUMN');
}
// =====================
// UI STATE ACTIONS
// =====================
setCurrentView(view) {
this.setState({ currentView: view }, 'SET_VIEW');
}
setLoading(isLoading) {
this.setState({ isLoading }, 'SET_LOADING');
}
setOnline(isOnline) {
this.setState({
isOnline,
syncStatus: isOnline ? 'synced' : 'offline'
}, 'SET_ONLINE');
}
setSyncStatus(status) {
this.setState({ syncStatus: status }, 'SET_SYNC_STATUS');
}
setDragState(dragState) {
this.setState({ dragState }, 'SET_DRAG_STATE');
}
setEditingTask(task) {
this.setState({ editingTask: task }, 'SET_EDITING_TASK');
}
// =====================
// MODAL ACTIONS
// =====================
openModal(modalId) {
if (!this.state.openModals.includes(modalId)) {
this.setState({
openModals: [...this.state.openModals, modalId]
}, 'OPEN_MODAL');
}
}
closeModal(modalId) {
this.setState({
openModals: this.state.openModals.filter(id => id !== modalId)
}, 'CLOSE_MODAL');
}
closeAllModals() {
this.setState({ openModals: [] }, 'CLOSE_ALL_MODALS');
}
isModalOpen(modalId) {
return this.state.openModals.includes(modalId);
}
// =====================
// UNDO/REDO ACTIONS
// =====================
pushUndo(action) {
const undoStack = [...this.state.undoStack, action];
// Limit stack size
if (undoStack.length > this.state.maxUndoHistory) {
undoStack.shift();
}
this.setState({
undoStack,
redoStack: [] // Clear redo stack on new action
}, 'PUSH_UNDO');
}
popUndo() {
if (this.state.undoStack.length === 0) return null;
const undoStack = [...this.state.undoStack];
const action = undoStack.pop();
this.setState({
undoStack,
redoStack: [...this.state.redoStack, action]
}, 'POP_UNDO');
return action;
}
popRedo() {
if (this.state.redoStack.length === 0) return null;
const redoStack = [...this.state.redoStack];
const action = redoStack.pop();
this.setState({
redoStack,
undoStack: [...this.state.undoStack, action]
}, 'POP_REDO');
return action;
}
canUndo() {
return this.state.undoStack.length > 0;
}
canRedo() {
return this.state.redoStack.length > 0;
}
// =====================
// USER ACTIONS
// =====================
setCurrentUser(user) {
this.setState({ currentUser: user }, 'SET_CURRENT_USER');
}
setUsers(users) {
this.setState({ users }, 'SET_USERS');
}
getUserById(userId) {
return this.state.users.find(u => u.id === userId);
}
}
// Create singleton instance
const store = new Store();
// Debug middleware (only in development)
if (window.location.hostname === 'localhost') {
store.use((prevState, updates, actionType) => {
console.log(`[Store] ${actionType}:`, updates);
return updates;
});
}
// Persistence middleware for filters (EXCLUDING search to prevent "hanging" search)
store.subscribe('filters', (filters) => {
// Save filters WITHOUT search - search should not persist across sessions
const { search, ...filtersToSave } = filters;
localStorage.setItem('task_filters', JSON.stringify(filtersToSave));
});
// Load persisted filters
const savedFilters = localStorage.getItem('task_filters');
if (savedFilters) {
try {
const parsed = JSON.parse(savedFilters);
// Ensure search is always empty on load
store.setFilters({ ...parsed, search: '' });
} catch (e) {
// Ignore parse errors
}
}
export default store;