- Add comprehensive TDD documentation to CLAUDE.md with coverage requirements and examples - Extract reusable UI components to frontend/src/components/ui/ (Modal, FormInput, Button, Alert, etc.) - Add shared constants (schedulePresets) and utility hooks (useCrudMutation, useFormValidation) - Update frontend/CLAUDE.md with component documentation and usage examples - Refactor CreateTaskModal to use shared components and constants - Fix test assertions to be more robust and accurate across all test files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
import { useMutation, useQueryClient, QueryKey, UseMutationOptions } from '@tanstack/react-query';
|
|
import apiClient from '../api/client';
|
|
import { AxiosError, AxiosResponse } from 'axios';
|
|
|
|
type HttpMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
|
|
interface CrudMutationConfig<TData, TVariables, TResponse = TData> {
|
|
/** The API endpoint (e.g., '/resources') */
|
|
endpoint: string;
|
|
/** HTTP method */
|
|
method: HttpMethod;
|
|
/** Query keys to invalidate on success */
|
|
invalidateKeys?: QueryKey[];
|
|
/** Transform response data */
|
|
transformResponse?: (response: AxiosResponse<TResponse>) => TData;
|
|
/** React Query mutation options */
|
|
options?: Omit<
|
|
UseMutationOptions<TData, AxiosError, TVariables>,
|
|
'mutationFn'
|
|
>;
|
|
}
|
|
|
|
/**
|
|
* Generic CRUD mutation hook factory
|
|
*
|
|
* @example
|
|
* // Create a resource
|
|
* const useCreateResource = () => useCrudMutation<Resource, CreateResourceData>({
|
|
* endpoint: '/resources',
|
|
* method: 'POST',
|
|
* invalidateKeys: [['resources']],
|
|
* });
|
|
*
|
|
* @example
|
|
* // Update a resource
|
|
* const useUpdateResource = () => useCrudMutation<Resource, { id: string; data: Partial<Resource> }>({
|
|
* endpoint: '/resources',
|
|
* method: 'PATCH',
|
|
* invalidateKeys: [['resources']],
|
|
* });
|
|
*/
|
|
export function useCrudMutation<TData = unknown, TVariables = unknown, TResponse = TData>({
|
|
endpoint,
|
|
method,
|
|
invalidateKeys = [],
|
|
transformResponse,
|
|
options = {},
|
|
}: CrudMutationConfig<TData, TVariables, TResponse>) {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<TData, AxiosError, TVariables>({
|
|
mutationFn: async (variables: TVariables) => {
|
|
let response: AxiosResponse<TResponse>;
|
|
|
|
// Handle different variable shapes for different methods
|
|
if (method === 'DELETE') {
|
|
// For DELETE, variables is typically just the ID
|
|
const id = typeof variables === 'object' && variables !== null && 'id' in variables
|
|
? (variables as { id: string | number }).id
|
|
: variables;
|
|
response = await apiClient.delete(`${endpoint}/${id}/`);
|
|
} else if (method === 'PUT' || method === 'PATCH') {
|
|
// For PUT/PATCH, variables should have id and data
|
|
if (typeof variables === 'object' && variables !== null && 'id' in variables) {
|
|
const { id, ...data } = variables as { id: string | number; [key: string]: unknown };
|
|
response = await apiClient[method.toLowerCase() as 'put' | 'patch'](`${endpoint}/${id}/`, data);
|
|
} else {
|
|
// If no id, just send to the endpoint
|
|
response = await apiClient[method.toLowerCase() as 'put' | 'patch'](`${endpoint}/`, variables);
|
|
}
|
|
} else {
|
|
// POST - create new
|
|
response = await apiClient.post(`${endpoint}/`, variables);
|
|
}
|
|
|
|
return transformResponse ? transformResponse(response) : (response.data as unknown as TData);
|
|
},
|
|
onSuccess: (data, variables, context) => {
|
|
// Invalidate specified query keys
|
|
invalidateKeys.forEach((key) => {
|
|
queryClient.invalidateQueries({ queryKey: key });
|
|
});
|
|
|
|
// Call custom onSuccess if provided
|
|
options.onSuccess?.(data, variables, context);
|
|
},
|
|
...options,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create hook factory for a resource
|
|
*/
|
|
export function createCrudHooks<
|
|
TResource,
|
|
TCreateData = Partial<TResource>,
|
|
TUpdateData = Partial<TResource>
|
|
>(endpoint: string, queryKey: string) {
|
|
return {
|
|
useCreate: (options?: UseMutationOptions<TResource, AxiosError, TCreateData>) =>
|
|
useCrudMutation<TResource, TCreateData>({
|
|
endpoint,
|
|
method: 'POST',
|
|
invalidateKeys: [[queryKey]],
|
|
options,
|
|
}),
|
|
|
|
useUpdate: (options?: UseMutationOptions<TResource, AxiosError, { id: string | number } & TUpdateData>) =>
|
|
useCrudMutation<TResource, { id: string | number } & TUpdateData>({
|
|
endpoint,
|
|
method: 'PATCH',
|
|
invalidateKeys: [[queryKey]],
|
|
options,
|
|
}),
|
|
|
|
useDelete: (options?: UseMutationOptions<void, AxiosError, string | number>) =>
|
|
useCrudMutation<void, string | number>({
|
|
endpoint,
|
|
method: 'DELETE',
|
|
invalidateKeys: [[queryKey]],
|
|
options: options as UseMutationOptions<void, AxiosError, string | number>,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export default useCrudMutation;
|