- Replace raw fetch() with apiPost/apiPatch/apiDelete in: - ImportExportActions.tsx: POST for file imports - KanbanBoard.tsx: PATCH for task status updates - ActiveProjectsWidget.tsx: POST for widget data fetches - useLayouts.ts: POST/PATCH/DELETE for layout management - Add apiPostFormData() method to API client for FormData uploads - Ensures CSRF token is included in all state-changing requests - Update tests to mock CSRF token fetch for API client usage Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
141 lines
3.7 KiB
TypeScript
141 lines
3.7 KiB
TypeScript
/**
|
|
* React Query hooks for layout management
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import type { UseQueryResult, UseMutationResult } from "@tanstack/react-query";
|
|
import type { UserLayout, WidgetPlacement } from "@mosaic/shared";
|
|
import { apiGet, apiPost, apiPatch, apiDelete } from "@/lib/api/client";
|
|
|
|
const LAYOUTS_KEY = ["layouts"];
|
|
|
|
interface CreateLayoutData {
|
|
name: string;
|
|
layout: WidgetPlacement[];
|
|
isDefault?: boolean;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
interface UpdateLayoutData {
|
|
id: string;
|
|
name?: string;
|
|
layout?: WidgetPlacement[];
|
|
isDefault?: boolean;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Fetch all layouts for the current user
|
|
*/
|
|
export function useLayouts(): UseQueryResult<UserLayout[]> {
|
|
return useQuery<UserLayout[]>({
|
|
queryKey: LAYOUTS_KEY,
|
|
queryFn: async (): Promise<UserLayout[]> => {
|
|
return apiGet<UserLayout[]>("/api/layouts");
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch a single layout by ID
|
|
*/
|
|
export function useLayout(id: string): UseQueryResult<UserLayout> {
|
|
return useQuery<UserLayout>({
|
|
queryKey: [...LAYOUTS_KEY, id],
|
|
queryFn: async (): Promise<UserLayout> => {
|
|
return apiGet<UserLayout>(`/api/layouts/${id}`);
|
|
},
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch the default layout
|
|
*/
|
|
export function useDefaultLayout(): UseQueryResult<UserLayout> {
|
|
return useQuery<UserLayout>({
|
|
queryKey: [...LAYOUTS_KEY, "default"],
|
|
queryFn: async (): Promise<UserLayout> => {
|
|
return apiGet<UserLayout>("/api/layouts/default");
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new layout (uses API client for CSRF protection)
|
|
*/
|
|
export function useCreateLayout(): UseMutationResult<UserLayout, Error, CreateLayoutData> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (data: CreateLayoutData): Promise<UserLayout> => {
|
|
return apiPost<UserLayout>("/api/layouts", data);
|
|
},
|
|
onSuccess: (): void => {
|
|
// Invalidate layouts cache to refetch
|
|
void queryClient.invalidateQueries({ queryKey: LAYOUTS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update an existing layout (uses API client for CSRF protection)
|
|
*/
|
|
export function useUpdateLayout(): UseMutationResult<UserLayout, Error, UpdateLayoutData> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id, ...data }: UpdateLayoutData): Promise<UserLayout> => {
|
|
return apiPatch<UserLayout>(`/api/layouts/${id}`, data);
|
|
},
|
|
onSuccess: (_data, variables): void => {
|
|
// Invalidate affected queries
|
|
void queryClient.invalidateQueries({ queryKey: LAYOUTS_KEY });
|
|
void queryClient.invalidateQueries({ queryKey: [...LAYOUTS_KEY, variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a layout (uses API client for CSRF protection)
|
|
*/
|
|
export function useDeleteLayout(): UseMutationResult<void, Error, string> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (id: string): Promise<void> => {
|
|
await apiDelete(`/api/layouts/${id}`);
|
|
},
|
|
onSuccess: (): void => {
|
|
// Invalidate layouts cache to refetch
|
|
void queryClient.invalidateQueries({ queryKey: LAYOUTS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
interface UseSaveLayoutReturn {
|
|
saveLayout: (layout: WidgetPlacement[]) => void;
|
|
isSaving: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Helper hook to save layout changes with debouncing
|
|
*/
|
|
export function useSaveLayout(layoutId: string): UseSaveLayoutReturn {
|
|
const updateLayout = useUpdateLayout();
|
|
|
|
const saveLayout = (layout: WidgetPlacement[]): void => {
|
|
updateLayout.mutate({
|
|
id: layoutId,
|
|
layout,
|
|
});
|
|
};
|
|
|
|
return {
|
|
saveLayout,
|
|
isSaving: updateLayout.isPending,
|
|
error: updateLayout.error,
|
|
};
|
|
}
|