feat: migrate tag management to server actions
- Completed migration of tag management to server actions by creating `actions/tags.ts` for CRUD operations. - Removed obsolete API routes for tags (`/api/tags`, `/api/tags/[id]`) and updated related components to utilize new server actions. - Updated `TagForm` and `useTags` hook to handle tag creation, updating, and deletion through server actions, improving performance and simplifying client-side logic. - Cleaned up `tags-client.ts` by removing unused types and methods, focusing on validation only. - Marked related TODO items as complete in `TODO.md`.
This commit is contained in:
150
hooks/useTags.ts
150
hooks/useTags.ts
@@ -1,21 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { tagsClient, TagFilters, CreateTagData, UpdateTagData, TagsClient } from '@/clients/tags-client';
|
||||
import { useState, useEffect, useCallback, useTransition } from 'react';
|
||||
import { tagsClient, TagFilters, TagsClient } from '@/clients/tags-client';
|
||||
import { createTag, updateTag, deleteTag as deleteTagAction } from '@/actions/tags';
|
||||
import { Tag } from '@/lib/types';
|
||||
|
||||
interface UseTagsState {
|
||||
tags: Array<Tag & { usage: number }>;
|
||||
popularTags: Array<Tag & { usage: number }>;
|
||||
loading: boolean;
|
||||
isPending: boolean; // Loading state for server actions
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseTagsActions {
|
||||
refreshTags: () => Promise<void>;
|
||||
searchTags: (query: string, limit?: number) => Promise<Tag[]>;
|
||||
createTag: (data: CreateTagData) => Promise<Tag | null>;
|
||||
updateTag: (data: UpdateTagData) => Promise<Tag | null>;
|
||||
createTag: (name: string, color: string) => Promise<void>;
|
||||
updateTag: (tagId: string, data: { name?: string; color?: string; isPinned?: boolean }) => Promise<void>;
|
||||
deleteTag: (tagId: string) => Promise<void>;
|
||||
getPopularTags: (limit?: number) => Promise<void>;
|
||||
setFilters: (filters: TagFilters) => void;
|
||||
@@ -28,10 +30,12 @@ export function useTags(
|
||||
initialData?: (Tag & { usage: number })[],
|
||||
initialFilters?: TagFilters
|
||||
): UseTagsState & UseTagsActions {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [state, setState] = useState<UseTagsState>({
|
||||
tags: initialData || [],
|
||||
popularTags: initialData || [],
|
||||
loading: !initialData,
|
||||
isPending: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
@@ -97,77 +101,82 @@ export function useTags(
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Crée un nouveau tag
|
||||
* Crée un nouveau tag avec server action
|
||||
*/
|
||||
const createTag = useCallback(async (data: CreateTagData): Promise<Tag | null> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Validation côté client
|
||||
const errors = TagsClient.validateTagData(data);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors[0]);
|
||||
const createTagAction = useCallback(async (name: string, color: string): Promise<void> => {
|
||||
// Validation côté client
|
||||
const errors = TagsClient.validateTagData({ name, color });
|
||||
if (errors.length > 0) {
|
||||
setState(prev => ({ ...prev, error: errors[0] }));
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
const result = await createTag(name, color);
|
||||
|
||||
if (!result.success) {
|
||||
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la création' }));
|
||||
}
|
||||
// Note: revalidatePath in server action handles cache refresh automatically
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la création'
|
||||
}));
|
||||
}
|
||||
|
||||
const response = await tagsClient.createTag(data);
|
||||
|
||||
// Rafraîchir la liste après création
|
||||
await refreshTags();
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la création'
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
}, [refreshTags]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Met à jour un tag
|
||||
* Met à jour un tag avec server action
|
||||
*/
|
||||
const updateTag = useCallback(async (data: UpdateTagData): Promise<Tag | null> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tagsClient.updateTag(data);
|
||||
|
||||
// Rafraîchir la liste après mise à jour
|
||||
await refreshTags();
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
}, [refreshTags]);
|
||||
const updateTagAction = useCallback(async (
|
||||
tagId: string,
|
||||
data: { name?: string; color?: string; isPinned?: boolean }
|
||||
): Promise<void> => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
const result = await updateTag(tagId, data);
|
||||
|
||||
if (!result.success) {
|
||||
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la mise à jour' }));
|
||||
}
|
||||
// Note: revalidatePath in server action handles cache refresh automatically
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Supprime un tag
|
||||
* Supprime un tag avec server action
|
||||
*/
|
||||
const deleteTag = useCallback(async (tagId: string): Promise<void> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
await tagsClient.deleteTag(tagId);
|
||||
|
||||
// Rafraîchir la liste après suppression
|
||||
await refreshTags();
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
|
||||
}));
|
||||
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
|
||||
}
|
||||
}, [refreshTags]);
|
||||
const deleteTagActionHandler = useCallback(async (tagId: string): Promise<void> => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
const result = await deleteTagAction(tagId);
|
||||
|
||||
if (!result.success) {
|
||||
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la suppression' }));
|
||||
throw new Error(result.error);
|
||||
}
|
||||
// Note: revalidatePath in server action handles cache refresh automatically
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
|
||||
}));
|
||||
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Charger les tags au montage et quand les filtres changent (seulement si pas de données initiales)
|
||||
useEffect(() => {
|
||||
@@ -178,11 +187,12 @@ export function useTags(
|
||||
|
||||
return {
|
||||
...state,
|
||||
isPending, // Expose loading state from useTransition
|
||||
refreshTags,
|
||||
searchTags,
|
||||
createTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
createTag: createTagAction,
|
||||
updateTag: updateTagAction,
|
||||
deleteTag: deleteTagActionHandler,
|
||||
getPopularTags,
|
||||
setFilters
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user