feat: refactor user preferences management

- Marked all user preferences actions as complete in TODO.md.
- Updated `user-preferences-client.ts` to remove outdated mutation methods, now handled by server actions.
- Deleted unused API routes for column visibility, kanban filters, and view preferences.
- Refactored `UserPreferencesContext.tsx` to utilize server actions for updates, improving performance with `useTransition`.
This commit is contained in:
Julien Froidefond
2025-09-18 13:10:04 +02:00
parent cece09d150
commit aeb4e17939
7 changed files with 347 additions and 270 deletions

18
TODO.md
View File

@@ -187,15 +187,15 @@
- [ ] **Nettoyage** : Modifier composants Daily pour `useTransition` - [ ] **Nettoyage** : Modifier composants Daily pour `useTransition`
#### Actions User Preferences (Priorité 3) #### Actions User Preferences (Priorité 3)
- [ ] Créer `actions/preferences.ts` pour les toggles - [x] Créer `actions/preferences.ts` pour les toggles
- [ ] `updateViewPreferences(preferences)` - Préférences d'affichage - [x] `updateViewPreferences(preferences)` - Préférences d'affichage
- [ ] `updateKanbanFilters(filters)` - Filtres Kanban - [x] `updateKanbanFilters(filters)` - Filtres Kanban
- [ ] `updateColumnVisibility(columns)` - Visibilité colonnes - [x] `updateColumnVisibility(columns)` - Visibilité colonnes
- [ ] `updateTheme(theme)` - Changement de thème - [x] `updateTheme(theme)` - Changement de thème
- [ ] Remplacer les hooks par server actions directes - [x] Remplacer les hooks par server actions directes
- [ ] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH) - [x] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH)
- [ ] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement) - [x] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement)
- [ ] **Nettoyage** : Modifier `useUserPreferences.ts` 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 - [ ] Créer `actions/tags.ts` pour la gestion tags

View File

@@ -1,5 +1,5 @@
import { httpClient } from './base/http-client'; import { httpClient } from './base/http-client';
import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility } from '@/lib/types'; import { UserPreferences } from '@/lib/types';
export interface UserPreferencesResponse { export interface UserPreferencesResponse {
success: boolean; success: boolean;
@@ -8,14 +8,9 @@ export interface UserPreferencesResponse {
error?: string; error?: string;
} }
export interface UserPreferencesUpdateResponse {
success: boolean;
message?: string;
error?: string;
}
/** /**
* Client HTTP pour les préférences utilisateur * Client HTTP pour les préférences utilisateur (lecture seule)
* Les mutations sont gérées par les server actions dans actions/preferences.ts
*/ */
export const userPreferencesClient = { export const userPreferencesClient = {
/** /**
@@ -29,49 +24,5 @@ export const userPreferencesClient = {
} }
return response.data; return response.data;
},
/**
* Sauvegarde toutes les préférences utilisateur
*/
async savePreferences(preferences: UserPreferences): Promise<void> {
const response = await httpClient.put<UserPreferencesUpdateResponse>('/user-preferences', preferences);
if (!response.success) {
throw new Error(response.error || 'Erreur lors de la sauvegarde des préférences');
}
},
/**
* Met à jour les filtres Kanban
*/
async updateKanbanFilters(filters: Partial<KanbanFilters>): Promise<void> {
const response = await httpClient.patch<UserPreferencesUpdateResponse>('/user-preferences/kanban-filters', filters);
if (!response.success) {
throw new Error(response.error || 'Erreur lors de la mise à jour des filtres Kanban');
}
},
/**
* Met à jour les préférences de vue
*/
async updateViewPreferences(preferences: Partial<ViewPreferences>): Promise<void> {
const response = await httpClient.patch<UserPreferencesUpdateResponse>('/user-preferences/view-preferences', preferences);
if (!response.success) {
throw new Error(response.error || 'Erreur lors de la mise à jour des préférences de vue');
}
},
/**
* Met à jour la visibilité des colonnes
*/
async updateColumnVisibility(visibility: Partial<ColumnVisibility>): Promise<void> {
const response = await httpClient.patch<UserPreferencesUpdateResponse>('/user-preferences/column-visibility', visibility);
if (!response.success) {
throw new Error(response.error || 'Erreur lors de la mise à jour de la visibilité des colonnes');
}
} }
}; };

218
src/actions/preferences.ts Normal file
View File

@@ -0,0 +1,218 @@
'use server';
import { userPreferencesService } from '@/services/user-preferences';
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import { revalidatePath } from 'next/cache';
/**
* Met à jour les préférences de vue
*/
export async function updateViewPreferences(updates: Partial<ViewPreferences>): Promise<{
success: boolean;
error?: string;
}> {
try {
await userPreferencesService.updateViewPreferences(updates);
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur updateViewPreferences:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Met à jour les filtres Kanban
*/
export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Promise<{
success: boolean;
error?: string;
}> {
try {
await userPreferencesService.updateKanbanFilters(updates);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur updateKanbanFilters:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Met à jour la visibilité des colonnes
*/
export async function updateColumnVisibility(updates: Partial<ColumnVisibility>): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const newColumnVisibility: ColumnVisibility = {
...preferences.columnVisibility,
...updates
};
await userPreferencesService.saveColumnVisibility(newColumnVisibility);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur updateColumnVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle la visibilité des objectifs
*/
export async function toggleObjectivesVisibility(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const showObjectives = !preferences.viewPreferences.showObjectives;
await userPreferencesService.updateViewPreferences({ showObjectives });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleObjectivesVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle le mode collapse des objectifs
*/
export async function toggleObjectivesCollapse(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const collapseObjectives = !preferences.viewPreferences.collapseObjectives;
await userPreferencesService.updateViewPreferences({ collapseObjectives });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleObjectivesCollapse:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Change le thème (light/dark)
*/
export async function setTheme(theme: 'light' | 'dark'): Promise<{
success: boolean;
error?: string;
}> {
try {
await userPreferencesService.updateViewPreferences({ theme });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur setTheme:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle le thème entre light et dark
*/
export async function toggleTheme(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
await userPreferencesService.updateViewPreferences({ theme: newTheme });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleTheme:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle la taille de police
*/
export async function toggleFontSize(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large'];
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize);
const nextIndex = (currentIndex + 1) % fontSizes.length;
const newFontSize = fontSizes[nextIndex];
await userPreferencesService.updateViewPreferences({ fontSize: newFontSize });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleFontSize:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle la visibilité d'une colonne Kanban
*/
export async function toggleColumnVisibility(status: TaskStatus): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses);
if (hiddenStatuses.has(status)) {
hiddenStatuses.delete(status);
} else {
hiddenStatuses.add(status);
}
await userPreferencesService.saveColumnVisibility({
hiddenStatuses: Array.from(hiddenStatuses)
});
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur toggleColumnVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}

View File

@@ -1,28 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* PATCH /api/user-preferences/column-visibility - Met à jour partiellement la visibilité des colonnes
*/
export async function PATCH(request: NextRequest) {
try {
const updates = await request.json();
const current = await userPreferencesService.getColumnVisibility();
await userPreferencesService.saveColumnVisibility({ ...current, ...updates });
return NextResponse.json({
success: true,
message: 'Visibilité des colonnes mise à jour avec succès'
});
} catch (error) {
console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour de la visibilité des colonnes'
},
{ status: 500 }
);
}
}

View File

@@ -1,76 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* GET /api/user-preferences/kanban-filters - Récupère les filtres Kanban
*/
export async function GET() {
try {
const filters = await userPreferencesService.getKanbanFilters();
return NextResponse.json({
success: true,
data: filters
});
} catch (error) {
console.error('Erreur lors de la récupération des filtres Kanban:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la récupération des filtres Kanban'
},
{ status: 500 }
);
}
}
/**
* PUT /api/user-preferences/kanban-filters - Met à jour les filtres Kanban
*/
export async function PUT(request: NextRequest) {
try {
const filters = await request.json();
await userPreferencesService.saveKanbanFilters(filters);
return NextResponse.json({
success: true,
message: 'Filtres Kanban sauvegardés avec succès'
});
} catch (error) {
console.error('Erreur lors de la sauvegarde des filtres Kanban:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la sauvegarde des filtres Kanban'
},
{ status: 500 }
);
}
}
/**
* PATCH /api/user-preferences/kanban-filters - Met à jour partiellement les filtres Kanban
*/
export async function PATCH(request: NextRequest) {
try {
const updates = await request.json();
const current = await userPreferencesService.getKanbanFilters();
await userPreferencesService.saveKanbanFilters({ ...current, ...updates });
return NextResponse.json({
success: true,
message: 'Filtres Kanban mis à jour avec succès'
});
} catch (error) {
console.error('Erreur lors de la mise à jour des filtres Kanban:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour des filtres Kanban'
},
{ status: 500 }
);
}
}

View File

@@ -1,28 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* PATCH /api/user-preferences/view-preferences - Met à jour partiellement les préférences de vue
*/
export async function PATCH(request: NextRequest) {
try {
const updates = await request.json();
const current = await userPreferencesService.getViewPreferences();
await userPreferencesService.saveViewPreferences({ ...current, ...updates });
return NextResponse.json({
success: true,
message: 'Préférences de vue mises à jour avec succès'
});
} catch (error) {
console.error('Erreur lors de la mise à jour des préférences de vue:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour des préférences de vue'
},
{ status: 500 }
);
}
}

View File

@@ -1,26 +1,37 @@
'use client'; 'use client';
import { createContext, useContext, ReactNode, useState, useCallback, useEffect } from 'react'; import { createContext, useContext, ReactNode, useState, useCallback, useEffect, useTransition } from 'react';
import { userPreferencesClient } from '@/clients/user-preferences-client';
import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import {
updateViewPreferences as updateViewPreferencesAction,
updateKanbanFilters as updateKanbanFiltersAction,
updateColumnVisibility as updateColumnVisibilityAction,
toggleObjectivesVisibility as toggleObjectivesVisibilityAction,
toggleObjectivesCollapse as toggleObjectivesCollapseAction,
toggleTheme as toggleThemeAction,
setTheme as setThemeAction,
toggleFontSize as toggleFontSizeAction,
toggleColumnVisibility as toggleColumnVisibilityAction
} from '@/actions/preferences';
interface UserPreferencesContextType { interface UserPreferencesContextType {
preferences: UserPreferences; preferences: UserPreferences;
isPending: boolean;
// Kanban Filters // Kanban Filters
updateKanbanFilters: (updates: Partial<KanbanFilters>) => Promise<void>; updateKanbanFilters: (updates: Partial<KanbanFilters>) => void;
// View Preferences // View Preferences
updateViewPreferences: (updates: Partial<ViewPreferences>) => Promise<void>; updateViewPreferences: (updates: Partial<ViewPreferences>) => void;
toggleObjectivesVisibility: () => Promise<void>; toggleObjectivesVisibility: () => void;
toggleObjectivesCollapse: () => Promise<void>; toggleObjectivesCollapse: () => void;
toggleTheme: () => Promise<void>; toggleTheme: () => void;
setTheme: (theme: 'light' | 'dark') => Promise<void>; setTheme: (theme: 'light' | 'dark') => void;
toggleFontSize: () => Promise<void>; toggleFontSize: () => void;
// Column Visibility // Column Visibility
updateColumnVisibility: (updates: Partial<ColumnVisibility>) => Promise<void>; updateColumnVisibility: (updates: Partial<ColumnVisibility>) => void;
toggleColumnVisibility: (status: TaskStatus) => Promise<void>; toggleColumnVisibility: (status: TaskStatus) => void;
isColumnVisible: (status: TaskStatus) => boolean; isColumnVisible: (status: TaskStatus) => boolean;
} }
@@ -33,6 +44,7 @@ interface UserPreferencesProviderProps {
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) { export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences); const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences);
const [isPending, startTransition] = useTransition();
// Synchroniser le thème avec le ThemeProvider global (si disponible) // Synchroniser le thème avec le ThemeProvider global (si disponible)
useEffect(() => { useEffect(() => {
@@ -44,87 +56,114 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
// === KANBAN FILTERS === // === KANBAN FILTERS ===
const updateKanbanFilters = useCallback(async (updates: Partial<KanbanFilters>) => { const updateKanbanFilters = useCallback((updates: Partial<KanbanFilters>) => {
const newFilters = { ...preferences.kanbanFilters, ...updates }; startTransition(async () => {
setPreferences(prev => ({ ...prev, kanbanFilters: newFilters })); const originalFilters = preferences.kanbanFilters;
// Optimistic update
setPreferences(prev => ({
...prev,
kanbanFilters: { ...prev.kanbanFilters, ...updates }
}));
try { try {
await userPreferencesClient.updateKanbanFilters(updates); const result = await updateKanbanFiltersAction(updates);
} catch (error) { if (!result.success) {
console.warn('Erreur lors de la sauvegarde des filtres Kanban:', error); console.error('Erreur server action:', result.error);
// Revert optimistic update on error setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters }));
setPreferences(prev => ({ ...prev, kanbanFilters: preferences.kanbanFilters }));
} }
} catch (error) {
console.error('Erreur lors de la mise à jour des filtres Kanban:', error);
setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters }));
}
});
}, [preferences.kanbanFilters]); }, [preferences.kanbanFilters]);
// === VIEW PREFERENCES === // === VIEW PREFERENCES ===
const updateViewPreferences = useCallback(async (updates: Partial<ViewPreferences>) => { const updateViewPreferences = useCallback((updates: Partial<ViewPreferences>) => {
const newPreferences = { ...preferences.viewPreferences, ...updates }; startTransition(async () => {
setPreferences(prev => ({ ...prev, viewPreferences: newPreferences })); const originalPreferences = preferences.viewPreferences;
// Optimistic update
setPreferences(prev => ({
...prev,
viewPreferences: { ...prev.viewPreferences, ...updates }
}));
try { try {
await userPreferencesClient.updateViewPreferences(updates); const result = await updateViewPreferencesAction(updates);
} catch (error) { if (!result.success) {
console.warn('Erreur lors de la sauvegarde des préférences de vue:', error); console.error('Erreur server action:', result.error);
// Revert optimistic update on error setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences }));
setPreferences(prev => ({ ...prev, viewPreferences: preferences.viewPreferences }));
} }
} catch (error) {
console.error('Erreur lors de la mise à jour des préférences de vue:', error);
setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences }));
}
});
}, [preferences.viewPreferences]); }, [preferences.viewPreferences]);
const toggleObjectivesVisibility = useCallback(async () => { const toggleObjectivesVisibility = useCallback(() => {
const newValue = !preferences.viewPreferences.showObjectives; startTransition(async () => {
await updateViewPreferences({ showObjectives: newValue }); await toggleObjectivesVisibilityAction();
}, [preferences.viewPreferences.showObjectives, updateViewPreferences]); });
}, []);
const toggleObjectivesCollapse = useCallback(async () => { const toggleObjectivesCollapse = useCallback(() => {
const newValue = !preferences.viewPreferences.objectivesCollapsed; startTransition(async () => {
await updateViewPreferences({ objectivesCollapsed: newValue }); await toggleObjectivesCollapseAction();
}, [preferences.viewPreferences.objectivesCollapsed, updateViewPreferences]); });
}, []);
const toggleTheme = useCallback(async () => { const toggleTheme = useCallback(() => {
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; startTransition(async () => {
await updateViewPreferences({ theme: newTheme }); await toggleThemeAction();
}, [preferences.viewPreferences.theme, updateViewPreferences]); });
}, []);
const setTheme = useCallback(async (theme: 'light' | 'dark') => { const setTheme = useCallback((theme: 'light' | 'dark') => {
await updateViewPreferences({ theme }); startTransition(async () => {
}, [updateViewPreferences]); await setThemeAction(theme);
});
}, []);
const toggleFontSize = useCallback(async () => { const toggleFontSize = useCallback(() => {
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; startTransition(async () => {
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); await toggleFontSizeAction();
const nextIndex = (currentIndex + 1) % fontSizes.length; });
const newFontSize = fontSizes[nextIndex]; }, []);
await updateViewPreferences({ fontSize: newFontSize });
}, [preferences.viewPreferences.fontSize, updateViewPreferences]);
// === COLUMN VISIBILITY === // === COLUMN VISIBILITY ===
const updateColumnVisibility = useCallback(async (updates: Partial<ColumnVisibility>) => { const updateColumnVisibility = useCallback((updates: Partial<ColumnVisibility>) => {
const newVisibility = { ...preferences.columnVisibility, ...updates }; startTransition(async () => {
setPreferences(prev => ({ ...prev, columnVisibility: newVisibility })); const originalVisibility = preferences.columnVisibility;
// Optimistic update
setPreferences(prev => ({
...prev,
columnVisibility: { ...prev.columnVisibility, ...updates }
}));
try { try {
await userPreferencesClient.updateColumnVisibility(updates); const result = await updateColumnVisibilityAction(updates);
} catch (error) { if (!result.success) {
console.warn('Erreur lors de la sauvegarde de la visibilité des colonnes:', error); console.error('Erreur server action:', result.error);
// Revert optimistic update on error setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility }));
setPreferences(prev => ({ ...prev, columnVisibility: preferences.columnVisibility }));
} }
} catch (error) {
console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error);
setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility }));
}
});
}, [preferences.columnVisibility]); }, [preferences.columnVisibility]);
const toggleColumnVisibility = useCallback(async (status: TaskStatus) => { const toggleColumnVisibility = useCallback((status: TaskStatus) => {
const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses); startTransition(async () => {
await toggleColumnVisibilityAction(status);
if (hiddenStatuses.has(status)) { });
hiddenStatuses.delete(status); }, []);
} else {
hiddenStatuses.add(status);
}
await updateColumnVisibility({ hiddenStatuses: Array.from(hiddenStatuses) });
}, [preferences.columnVisibility.hiddenStatuses, updateColumnVisibility]);
const isColumnVisible = useCallback((status: TaskStatus) => { const isColumnVisible = useCallback((status: TaskStatus) => {
return !preferences.columnVisibility.hiddenStatuses.includes(status); return !preferences.columnVisibility.hiddenStatuses.includes(status);
@@ -132,6 +171,7 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
const contextValue: UserPreferencesContextType = { const contextValue: UserPreferencesContextType = {
preferences, preferences,
isPending,
updateKanbanFilters, updateKanbanFilters,
updateViewPreferences, updateViewPreferences,
toggleObjectivesVisibility, toggleObjectivesVisibility,