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
|
- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions
|
||||||
|
|
||||||
#### Actions Tags (Priorité 4)
|
#### Actions Tags (Priorité 4)
|
||||||
- [ ] Créer `actions/tags.ts` pour la gestion tags
|
- [x] Créer `actions/tags.ts` pour la gestion tags
|
||||||
- [ ] `createTag(name, color)` - Création tag
|
- [x] `createTag(name, color)` - Création tag
|
||||||
- [ ] `updateTag(tagId, data)` - Modification tag
|
- [x] `updateTag(tagId, data)` - Modification tag
|
||||||
- [ ] `deleteTag(tagId)` - Suppression tag
|
- [x] `deleteTag(tagId)` - Suppression tag
|
||||||
- [ ] Modifier les formulaires tags pour server actions
|
- [x] Modifier les formulaires tags pour server actions
|
||||||
- [ ] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
|
- [x] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
|
||||||
- [ ] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
|
- [x] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
|
||||||
- [ ] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
|
- [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
|
||||||
|
|
||||||
#### Migration progressive avec nettoyage immédiat
|
#### Migration progressive avec nettoyage immédiat
|
||||||
**Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes
|
**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 { HttpClient } from './base/http-client';
|
||||||
import { Tag, ApiResponse } from '@/lib/types';
|
import { Tag, ApiResponse } from '@/lib/types';
|
||||||
|
|
||||||
// Types pour les requêtes
|
// Types pour les requêtes (now only used for validation - CRUD operations moved to server actions)
|
||||||
export interface CreateTagData {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateTagData {
|
|
||||||
tagId: string;
|
|
||||||
name?: string;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagFilters {
|
export interface TagFilters {
|
||||||
q?: string; // Recherche par nom
|
q?: string; // Recherche par nom
|
||||||
@@ -89,27 +79,7 @@ export class TagsClient extends HttpClient {
|
|||||||
return this.get<TagResponse>(`/${id}`);
|
return this.get<TagResponse>(`/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// CRUD operations removed - now handled by server actions in /actions/tags.ts
|
||||||
* 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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valide le format d'une couleur hexadécimale
|
* 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[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!data.name || typeof data.name !== 'string') {
|
if (!data.name || typeof data.name !== 'string') {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useTransition } from 'react';
|
||||||
import { Tag } from '@/lib/types';
|
import { Tag } from '@/lib/types';
|
||||||
import { TagsClient } from '@/clients/tags-client';
|
import { TagsClient } from '@/clients/tags-client';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { createTag, updateTag } from '@/actions/tags';
|
||||||
|
|
||||||
interface TagFormProps {
|
interface TagFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
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
|
tag?: Tag | null; // Si fourni, mode édition
|
||||||
loading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRESET_COLORS = [
|
const PRESET_COLORS = [
|
||||||
@@ -30,7 +30,8 @@ const PRESET_COLORS = [
|
|||||||
'#F43F5E', // Rose
|
'#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({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
color: '#3B82F6',
|
color: '#3B82F6',
|
||||||
@@ -66,12 +67,42 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
startTransition(async () => {
|
||||||
await onSubmit(formData);
|
try {
|
||||||
onClose();
|
let result;
|
||||||
} catch (error) {
|
|
||||||
setErrors([error instanceof Error ? error.message : 'Erreur inconnue']);
|
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) => {
|
const handleColorSelect = (color: string) => {
|
||||||
@@ -102,7 +133,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
|||||||
}}
|
}}
|
||||||
placeholder="Nom du tag..."
|
placeholder="Nom du tag..."
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +181,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
|||||||
type="color"
|
type="color"
|
||||||
value={formData.color}
|
value={formData.color}
|
||||||
onChange={handleCustomColorChange}
|
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"
|
className="w-12 h-8 rounded border border-slate-600 bg-slate-800 cursor-pointer disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -163,7 +194,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
|||||||
}}
|
}}
|
||||||
placeholder="#RRGGBB"
|
placeholder="#RRGGBB"
|
||||||
maxLength={7}
|
maxLength={7}
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
className="w-24 text-xs font-mono"
|
className="w-24 text-xs font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +211,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.isPinned}
|
checked={formData.isPinned}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, isPinned: e.target.checked }))}
|
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"
|
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">
|
<span className="text-sm text-slate-300">
|
||||||
@@ -210,16 +241,16 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
150
hooks/useTags.ts
150
hooks/useTags.ts
@@ -1,21 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useTransition } from 'react';
|
||||||
import { tagsClient, TagFilters, CreateTagData, UpdateTagData, TagsClient } from '@/clients/tags-client';
|
import { tagsClient, TagFilters, TagsClient } from '@/clients/tags-client';
|
||||||
|
import { createTag, updateTag, deleteTag as deleteTagAction } from '@/actions/tags';
|
||||||
import { Tag } from '@/lib/types';
|
import { Tag } from '@/lib/types';
|
||||||
|
|
||||||
interface UseTagsState {
|
interface UseTagsState {
|
||||||
tags: Array<Tag & { usage: number }>;
|
tags: Array<Tag & { usage: number }>;
|
||||||
popularTags: Array<Tag & { usage: number }>;
|
popularTags: Array<Tag & { usage: number }>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
isPending: boolean; // Loading state for server actions
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseTagsActions {
|
interface UseTagsActions {
|
||||||
refreshTags: () => Promise<void>;
|
refreshTags: () => Promise<void>;
|
||||||
searchTags: (query: string, limit?: number) => Promise<Tag[]>;
|
searchTags: (query: string, limit?: number) => Promise<Tag[]>;
|
||||||
createTag: (data: CreateTagData) => Promise<Tag | null>;
|
createTag: (name: string, color: string) => Promise<void>;
|
||||||
updateTag: (data: UpdateTagData) => Promise<Tag | null>;
|
updateTag: (tagId: string, data: { name?: string; color?: string; isPinned?: boolean }) => Promise<void>;
|
||||||
deleteTag: (tagId: string) => Promise<void>;
|
deleteTag: (tagId: string) => Promise<void>;
|
||||||
getPopularTags: (limit?: number) => Promise<void>;
|
getPopularTags: (limit?: number) => Promise<void>;
|
||||||
setFilters: (filters: TagFilters) => void;
|
setFilters: (filters: TagFilters) => void;
|
||||||
@@ -28,10 +30,12 @@ export function useTags(
|
|||||||
initialData?: (Tag & { usage: number })[],
|
initialData?: (Tag & { usage: number })[],
|
||||||
initialFilters?: TagFilters
|
initialFilters?: TagFilters
|
||||||
): UseTagsState & UseTagsActions {
|
): UseTagsState & UseTagsActions {
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
const [state, setState] = useState<UseTagsState>({
|
const [state, setState] = useState<UseTagsState>({
|
||||||
tags: initialData || [],
|
tags: initialData || [],
|
||||||
popularTags: initialData || [],
|
popularTags: initialData || [],
|
||||||
loading: !initialData,
|
loading: !initialData,
|
||||||
|
isPending: false,
|
||||||
error: null
|
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> => {
|
const createTagAction = useCallback(async (name: string, color: string): Promise<void> => {
|
||||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
// Validation côté client
|
||||||
|
const errors = TagsClient.validateTagData({ name, color });
|
||||||
try {
|
if (errors.length > 0) {
|
||||||
// Validation côté client
|
setState(prev => ({ ...prev, error: errors[0] }));
|
||||||
const errors = TagsClient.validateTagData(data);
|
return;
|
||||||
if (errors.length > 0) {
|
}
|
||||||
throw new Error(errors[0]);
|
|
||||||
|
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> => {
|
const updateTagAction = useCallback(async (
|
||||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
tagId: string,
|
||||||
|
data: { name?: string; color?: string; isPinned?: boolean }
|
||||||
try {
|
): Promise<void> => {
|
||||||
const response = await tagsClient.updateTag(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
// Rafraîchir la liste après mise à jour
|
setState(prev => ({ ...prev, error: null }));
|
||||||
await refreshTags();
|
const result = await updateTag(tagId, data);
|
||||||
|
|
||||||
return response.data;
|
if (!result.success) {
|
||||||
} catch (error) {
|
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la mise à jour' }));
|
||||||
setState(prev => ({
|
}
|
||||||
...prev,
|
// Note: revalidatePath in server action handles cache refresh automatically
|
||||||
loading: false,
|
} catch (error) {
|
||||||
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
|
setState(prev => ({
|
||||||
}));
|
...prev,
|
||||||
return null;
|
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
|
||||||
}
|
}));
|
||||||
}, [refreshTags]);
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supprime un tag
|
* Supprime un tag avec server action
|
||||||
*/
|
*/
|
||||||
const deleteTag = useCallback(async (tagId: string): Promise<void> => {
|
const deleteTagActionHandler = useCallback(async (tagId: string): Promise<void> => {
|
||||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
try {
|
setState(prev => ({ ...prev, error: null }));
|
||||||
await tagsClient.deleteTag(tagId);
|
const result = await deleteTagAction(tagId);
|
||||||
|
|
||||||
// Rafraîchir la liste après suppression
|
if (!result.success) {
|
||||||
await refreshTags();
|
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la suppression' }));
|
||||||
} catch (error) {
|
throw new Error(result.error);
|
||||||
setState(prev => ({
|
}
|
||||||
...prev,
|
// Note: revalidatePath in server action handles cache refresh automatically
|
||||||
loading: false,
|
} catch (error) {
|
||||||
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
|
setState(prev => ({
|
||||||
}));
|
...prev,
|
||||||
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
|
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
|
||||||
}
|
}));
|
||||||
}, [refreshTags]);
|
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)
|
// Charger les tags au montage et quand les filtres changent (seulement si pas de données initiales)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -178,11 +187,12 @@ export function useTags(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
isPending, // Expose loading state from useTransition
|
||||||
refreshTags,
|
refreshTags,
|
||||||
searchTags,
|
searchTags,
|
||||||
createTag,
|
createTag: createTagAction,
|
||||||
updateTag,
|
updateTag: updateTagAction,
|
||||||
deleteTag,
|
deleteTag: deleteTagActionHandler,
|
||||||
getPopularTags,
|
getPopularTags,
|
||||||
setFilters
|
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 and DELETE routes removed - now handled by server actions in /actions/tags.ts
|
||||||
* 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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,60 +41,4 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// POST route removed - now handled by server actions in /actions/tags.ts
|
||||||
* 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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useState, useMemo } from 'react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tag } from '@/lib/types';
|
import { Tag } from '@/lib/types';
|
||||||
import { useTags } from '@/hooks/useTags';
|
import { useTags } from '@/hooks/useTags';
|
||||||
import { CreateTagData } from '@/clients/tags-client';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { TagForm } from '@/components/forms/TagForm';
|
import { TagForm } from '@/components/forms/TagForm';
|
||||||
@@ -19,8 +18,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
|||||||
tags,
|
tags,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
createTag,
|
refreshTags,
|
||||||
updateTag,
|
|
||||||
deleteTag
|
deleteTag
|
||||||
} = useTags(initialTags as (Tag & { usage: number })[]);
|
} = useTags(initialTags as (Tag & { usage: number })[]);
|
||||||
|
|
||||||
@@ -50,29 +48,21 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
|||||||
});
|
});
|
||||||
}, [tags, searchQuery]);
|
}, [tags, searchQuery]);
|
||||||
|
|
||||||
const handleCreateTag = async (data: CreateTagData) => {
|
|
||||||
await createTag(data);
|
|
||||||
setIsCreateModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditTag = (tag: Tag) => {
|
const handleEditTag = (tag: Tag) => {
|
||||||
setEditingTag(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) => {
|
const handleDeleteTag = async (tag: Tag) => {
|
||||||
|
if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setDeletingTagId(tag.id);
|
setDeletingTagId(tag.id);
|
||||||
try {
|
try {
|
||||||
|
// Utiliser la server action directement depuis useTags
|
||||||
await deleteTag(tag.id);
|
await deleteTag(tag.id);
|
||||||
|
// Refresh la liste des tags
|
||||||
|
await refreshTags();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la suppression:', error);
|
console.error('Erreur lors de la suppression:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -220,16 +210,14 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
|||||||
<TagForm
|
<TagForm
|
||||||
isOpen={isCreateModalOpen}
|
isOpen={isCreateModalOpen}
|
||||||
onClose={() => setIsCreateModalOpen(false)}
|
onClose={() => setIsCreateModalOpen(false)}
|
||||||
onSubmit={handleCreateTag}
|
onSuccess={refreshTags}
|
||||||
loading={false}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TagForm
|
<TagForm
|
||||||
isOpen={!!editingTag}
|
isOpen={!!editingTag}
|
||||||
onClose={() => setEditingTag(null)}
|
onClose={() => setEditingTag(null)}
|
||||||
onSubmit={handleUpdateTag}
|
onSuccess={refreshTags}
|
||||||
tag={editingTag}
|
tag={editingTag}
|
||||||
loading={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user