refactor(simulation-ee): Sprint 3.5 clean — FTD-17/18/19 résolus, factorisation SimulationForm
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
385b29679e
commit
18f92098cb
11 changed files with 36 additions and 66 deletions
|
|
@ -179,46 +179,7 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s
|
|||
|
||||
---
|
||||
|
||||
### FTD-17 — Clé `['plan']` dupliquée entre features
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert — accepté par design (Sprint 3)
|
||||
**Estimation de session :** 30 min
|
||||
**Description :** La queryKey TanStack Query `['plan']` est écrite littéralement dans deux endroits : `features/dashboard/hooks/usePlan.ts` (via `PLAN_QUERY_KEY`) et `features/simulations/pages/SimulationPage.tsx` (inline). TanStack Query partage bien le cache par clé identique, donc la déduplication fonctionne en pratique. Mais toute faute de frappe dans l'une des occurrences briserait silencieusement la mise en cache partagée.
|
||||
|
||||
**À faire :**
|
||||
- Déplacer `PLAN_QUERY_KEY` vers `src/entities/user/api.ts` (ou un nouveau `src/entities/user/query-keys.ts`)
|
||||
- Importer `PLAN_QUERY_KEY` dans `usePlan.ts` et `SimulationPage.tsx` depuis ce fichier partagé
|
||||
- Supprimer la constante locale dans `usePlan.ts`
|
||||
|
||||
**Condition de résolution :** avant l'ajout d'une troisième feature qui consomme `['plan']` (T2 Live page, rapport page).
|
||||
|
||||
---
|
||||
|
||||
### FTD-18 — SimulationForm utilise encore shadcn Button
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert
|
||||
**Estimation de session :** 30 min
|
||||
**Description :** `src/features/simulations/components/SimulationForm.tsx` importe `Button` depuis `@/shared/components/ui/button` (shadcn). Depuis Sprint 0.5 bis, la primitive canonique est `@/shared/ui/Button`. SimulationForm a été volontairement exclu de l'étape D1 pour respecter la règle max 3 fichiers par étape.
|
||||
|
||||
**À faire :**
|
||||
- Remplacer l'import shadcn par `@/shared/ui/Button`
|
||||
- Adapter `variant` si nécessaire (`"outline"` → `"secondary"`)
|
||||
|
||||
**Condition de résolution :** session dédiée — à grouper avec d'autres migrations shadcn → shared/ui si elles surviennent.
|
||||
|
||||
---
|
||||
|
||||
### FTD-19 — Token `--shadow-focus` absent de `src/index.css`
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert
|
||||
**Estimation de session :** 15 min
|
||||
**Description :** `DESIGN_SYSTEM.md` spécifie un token `--shadow-focus` pour les états focus des composants interactifs. Ce token n'a jamais été ajouté à `src/index.css`. En attendant, le focus est rendu via `ring-2 ring-expria/20` (token Direction H valide), utilisé dans `Button`, `Card`, `MobileHeader`. Le rendu visuel est correct mais ne correspond pas au token documenté.
|
||||
|
||||
**À faire :**
|
||||
- Ajouter `--shadow-focus` dans `@theme {}` de `src/index.css` (valeur à aligner avec `DESIGN_SYSTEM.md`)
|
||||
- Remplacer `ring-2 ring-expria/20` par `shadow-focus` dans les composants concernés
|
||||
|
||||
**Condition de résolution :** session design system dédiée (peut être groupée avec FTD-15 ou une refonte de tokens).
|
||||
> FTD-17, FTD-18, FTD-19 résolus au Sprint 3.5 (2026-04-22) — voir §5 Historique des résolutions.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -366,6 +327,9 @@ Frontend :
|
|||
| FTD-13 | Incompatibilité Vitest 3 / Vite 8 (conflit de types `Plugin<any>` entre le Vite 8 top-level avec Rolldown et le Vite 7 pinné de Vitest 3.2.4 ; `npm run build` cassé) | 2026-04-17 | Résolu par upgrade Vitest `3.2.4 → 4.1.4` (et `@vitest/coverage-v8` idem) à l'étape 12-bis du Sprint 0. Vitest 4.x supporte nativement Vite 8 Rolldown. Correctif complémentaire : script `typecheck` passé de `tsc --noEmit -p tsconfig.app.json` à `tsc -b --noEmit` pour couvrir aussi `tsconfig.node.json` (d'où `vite.config.ts`) et éviter qu'un bug similaire échappe à la CI. |
|
||||
| FTD-16 | `VITE_MAINTENANCE_MODE` non lu dans le code — la variable d'env était dans `env.ts` mais jamais consommée | 2026-04-18 | Résolu au Sprint 1 étape 6. Ajout de `isMaintenanceMode` dans `src/shared/config/env.ts` et garde dans `src/app/main.tsx` : `isMaintenanceMode ? <MaintenancePage /> : <Providers />`. `MaintenancePage` est statique (aucun provider requis), tokens Direction H exclusivement. |
|
||||
| FTD-22 | Code orphelin suite à la refonte UX `/sujets` (2026-04-21) — composant `SujetSelector` et helper `selectSujet` plus référencés après bascule dropdown → page dédiée | 2026-04-21 | Résolu **partiellement**. Supprimé : `src/features/simulations/components/SujetSelector.tsx` + helper `selectSujet` de `useSimulation.ts` (les tests `useSimulation.test.tsx` adaptés en utilisant `changeSubject` + `setStep('task-selected')` via `useSimulationFlow`). **Conservés intentionnellement** : le step `'choosing-subject'` (utilisé par `SimulationFlowProvider.selectTask` pour les tâches avec catalogue et par `SimulationPage` pour naviguer vers `/sujets`) et le helper `goToSubjectPicker` (bouton "Changer de sujet" dans `SimulationForm`). |
|
||||
| FTD-17 | Clé `['plan']` dupliquée entre features (`usePlan`, `SimulationPage`, `RapportPage`) | 2026-04-22 | Résolu au Sprint 3.5. Création de `src/entities/user/query-keys.ts` (constantes pures, aucun import runtime) exportant `PLAN_QUERY_KEY = ['plan'] as const`. `features/dashboard/hooks/usePlan.ts` l'importe et le re-exporte pour conserver la rétro-compatibilité de l'import `PLAN_QUERY_KEY`. `SimulationPage.tsx` et `RapportPage.tsx` remplacent leur `useQuery` inline par le hook `usePlan()` — dédup totale de la clé et de la config staleTime. |
|
||||
| FTD-18 | SimulationForm utilise encore le shadcn Button au lieu de la primitive `@/shared/ui/Button` | 2026-04-22 | Résolu au Sprint 3.5. Remplacement de l'import `@/shared/components/ui/button` par `@/shared/ui/Button` dans `SimulationForm.tsx`. Aucun variant à adapter (usage du Button sans prop `variant` → `primary` par défaut dans les deux implémentations). Les 7 autres consommateurs shadcn (`Login/RegisterPage`, `PaywallBanner`, `DesignSystemPage`, `ThemeToggle`, `dialog.tsx`) restent hors scope de cette FTD. |
|
||||
| FTD-19 | Token `--shadow-focus` absent de `src/index.css` | 2026-04-22 | Résolu au Sprint 3.5. Ajout de `--shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18)` dans `@theme {}` (valeur conforme à `DESIGN_SYSTEM.md §2`) et `--shadow-focus: 0 0 0 3px rgba(91, 127, 255, 0.32)` dans `.dark {}` (recalculé sur la teinte expria dark `#5B7FFF`). Tailwind 4 génère automatiquement l'utility `shadow-focus`. Migration de 5 occurrences `ring-2 ring-expria/20` → `shadow-focus` dans `Button.tsx`, `Card.tsx`, `SimulationForm.tsx` (×3), `SpecialCharsKeyboard.tsx`. Factorisation bonus : className dupliquée des boutons secondaires de `SimulationForm` extraite en const `secondaryActionBtn`. |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -384,3 +348,4 @@ Frontend :
|
|||
| 1.8 | 2026-04-20 | Ajout FTD-21 🔴 (persistance session simulation — prod + sujet perdus au refresh, session dédiée après G1-G5) |
|
||||
| 1.9 | 2026-04-21 | FTD-22 résolu partiellement (nettoyage code orphelin refonte `/sujets` — `SujetSelector` + `selectSujet` supprimés ; `choosing-subject` + `goToSubjectPicker` conservés) |
|
||||
| 1.10 | 2026-04-21 | FTD-21 résolu partiellement pour `/simulation/ee` (autosave 30 s + `beforeunload` + reprise via `localStorage` + `PATCH /:id/contenu` + `PATCH /:id/sujet` + `getById` tolère `rapport=null`) ; EO + examen restent ouverts |
|
||||
| 1.11 | 2026-04-22 | Sprint 3.5 Clean — FTD-17, FTD-18, FTD-19 résolus. 15 FTD actives restantes (cap de 15 respecté) |
|
||||
|
|
|
|||
13
src/entities/user/query-keys.ts
Normal file
13
src/entities/user/query-keys.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Clés TanStack Query partagées pour le domaine `user`.
|
||||
*
|
||||
* Source unique — importée par `features/dashboard/hooks/usePlan`,
|
||||
* `features/simulations/pages/SimulationPage`, `features/simulations/pages/RapportPage`,
|
||||
* et tout futur consommateur du statut de plan. Une clé locale inline briserait
|
||||
* silencieusement le cache partagé de TanStack Query à la moindre faute de frappe.
|
||||
*
|
||||
* Ce module ne contient que des constantes pures — aucun import React, TanStack
|
||||
* ou autre dépendance runtime.
|
||||
*/
|
||||
|
||||
export const PLAN_QUERY_KEY = ['plan'] as const
|
||||
|
|
@ -12,8 +12,9 @@
|
|||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getPlanStatus } from '@/entities/user/api'
|
||||
import { PLAN_QUERY_KEY } from '@/entities/user/query-keys'
|
||||
|
||||
export const PLAN_QUERY_KEY = ['plan'] as const
|
||||
export { PLAN_QUERY_KEY }
|
||||
|
||||
export function usePlan() {
|
||||
return useQuery({
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import { useEffect, useRef, useState, type FormEvent } from 'react'
|
||||
import { Clock, Lightbulb, Loader2, Shuffle } from 'lucide-react'
|
||||
import { z } from 'zod'
|
||||
import { Button } from '@/shared/components/ui/button'
|
||||
import { Button } from '@/shared/ui/Button'
|
||||
import { formatTache } from '@/entities/production/lib'
|
||||
import { hasAccess, type Plan } from '@/entities/user/lib'
|
||||
import type { SujetData, Tache } from '@/entities/production/types'
|
||||
|
|
@ -32,6 +32,9 @@ import { IdeesSuggestions } from './IdeesSuggestions'
|
|||
const MIN_WORDS_IDEES = 30
|
||||
const LS_SIMULATION_ID_KEY = 'expria_simulation_id'
|
||||
|
||||
const secondaryActionBtn =
|
||||
'inline-flex items-center gap-1.5 rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-2 transition-colors hover:border-expria hover:text-expria focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
||||
const textSchema = z.object({
|
||||
texte: z
|
||||
.string()
|
||||
|
|
@ -212,7 +215,7 @@ export function SimulationForm({
|
|||
onClick={handleIdeesClick}
|
||||
disabled={ideesDisabled}
|
||||
title={ideesTitle}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-2 transition-colors hover:border-expria hover:text-expria focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className={secondaryActionBtn}
|
||||
aria-label="Obtenir des suggestions d'idées"
|
||||
>
|
||||
<Lightbulb className="size-4" aria-hidden="true" />
|
||||
|
|
@ -222,7 +225,7 @@ export function SimulationForm({
|
|||
type="button"
|
||||
onClick={onChangeSujet}
|
||||
disabled={isSubmitting}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-2 transition-colors hover:border-expria hover:text-expria focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className={secondaryActionBtn}
|
||||
aria-label="Changer de sujet"
|
||||
>
|
||||
<Shuffle className="size-4" aria-hidden="true" />
|
||||
|
|
@ -302,7 +305,7 @@ export function SimulationForm({
|
|||
placeholder="Rédigez votre texte ici…"
|
||||
aria-invalid={!!fieldError}
|
||||
aria-describedby={fieldError ? 'texte-error' : undefined}
|
||||
className="w-full resize-none overflow-y-hidden rounded-md border border-line bg-surface p-3 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="w-full resize-none overflow-y-hidden rounded-md border border-line bg-surface p-3 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<WordCountBar count={wordCount} config={config} />
|
||||
{autosave.savedAt && !fieldError && (
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function SpecialCharsKeyboard({ onInsert, disabled = false }: Props) {
|
|||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onInsert(char)}
|
||||
aria-label={`Insérer le caractère ${char}`}
|
||||
className="size-8 shrink-0 rounded-md border border-line bg-surface text-sm font-medium text-ink-1 transition-colors hover:border-expria hover:bg-expria-50 hover:text-expria focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="size-8 shrink-0 rounded-md border border-line bg-surface text-sm font-medium text-ink-1 transition-colors hover:border-expria hover:bg-expria-50 hover:text-expria focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{char}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
* staleTime Infinity : un rapport ne change jamais après correction.
|
||||
*
|
||||
* Règle H : aucune logique métier — expose les données brutes.
|
||||
* FTD-17 : queryKey ['plan'] déjà utilisé dans SimulationPage — ['rapport', id] est distinct.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@
|
|||
import { useEffect } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Lock } from 'lucide-react'
|
||||
import { getPlanStatus } from '@/entities/user/api'
|
||||
import { usePlan } from '@/features/dashboard/hooks/usePlan'
|
||||
import { isSectionVisible } from '@/entities/report/lib'
|
||||
import { useRapport } from '../hooks/useRapport'
|
||||
import { Card } from '@/shared/ui/Card'
|
||||
|
|
@ -130,11 +129,7 @@ export function RapportPage() {
|
|||
data: planData,
|
||||
isLoading: isPlanLoading,
|
||||
isError: isPlanError,
|
||||
} = useQuery({
|
||||
queryKey: ['plan'],
|
||||
queryFn: getPlanStatus,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
} = usePlan()
|
||||
|
||||
const onUpgrade = () => navigate('/plan')
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,11 @@
|
|||
*
|
||||
* Règle D : quotas et permissions passent par canSimulate() — jamais de plan === '...'
|
||||
* Règle H : aucune logique métier — tout est dans useSimulation() et les entités.
|
||||
*
|
||||
* Nota : queryKey ['plan'] est dupliqué depuis features/dashboard/hooks/usePlan.
|
||||
* TanStack Query partage le cache par clé — FTD-17.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getPlanStatus } from '@/entities/user/api'
|
||||
import { usePlan } from '@/features/dashboard/hooks/usePlan'
|
||||
import { Button } from '@/shared/ui/Button'
|
||||
import { useSimulation } from '../hooks/useSimulation'
|
||||
import { TaskSelector } from '../components/TaskSelector'
|
||||
|
|
@ -41,11 +37,7 @@ export function SimulationPage() {
|
|||
isLoading: isPlanLoading,
|
||||
isError: isPlanError,
|
||||
refetch: refetchPlan,
|
||||
} = useQuery({
|
||||
queryKey: ['plan'],
|
||||
queryFn: getPlanStatus,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
} = usePlan()
|
||||
|
||||
const {
|
||||
step,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||
--shadow-lg: 0 12px 28px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
|
||||
--shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18);
|
||||
}
|
||||
|
||||
/* ─── Dark mode — override des tokens couleur et ombres ──────────── */
|
||||
|
|
@ -94,6 +95,7 @@
|
|||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
|
||||
--shadow-focus: 0 0 0 3px rgba(91, 127, 255, 0.32);
|
||||
}
|
||||
|
||||
/* ─── Rendu sub-pixel global (non couvert par Tailwind) ──────────── */
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function Button({
|
|||
className={cn(
|
||||
// base
|
||||
'inline-flex cursor-pointer items-center justify-center font-medium transition-colors duration-150',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-expria/20',
|
||||
'focus-visible:outline-none focus-visible:shadow-focus',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const variantClasses: Record<CardVariant, string> = {
|
|||
default : 'shadow-sm',
|
||||
raised : 'shadow-md',
|
||||
interactive:
|
||||
'shadow-sm cursor-pointer transition-colors duration-150 hover:border-expria hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-expria/20',
|
||||
'shadow-sm cursor-pointer transition-colors duration-150 hover:border-expria hover:bg-surface-hover focus-visible:outline-none focus-visible:shadow-focus',
|
||||
}
|
||||
|
||||
export function Card({ variant = 'default', className, children, onClick }: CardProps) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue