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:
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