refactor: use Zustand for task state, optimize GraphQL requests, and fix recursive setState bug

This commit is contained in:
Sean Sube 2025-06-17 18:24:34 -05:00
parent b4d1c5db4e
commit 546ef8216d
No known key found for this signature in database
GPG Key ID: 3EED7B957D362AF1
7 changed files with 912 additions and 7 deletions

View File

@ -52,3 +52,117 @@ export default tseslint.config({
},
})
```
# Task Receipts Client
A React application for managing task receipts with GraphQL API integration and Zustand state management.
## Architecture
### State Management with Zustand
The application uses Zustand for state management to optimize GraphQL API requests and improve performance:
#### Key Features:
- **Centralized State**: All task data is stored in a single Zustand store
- **Optimized Queries**: GraphQL queries are only made when data is not already available in the store
- **Efficient Updates**: Mutations update the store directly, reducing the need for refetching
- **Selective Rendering**: Components only re-render when their specific data changes
#### Store Structure:
```typescript
interface TaskState {
// Data
groups: GroupWithTasks[];
recentTasks: TaskWithSteps[];
frequentTasks: TaskWithSteps[];
currentStep: StepWithNotes | null;
// Loading states
isLoadingGroups: boolean;
isLoadingRecentTasks: boolean;
isLoadingFrequentTasks: boolean;
isLoadingStep: boolean;
// Selection state
selectedGroup: GroupWithTasks | undefined;
selectedTask: TaskWithSteps | undefined;
selectedStep: StepWithNotes | undefined;
// Actions and helper methods...
}
```
#### Hooks:
1. **`useTaskDataOptimized`**: Main hook that manages GraphQL queries and store updates
2. **`useTaskSelector`**: Collection of selector hooks for efficient data access
3. **`useTaskStore`**: Direct access to the Zustand store
#### Performance Optimizations:
- **Skip Queries**: Queries are skipped if data already exists in the store
- **Cache-First Policy**: Apollo Client uses cache-first for queries
- **Selective Updates**: Only relevant parts of the store are updated
- **Efficient Selectors**: Components subscribe only to the data they need
### GraphQL Integration
The application uses Apollo Client for GraphQL operations with optimized caching policies:
- **Cache-First**: Queries prioritize cached data
- **Cache-and-Network**: Watch queries update from cache first, then network
- **Optimistic Updates**: Mutations update the store immediately
### Component Structure
- **App.tsx**: Main application component using the optimized hook
- **Layouts**: Desktop and mobile layouts for different screen sizes
- **Components**: Reusable UI components
- **Hooks**: Custom hooks for data management and device detection
## Development
### Prerequisites
- Node.js 18+
- npm or yarn
### Installation
```bash
npm install
```
### Development Server
```bash
npm run dev
```
### Build
```bash
npm run build
```
## State Management Flow
1. **Initial Load**:
- `useTaskDataOptimized` checks if data exists in store
- If not, makes GraphQL queries to fetch data
- Updates store with fetched data
2. **User Interactions**:
- Selection changes update store directly
- Mutations update store optimistically
- Components re-render only when their data changes
3. **Data Persistence**:
- Store maintains data across component unmounts
- Queries are skipped if data is already available
- Cache policies ensure efficient data retrieval
## Benefits
- **Reduced API Calls**: Data is fetched once and reused
- **Better Performance**: Components only re-render when necessary
- **Improved UX**: Faster navigation and interactions
- **Maintainable Code**: Centralized state management
- **Type Safety**: Full TypeScript support with proper typing

View File

@ -13,11 +13,12 @@
"@apollo/client": "^3.9.5",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.10",
"@mui/material": "^5.17.1",
"@task-receipts/shared": "file:../shared",
"graphql": "^16.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"zustand": "^5.0.5"
},
"devDependencies": {
"@types/react": "^18.2.55",

View File

@ -1,10 +1,10 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { ApolloProvider } from '@apollo/client';
import { client } from './graphql/client';
import { DesktopLayout } from './layouts/DesktopLayout';
import { MobileLayout } from './layouts/MobileLayout';
import { UserSelection } from './components/UserSelection';
import { useTaskData } from './hooks/useTaskData';
import { useTaskDataOptimized } from './hooks/useTaskDataOptimized';
import { useDeviceType } from './hooks/useDeviceType';
import { useUserSelection } from './hooks/useUserSelection';
import './App.css';
@ -25,7 +25,7 @@ export function App() {
handlePrintTask,
handlePrintStep,
handleAddNote
} = useTaskData();
} = useTaskDataOptimized();
const handleBack = useCallback(() => {
if (selectedStep) {

View File

@ -58,11 +58,11 @@ export const client = new ApolloClient({
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'network-only',
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'network-only',
fetchPolicy: 'cache-first',
errorPolicy: 'all',
},
},

View File

@ -0,0 +1,446 @@
import { useEffect } from 'react';
import { useQuery, useMutation } from '@apollo/client';
import { GET_GROUPS, GET_TASKS, GET_STEP, GET_RECENT_TASKS, GET_FREQUENT_TASKS } from '../graphql/queries';
import { PRINT_TASK, PRINT_STEP, CREATE_NOTE, CREATE_GROUP, CREATE_TASK, CREATE_STEP } from '../graphql/mutations';
import { useTaskStore } from '../stores/taskStore';
import type { GroupWithTasks, TaskWithSteps, StepWithNotes } from '../types';
import { doesExist, isArray } from '../utils/typeGuards';
interface GraphQLStep {
id: number;
name: string;
instructions: string;
taskId: number;
order: number;
notes: GraphQLNote[];
print_count: number;
last_printed_at: string | null;
}
interface GraphQLNote {
id: number;
content: string;
created_at: string;
updated_at: string;
user: {
id: number;
name: string;
};
}
interface GraphQLTask {
id: number;
name: string;
groupId: number;
steps: GraphQLStep[];
notes: GraphQLNote[];
print_count: number;
last_printed_at: string | null;
}
interface GraphQLGroup {
id: number;
name: string;
tasks: GraphQLTask[];
created_at: string;
updated_at: string;
}
function toStepWithNotes(step: GraphQLStep | null | undefined): StepWithNotes | null {
if (!doesExist(step)) return null;
return {
id: step.id,
name: step.name,
instructions: step.instructions,
task_id: step.taskId,
order: step.order,
print_count: step.print_count,
last_printed_at: step.last_printed_at ? new Date(step.last_printed_at) : undefined,
notes: isArray(step.notes) ? step.notes.map(note => ({
id: note.id,
content: note.content,
created_at: new Date(note.created_at),
updated_at: new Date(note.updated_at),
created_by: note.user.id,
user: {
id: note.user.id,
name: note.user.name,
},
})) : [],
};
}
function toTaskWithSteps(task: GraphQLTask | null | undefined): TaskWithSteps | null {
if (!doesExist(task)) return null;
return {
id: task.id,
name: task.name,
group_id: task.groupId,
print_count: task.print_count,
last_printed_at: task.last_printed_at ? new Date(task.last_printed_at) : undefined,
steps: isArray(task.steps) ? task.steps.map(step => toStepWithNotes(step)).filter(doesExist) : [],
notes: isArray(task.notes) ? task.notes.map(note => ({
id: note.id,
content: note.content,
created_at: new Date(note.created_at),
updated_at: new Date(note.updated_at),
created_by: note.user.id,
user: {
id: note.user.id,
name: note.user.name,
},
})) : [],
};
}
function toGroupWithTasks(group: GraphQLGroup | null | undefined): GroupWithTasks | null {
if (!doesExist(group)) return null;
return {
id: group.id,
name: group.name,
created_at: new Date(group.created_at),
updated_at: new Date(group.updated_at),
tasks: isArray(group.tasks) ? group.tasks.map(task => toTaskWithSteps(task)).filter(doesExist) : [],
};
}
export function useTaskDataOptimized() {
const {
groups,
recentTasks,
frequentTasks,
currentStep,
isLoadingGroups,
isLoadingRecentTasks,
isLoadingFrequentTasks,
isLoadingStep,
selectedGroup,
selectedTask,
selectedStep,
setGroups,
setRecentTasks,
setFrequentTasks,
setCurrentStep,
setLoadingGroups,
setLoadingRecentTasks,
setLoadingFrequentTasks,
setLoadingStep,
setSelectedGroup,
setSelectedTask,
setSelectedStep,
updateTaskPrintCount,
updateStepPrintCount,
addNoteToStep,
addTaskToGroup,
addStepToTask,
getTaskById,
getStepById,
} = useTaskStore();
// Only fetch groups if we don't have them already
const { data: groupsData, loading: groupsLoading } = useQuery(GET_GROUPS, {
skip: groups.length > 0,
fetchPolicy: 'cache-first',
});
// Only fetch recent tasks if we don't have them already
const { data: recentTasksData, loading: recentTasksLoading } = useQuery(GET_RECENT_TASKS, {
skip: recentTasks.length > 0,
fetchPolicy: 'cache-first',
});
// Only fetch frequent tasks if we don't have them already
const { data: frequentTasksData, loading: frequentTasksLoading } = useQuery(GET_FREQUENT_TASKS, {
skip: frequentTasks.length > 0,
fetchPolicy: 'cache-first',
});
// Only fetch tasks for a group if we don't have them already
const { data: tasksData, loading: tasksLoading } = useQuery(GET_TASKS, {
variables: { groupId: selectedGroup?.id },
skip: !selectedGroup?.id || (selectedGroup && selectedGroup.tasks.length > 0),
fetchPolicy: 'cache-first',
});
// Only fetch step details if we don't have them already
const { data: stepData, loading: stepLoading } = useQuery(GET_STEP, {
variables: { id: selectedStep?.id },
skip: !selectedStep?.id || (selectedStep && selectedStep.notes.length > 0),
fetchPolicy: 'cache-first',
});
// Mutations
const [printTask] = useMutation(PRINT_TASK);
const [printStep] = useMutation(PRINT_STEP);
const [createNote] = useMutation(CREATE_NOTE);
const [createGroup] = useMutation(CREATE_GROUP);
const [createTask] = useMutation(CREATE_TASK);
const [createStep] = useMutation(CREATE_STEP);
// Update store when data is fetched
useEffect(() => {
if (groupsData?.groups && groupsData.groups.length > 0) {
const processedGroups = groupsData.groups
.map((group: GraphQLGroup) => toGroupWithTasks(group))
.filter(doesExist);
setGroups(processedGroups);
}
}, [groupsData, setGroups]);
useEffect(() => {
if (recentTasksData?.recentTasks && recentTasksData.recentTasks.length > 0) {
const processedTasks = recentTasksData.recentTasks
.map((task: GraphQLTask) => toTaskWithSteps(task))
.filter(doesExist);
setRecentTasks(processedTasks);
}
}, [recentTasksData, setRecentTasks]);
useEffect(() => {
if (frequentTasksData?.frequentTasks && frequentTasksData.frequentTasks.length > 0) {
const processedTasks = frequentTasksData.frequentTasks
.map((task: GraphQLTask) => toTaskWithSteps(task))
.filter(doesExist);
setFrequentTasks(processedTasks);
}
}, [frequentTasksData, setFrequentTasks]);
// Update tasks in the store when fetched, but don't update selectedGroup
useEffect(() => {
if (tasksData?.tasks && tasksData.tasks.length > 0 && selectedGroup) {
const processedTasks = tasksData.tasks
.map((task: GraphQLTask) => toTaskWithSteps(task))
.filter(doesExist);
// Update the groups array with the new tasks for this group
const updatedGroups = groups.map(group =>
group.id === selectedGroup.id
? { ...group, tasks: processedTasks }
: group
);
setGroups(updatedGroups);
}
}, [tasksData, selectedGroup?.id, groups, setGroups]);
// Update step details in the store when fetched, but don't update selectedStep
useEffect(() => {
if (stepData?.step && selectedStep) {
const processedStep = toStepWithNotes(stepData.step);
if (processedStep) {
setCurrentStep(processedStep);
}
}
}, [stepData, selectedStep?.id, setCurrentStep]);
// Update loading states
useEffect(() => {
setLoadingGroups(groupsLoading);
}, [groupsLoading, setLoadingGroups]);
useEffect(() => {
setLoadingRecentTasks(recentTasksLoading);
}, [recentTasksLoading, setLoadingRecentTasks]);
useEffect(() => {
setLoadingFrequentTasks(frequentTasksLoading);
}, [frequentTasksLoading, setLoadingFrequentTasks]);
useEffect(() => {
setLoadingStep(stepLoading);
}, [stepLoading, setLoadingStep]);
// Create virtual groups for recent and frequent tasks
const virtualGroups: GroupWithTasks[] = [
{
id: -1,
name: 'Recent Tasks',
created_at: new Date(),
updated_at: new Date(),
tasks: recentTasks,
},
{
id: -2,
name: 'Frequent Tasks',
created_at: new Date(),
updated_at: new Date(),
tasks: frequentTasks,
},
];
const allGroups = [...virtualGroups, ...groups];
const handlePrintTask = async (taskId: string, userId: string) => {
if (!taskId || !userId) {
console.error('Cannot print task: missing taskId or userId');
return;
}
try {
const result = await printTask({ variables: { id: taskId, userId } });
if (result.data?.printTask) {
const task = result.data.printTask;
updateTaskPrintCount(
parseInt(taskId),
task.print_count,
task.last_printed_at
);
}
} catch (error) {
console.error('Error printing task:', error);
}
};
const handlePrintStep = async (stepId: string, userId: string) => {
if (!stepId || !userId) {
console.error('Cannot print step: missing stepId or userId');
return;
}
try {
const result = await printStep({ variables: { id: stepId, userId } });
if (result.data?.printStep) {
const step = result.data.printStep;
updateStepPrintCount(
parseInt(stepId),
step.print_count,
step.last_printed_at
);
}
} catch (error) {
console.error('Error printing step:', error);
}
};
const handleAddNote = async (content: string) => {
if (!selectedStep?.id) {
console.error('Cannot add note: missing stepId');
return;
}
try {
const result = await createNote({
variables: {
content,
stepId: String(selectedStep.id),
},
});
if (result.data?.createNote) {
const note = result.data.createNote;
addNoteToStep(selectedStep.id, {
id: note.id,
content: note.content,
created_at: new Date(note.created_at),
updated_at: new Date(note.updated_at),
created_by: note.user.id,
user: {
id: note.user.id,
name: note.user.name,
},
});
}
} catch (error) {
console.error('Error adding note:', error);
}
};
const handleCreateGroup = async (name: string) => {
if (!name) {
console.error('Cannot create group: missing name');
return;
}
try {
const result = await createGroup({ variables: { name } });
if (result.data?.createGroup) {
const group = result.data.createGroup;
const newGroup: GroupWithTasks = {
id: group.id,
name: group.name,
created_at: new Date(group.created_at),
updated_at: new Date(group.updated_at),
tasks: [],
};
setGroups([...groups, newGroup]);
}
} catch (error) {
console.error('Error creating group:', error);
}
};
const handleCreateTask = async (name: string, groupId: string) => {
if (!name || !groupId) {
console.error('Cannot create task: missing name or groupId');
return;
}
try {
const result = await createTask({ variables: { name, groupId } });
if (result.data?.createTask) {
const task = result.data.createTask;
const newTask: TaskWithSteps = {
id: task.id,
name: task.name,
group_id: task.group_id,
print_count: 0,
steps: [],
notes: [],
};
addTaskToGroup(parseInt(groupId), newTask);
}
} catch (error) {
console.error('Error creating task:', error);
}
};
const handleCreateStep = async (name: string, instructions: string, taskId: string, order: number) => {
if (!name || !instructions || !taskId || order < 1) {
console.error('Cannot create step: missing required fields');
return;
}
try {
const result = await createStep({ variables: { name, instructions, taskId, order } });
if (result.data?.createStep) {
const step = result.data.createStep;
const newStep: StepWithNotes = {
id: step.id,
name: step.name,
instructions: step.instructions,
task_id: step.task_id,
order: step.order,
print_count: 0,
notes: [],
};
addStepToTask(parseInt(taskId), newStep);
}
} catch (error) {
console.error('Error creating step:', error);
}
};
const loading = isLoadingGroups || isLoadingRecentTasks || isLoadingFrequentTasks || isLoadingStep || tasksLoading;
return {
groups: allGroups,
step: currentStep,
loading,
selectedGroup,
selectedTask,
selectedStep,
setSelectedGroup,
setSelectedTask,
setSelectedStep,
handlePrintTask,
handlePrintStep,
handleAddNote,
handleCreateGroup,
handleCreateTask,
handleCreateStep,
getTaskById,
getStepById,
};
}

View File

@ -0,0 +1,85 @@
import { useTaskStore } from '../stores/taskStore';
import type { GroupWithTasks } from '../types';
// Selector hooks for efficient data access
export function useGroups() {
return useTaskStore((state) => state.groups);
}
export function useRecentTasks() {
return useTaskStore((state) => state.recentTasks);
}
export function useFrequentTasks() {
return useTaskStore((state) => state.frequentTasks);
}
export function useSelectedGroup() {
return useTaskStore((state) => state.selectedGroup);
}
export function useSelectedTask() {
return useTaskStore((state) => state.selectedTask);
}
export function useSelectedStep() {
return useTaskStore((state) => state.selectedStep);
}
export function useLoadingStates() {
return useTaskStore((state) => ({
isLoadingGroups: state.isLoadingGroups,
isLoadingRecentTasks: state.isLoadingRecentTasks,
isLoadingFrequentTasks: state.isLoadingFrequentTasks,
isLoadingStep: state.isLoadingStep,
}));
}
export function useTaskActions() {
return useTaskStore((state) => ({
setSelectedGroup: state.setSelectedGroup,
setSelectedTask: state.setSelectedTask,
setSelectedStep: state.setSelectedStep,
getTaskById: state.getTaskById,
getStepById: state.getStepById,
getGroupById: state.getGroupById,
}));
}
// Custom selector for a specific task
export function useTaskById(taskId: number) {
return useTaskStore((state) => state.getTaskById(taskId));
}
// Custom selector for a specific step
export function useStepById(stepId: number) {
return useTaskStore((state) => state.getStepById(stepId));
}
// Custom selector for a specific group
export function useGroupById(groupId: number) {
return useTaskStore((state) => state.getGroupById(groupId));
}
// Selector for all groups including virtual ones
export function useAllGroups() {
return useTaskStore((state) => {
const virtualGroups: GroupWithTasks[] = [
{
id: -1,
name: 'Recent Tasks',
created_at: new Date(),
updated_at: new Date(),
tasks: state.recentTasks,
},
{
id: -2,
name: 'Frequent Tasks',
created_at: new Date(),
updated_at: new Date(),
tasks: state.frequentTasks,
},
];
return [...virtualGroups, ...state.groups];
});
}

View File

@ -0,0 +1,259 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import type { GroupWithTasks, TaskWithSteps, StepWithNotes } from '../types';
interface TaskState {
// Data
groups: GroupWithTasks[];
recentTasks: TaskWithSteps[];
frequentTasks: TaskWithSteps[];
currentStep: StepWithNotes | null;
// Loading states
isLoadingGroups: boolean;
isLoadingRecentTasks: boolean;
isLoadingFrequentTasks: boolean;
isLoadingStep: boolean;
// Selection state
selectedGroup: GroupWithTasks | undefined;
selectedTask: TaskWithSteps | undefined;
selectedStep: StepWithNotes | undefined;
// Actions
setGroups: (groups: GroupWithTasks[]) => void;
setRecentTasks: (tasks: TaskWithSteps[]) => void;
setFrequentTasks: (tasks: TaskWithSteps[]) => void;
setCurrentStep: (step: StepWithNotes | null) => void;
setLoadingGroups: (loading: boolean) => void;
setLoadingRecentTasks: (loading: boolean) => void;
setLoadingFrequentTasks: (loading: boolean) => void;
setLoadingStep: (loading: boolean) => void;
setSelectedGroup: (group: GroupWithTasks | undefined) => void;
setSelectedTask: (task: TaskWithSteps | undefined) => void;
setSelectedStep: (step: StepWithNotes | undefined) => void;
// Helper methods
getTaskById: (taskId: number) => TaskWithSteps | undefined;
getStepById: (stepId: number) => StepWithNotes | undefined;
getGroupById: (groupId: number) => GroupWithTasks | undefined;
// Update methods for mutations
updateTaskPrintCount: (taskId: number, printCount: number, lastPrintedAt: string | null) => void;
updateStepPrintCount: (stepId: number, printCount: number, lastPrintedAt: string | null) => void;
addNoteToStep: (stepId: number, note: any) => void;
addTaskToGroup: (groupId: number, task: TaskWithSteps) => void;
addStepToTask: (taskId: number, step: StepWithNotes) => void;
// Clear methods
clearData: () => void;
}
export const useTaskStore = create<TaskState>()(
devtools(
(set, get) => ({
// Initial state
groups: [],
recentTasks: [],
frequentTasks: [],
currentStep: null,
isLoadingGroups: false,
isLoadingRecentTasks: false,
isLoadingFrequentTasks: false,
isLoadingStep: false,
selectedGroup: undefined,
selectedTask: undefined,
selectedStep: undefined,
// Actions
setGroups: (groups) => set({ groups }),
setRecentTasks: (tasks) => set({ recentTasks: tasks }),
setFrequentTasks: (tasks) => set({ frequentTasks: tasks }),
setCurrentStep: (step) => set({ currentStep: step }),
setLoadingGroups: (loading) => set({ isLoadingGroups: loading }),
setLoadingRecentTasks: (loading) => set({ isLoadingRecentTasks: loading }),
setLoadingFrequentTasks: (loading) => set({ isLoadingFrequentTasks: loading }),
setLoadingStep: (loading) => set({ isLoadingStep: loading }),
setSelectedGroup: (group) => set({ selectedGroup: group }),
setSelectedTask: (task) => set({ selectedTask: task }),
setSelectedStep: (step) => set({ selectedStep: step }),
// Helper methods
getTaskById: (taskId) => {
const state = get();
// Search in all groups
for (const group of state.groups) {
const task = group.tasks.find(t => t.id === taskId);
if (task) return task;
}
// Search in recent tasks
const recentTask = state.recentTasks.find(t => t.id === taskId);
if (recentTask) return recentTask;
// Search in frequent tasks
const frequentTask = state.frequentTasks.find(t => t.id === taskId);
if (frequentTask) return frequentTask;
return undefined;
},
getStepById: (stepId) => {
const state = get();
// Search in all groups
for (const group of state.groups) {
for (const task of group.tasks) {
const step = task.steps.find(s => s.id === stepId);
if (step) return step;
}
}
// Search in recent tasks
for (const task of state.recentTasks) {
const step = task.steps.find(s => s.id === stepId);
if (step) return step;
}
// Search in frequent tasks
for (const task of state.frequentTasks) {
const step = task.steps.find(s => s.id === stepId);
if (step) return step;
}
return undefined;
},
getGroupById: (groupId) => {
const state = get();
return state.groups.find(g => g.id === groupId);
},
// Update methods for mutations
updateTaskPrintCount: (taskId, printCount, lastPrintedAt) => {
set((state) => {
const updateTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
tasks.map(task =>
task.id === taskId
? {
...task,
print_count: printCount,
last_printed_at: lastPrintedAt ? new Date(lastPrintedAt) : null,
}
: task
);
return {
groups: state.groups.map(group => ({
...group,
tasks: updateTaskInArray(group.tasks),
})),
recentTasks: updateTaskInArray(state.recentTasks),
frequentTasks: updateTaskInArray(state.frequentTasks),
};
});
},
updateStepPrintCount: (stepId, printCount, lastPrintedAt) => {
set((state) => {
const updateStepInTask = (task: TaskWithSteps): TaskWithSteps => ({
...task,
steps: task.steps.map(step =>
step.id === stepId
? {
...step,
print_count: printCount,
last_printed_at: lastPrintedAt ? new Date(lastPrintedAt) : null,
}
: step
),
});
const updateTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
tasks.map(task => updateStepInTask(task));
return {
groups: state.groups.map(group => ({
...group,
tasks: updateTaskInArray(group.tasks),
})),
recentTasks: updateTaskInArray(state.recentTasks),
frequentTasks: updateTaskInArray(state.frequentTasks),
};
});
},
addNoteToStep: (stepId, note) => {
set((state) => {
const addNoteToStepInTask = (task: TaskWithSteps): TaskWithSteps => ({
...task,
steps: task.steps.map(step =>
step.id === stepId
? {
...step,
notes: [...step.notes, note],
}
: step
),
});
const updateTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
tasks.map(task => addNoteToStepInTask(task));
return {
groups: state.groups.map(group => ({
...group,
tasks: updateTaskInArray(group.tasks),
})),
recentTasks: updateTaskInArray(state.recentTasks),
frequentTasks: updateTaskInArray(state.frequentTasks),
};
});
},
addTaskToGroup: (groupId, task) => {
set((state) => ({
groups: state.groups.map(group =>
group.id === groupId
? { ...group, tasks: [...group.tasks, task] }
: group
),
}));
},
addStepToTask: (taskId, step) => {
set((state) => {
const addStepToTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
tasks.map(task =>
task.id === taskId
? { ...task, steps: [...task.steps, step] }
: task
);
return {
groups: state.groups.map(group => ({
...group,
tasks: addStepToTaskInArray(group.tasks),
})),
recentTasks: addStepToTaskInArray(state.recentTasks),
frequentTasks: addStepToTaskInArray(state.frequentTasks),
};
});
},
clearData: () => {
set({
groups: [],
recentTasks: [],
frequentTasks: [],
currentStep: null,
selectedGroup: undefined,
selectedTask: undefined,
selectedStep: undefined,
});
},
}),
{
name: 'task-store',
}
)
);