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:
16
TODO.md
16
TODO.md
@@ -204,14 +204,14 @@
|
||||
- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions
|
||||
|
||||
#### Actions Tags (Priorité 4)
|
||||
- [ ] Créer `actions/tags.ts` pour la gestion tags
|
||||
- [ ] `createTag(name, color)` - Création tag
|
||||
- [ ] `updateTag(tagId, data)` - Modification tag
|
||||
- [ ] `deleteTag(tagId)` - Suppression tag
|
||||
- [ ] Modifier les formulaires tags pour server actions
|
||||
- [ ] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
|
||||
- [ ] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
|
||||
- [ ] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
|
||||
- [x] Créer `actions/tags.ts` pour la gestion tags
|
||||
- [x] `createTag(name, color)` - Création tag
|
||||
- [x] `updateTag(tagId, data)` - Modification tag
|
||||
- [x] `deleteTag(tagId)` - Suppression tag
|
||||
- [x] Modifier les formulaires tags pour server actions
|
||||
- [x] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
|
||||
- [x] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
|
||||
- [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
|
||||
|
||||
#### Migration progressive avec nettoyage immédiat
|
||||
**Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { HttpClient } from './base/http-client';
|
||||
import { Tag, ApiResponse } from '@/lib/types';
|
||||
|
||||
// Types pour les requêtes
|
||||
export interface CreateTagData {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface UpdateTagData {
|
||||
tagId: string;
|
||||
name?: string;
|
||||
color?: string;
|
||||
}
|
||||
// Types pour les requêtes (now only used for validation - CRUD operations moved to server actions)
|
||||
|
||||
export interface TagFilters {
|
||||
q?: string; // Recherche par nom
|
||||
@@ -89,27 +79,7 @@ export class TagsClient extends HttpClient {
|
||||
return this.get<TagResponse>(`/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau tag
|
||||
*/
|
||||
async createTag(data: CreateTagData): Promise<TagResponse> {
|
||||
return this.post<TagResponse>('', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un tag
|
||||
*/
|
||||
async updateTag(data: UpdateTagData): Promise<TagResponse> {
|
||||
const { tagId, ...updates } = data;
|
||||
return this.patch<TagResponse>(`/${tagId}`, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un tag
|
||||
*/
|
||||
async deleteTag(id: string): Promise<ApiResponse<void>> {
|
||||
return this.delete<ApiResponse<void>>(`/${id}`);
|
||||
}
|
||||
// CRUD operations removed - now handled by server actions in /actions/tags.ts
|
||||
|
||||
/**
|
||||
* Valide le format d'une couleur hexadécimale
|
||||
@@ -139,9 +109,9 @@ export class TagsClient extends HttpClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les données d'un tag
|
||||
* Valide les données d'un tag (utilisé par les formulaires avant server actions)
|
||||
*/
|
||||
static validateTagData(data: Partial<CreateTagData>): string[] {
|
||||
static validateTagData(data: { name?: string; color?: string }): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!data.name || typeof data.name !== 'string') {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useTransition } from 'react';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { TagsClient } from '@/clients/tags-client';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { createTag, updateTag } from '@/actions/tags';
|
||||
|
||||
interface TagFormProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { name: string; color: string }) => Promise<void>;
|
||||
onSuccess?: () => Promise<void>; // Callback après succès pour refresh
|
||||
tag?: Tag | null; // Si fourni, mode édition
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const PRESET_COLORS = [
|
||||
@@ -30,7 +30,8 @@ const PRESET_COLORS = [
|
||||
'#F43F5E', // Rose
|
||||
];
|
||||
|
||||
export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: TagFormProps) {
|
||||
export function TagForm({ isOpen, onClose, onSuccess, tag }: TagFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
color: '#3B82F6',
|
||||
@@ -66,12 +67,42 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onSubmit(formData);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
setErrors([error instanceof Error ? error.message : 'Erreur inconnue']);
|
||||
}
|
||||
startTransition(async () => {
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (tag) {
|
||||
// Mode édition
|
||||
result = await updateTag(tag.id, {
|
||||
name: formData.name,
|
||||
color: formData.color,
|
||||
isPinned: formData.isPinned
|
||||
});
|
||||
} else {
|
||||
// Mode création
|
||||
result = await createTag(formData.name, formData.color);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
onClose();
|
||||
// Reset form
|
||||
setFormData({
|
||||
name: '',
|
||||
color: TagsClient.generateRandomColor(),
|
||||
isPinned: false
|
||||
});
|
||||
setErrors([]);
|
||||
// Refresh la liste des tags
|
||||
if (onSuccess) {
|
||||
await onSuccess();
|
||||
}
|
||||
} else {
|
||||
setErrors([result.error || 'Erreur inconnue']);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors([error instanceof Error ? error.message : 'Erreur inconnue']);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleColorSelect = (color: string) => {
|
||||
@@ -102,7 +133,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
||||
}}
|
||||
placeholder="Nom du tag..."
|
||||
maxLength={50}
|
||||
disabled={loading}
|
||||
disabled={isPending}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -150,7 +181,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
||||
type="color"
|
||||
value={formData.color}
|
||||
onChange={handleCustomColorChange}
|
||||
disabled={loading}
|
||||
disabled={isPending}
|
||||
className="w-12 h-8 rounded border border-slate-600 bg-slate-800 cursor-pointer disabled:cursor-not-allowed"
|
||||
/>
|
||||
<Input
|
||||
@@ -163,7 +194,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
||||
}}
|
||||
placeholder="#RRGGBB"
|
||||
maxLength={7}
|
||||
disabled={loading}
|
||||
disabled={isPending}
|
||||
className="w-24 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
@@ -180,7 +211,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
||||
type="checkbox"
|
||||
checked={formData.isPinned}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, isPinned: e.target.checked }))}
|
||||
disabled={loading}
|
||||
disabled={isPending}
|
||||
className="w-4 h-4 rounded border border-slate-600 bg-slate-800 text-purple-600 focus:ring-purple-500 focus:ring-2 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<span className="text-sm text-slate-300">
|
||||
@@ -210,16 +241,16 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
disabled={isPending}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={loading || !formData.name.trim()}
|
||||
disabled={isPending || !formData.name.trim()}
|
||||
>
|
||||
{loading ? 'Enregistrement...' : (tag ? 'Mettre à jour' : 'Créer')}
|
||||
{isPending ? 'Enregistrement...' : (tag ? 'Mettre à jour' : 'Créer')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
101
src/actions/tags.ts
Normal file
101
src/actions/tags.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
'use server';
|
||||
|
||||
import { tagsService } from '@/services/tags';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { Tag } from '@/lib/types';
|
||||
|
||||
export type ActionResult<T = void> = {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Créer un nouveau tag
|
||||
*/
|
||||
export async function createTag(
|
||||
name: string,
|
||||
color: string
|
||||
): Promise<ActionResult<Tag>> {
|
||||
try {
|
||||
const tag = await tagsService.createTag({ name, color });
|
||||
|
||||
// Revalider les pages qui utilisent les tags
|
||||
revalidatePath('/');
|
||||
revalidatePath('/kanban');
|
||||
revalidatePath('/tags');
|
||||
|
||||
return { success: true, data: tag };
|
||||
} catch (error) {
|
||||
console.error('Error creating tag:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create tag'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour un tag existant
|
||||
*/
|
||||
export async function updateTag(
|
||||
tagId: string,
|
||||
data: { name?: string; color?: string; isPinned?: boolean }
|
||||
): Promise<ActionResult<Tag>> {
|
||||
try {
|
||||
const tag = await tagsService.updateTag(tagId, data);
|
||||
|
||||
if (!tag) {
|
||||
return { success: false, error: 'Tag non trouvé' };
|
||||
}
|
||||
|
||||
// Revalider les pages qui utilisent les tags
|
||||
revalidatePath('/');
|
||||
revalidatePath('/kanban');
|
||||
revalidatePath('/tags');
|
||||
|
||||
return { success: true, data: tag };
|
||||
} catch (error) {
|
||||
console.error('Error updating tag:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update tag'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un tag
|
||||
*/
|
||||
export async function deleteTag(tagId: string): Promise<ActionResult> {
|
||||
try {
|
||||
await tagsService.deleteTag(tagId);
|
||||
|
||||
// Revalider les pages qui utilisent les tags
|
||||
revalidatePath('/');
|
||||
revalidatePath('/kanban');
|
||||
revalidatePath('/tags');
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting tag:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete tag'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action rapide pour créer un tag depuis un input
|
||||
*/
|
||||
export async function quickCreateTag(formData: FormData): Promise<ActionResult<Tag>> {
|
||||
const name = formData.get('name') as string;
|
||||
const color = formData.get('color') as string;
|
||||
|
||||
if (!name?.trim()) {
|
||||
return { success: false, error: 'Tag name is required' };
|
||||
}
|
||||
|
||||
return createTag(name.trim(), color || '#3B82F6');
|
||||
}
|
||||
@@ -36,112 +36,4 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/tags/[id] - Met à jour un tag
|
||||
*/
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, color, isPinned } = body;
|
||||
|
||||
// Validation
|
||||
if (name !== undefined && (typeof name !== 'string' || !name.trim())) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le nom du tag doit être une chaîne non vide' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (color !== undefined && (typeof color !== 'string' || !/^#[0-9A-F]{6}$/i.test(color))) {
|
||||
return NextResponse.json(
|
||||
{ error: 'La couleur doit être au format hexadécimal (#RRGGBB)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tag = await tagsService.updateTag(id, { name, color, isPinned });
|
||||
|
||||
if (!tag) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tag non trouvé' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: tag,
|
||||
message: 'Tag mis à jour avec succès'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour du tag:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('non trouvé')) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('existe déjà')) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Erreur lors de la mise à jour du tag',
|
||||
message: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/tags/[id] - Supprime un tag
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
await tagsService.deleteTag(id);
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Tag supprimé avec succès'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression du tag:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('non trouvé')) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('utilisé par')) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Erreur lors de la suppression du tag',
|
||||
message: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
// PATCH and DELETE routes removed - now handled by server actions in /actions/tags.ts
|
||||
|
||||
@@ -41,60 +41,4 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/tags - Crée un nouveau tag
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, color } = body;
|
||||
|
||||
// Validation
|
||||
if (!name || typeof name !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le nom du tag est requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!color || typeof color !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'La couleur du tag est requise' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validation du format couleur (hex)
|
||||
if (!/^#[0-9A-F]{6}$/i.test(color)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'La couleur doit être au format hexadécimal (#RRGGBB)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tag = await tagsService.createTag({ name, color });
|
||||
|
||||
return NextResponse.json({
|
||||
data: tag,
|
||||
message: 'Tag créé avec succès'
|
||||
}, { status: 201 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du tag:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('existe déjà')) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Erreur lors de la création du tag',
|
||||
message: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
// POST route removed - now handled by server actions in /actions/tags.ts
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { useTags } from '@/hooks/useTags';
|
||||
import { CreateTagData } from '@/clients/tags-client';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { TagForm } from '@/components/forms/TagForm';
|
||||
@@ -19,8 +18,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
tags,
|
||||
loading,
|
||||
error,
|
||||
createTag,
|
||||
updateTag,
|
||||
refreshTags,
|
||||
deleteTag
|
||||
} = useTags(initialTags as (Tag & { usage: number })[]);
|
||||
|
||||
@@ -50,29 +48,21 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
});
|
||||
}, [tags, searchQuery]);
|
||||
|
||||
const handleCreateTag = async (data: CreateTagData) => {
|
||||
await createTag(data);
|
||||
setIsCreateModalOpen(false);
|
||||
};
|
||||
|
||||
const handleEditTag = (tag: Tag) => {
|
||||
setEditingTag(tag);
|
||||
};
|
||||
|
||||
const handleUpdateTag = async (data: { name: string; color: string; isPinned?: boolean }) => {
|
||||
if (editingTag) {
|
||||
await updateTag({
|
||||
tagId: editingTag.id,
|
||||
...data
|
||||
});
|
||||
setEditingTag(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = async (tag: Tag) => {
|
||||
if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingTagId(tag.id);
|
||||
try {
|
||||
// Utiliser la server action directement depuis useTags
|
||||
await deleteTag(tag.id);
|
||||
// Refresh la liste des tags
|
||||
await refreshTags();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression:', error);
|
||||
} finally {
|
||||
@@ -220,16 +210,14 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
<TagForm
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSubmit={handleCreateTag}
|
||||
loading={false}
|
||||
onSuccess={refreshTags}
|
||||
/>
|
||||
|
||||
<TagForm
|
||||
isOpen={!!editingTag}
|
||||
onClose={() => setEditingTag(null)}
|
||||
onSubmit={handleUpdateTag}
|
||||
onSuccess={refreshTags}
|
||||
tag={editingTag}
|
||||
loading={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user