Appearance
Inline edit — édition en place sur les fiches détail
1. Responsibilities
- Permettre la modification de chaque champ d'une fiche Contact ou Entreprise sans ouvrir la modale
ContactFormModal/CompanyFormModal. - Au survol (desktop) ou au tap (mobile), afficher un contour + icône crayon sur le champ.
- Sauvegarder automatiquement sur
blurouEnter, annuler surEscape. - Valider le payload côté client via les schémas Zod existants (
UpdateContactSchema,UpdateCompanySchema) avant d'envoyer le PATCH. - Conserver les modales existantes comme parcours de création et fallback d'édition.
2. UX
| Contexte | Comportement |
|---|---|
| Desktop hover/focus | Contour --color-border-strong + fond --bg-sunken + icône crayon opacity: 1. |
Mobile (<640px) | Icône crayon opacity: 0.55 en permanence, zone de tap ≥ 44 px. |
| Click / tap | Bascule en mode édition, input autofocus, sélection complète pour les inputs texte. |
| Enter (single-line) | Sauvegarde + retour mode lecture. |
| Cmd/Ctrl+Enter (textarea) | Sauvegarde. Enter seul = nouvelle ligne. |
| Escape | Annule, restaure la valeur précédente, retour mode lecture. |
| Blur (clic hors champ) | Sauvegarde. |
| Offline | Le composant est désactivé via useOfflineMode().isOffline. |
3. Component API
Fichier : apps/web/components/inline-edit/InlineEditField.vue
| Prop | Type | Description |
|---|---|---|
modelValue | V | null | undefined | Valeur courante (toute primitive ou tableau pour les tags). |
fieldName | string | Clé du champ dans le payload PATCH ('email', 'companyId', 'tags', …). |
type | 'text' | 'email' | 'tel' | 'url' | 'textarea' | 'select' | 'tags' | 'company' | Détermine l'éditeur rendu en mode édition. |
options | { value: string; label: string }[] | Obligatoire pour type='select'. |
schema | ZodTypeAny | Schéma Zod par champ (UpdateContactSchema.shape.email). Validation synchrone avant onSave. |
onSave | (payload: Record<string, unknown>) => Promise<unknown> | Callback qui envoie le PATCH ; lever permet de déclencher rollback + toast. |
label | string | Utilisé pour aria-label="Modifier <label>" et placeholder « Ajouter <label> ». |
disabled | boolean | Désactive le déclencheur. |
formatDisplay | (v: V) => string | Formatte la valeur affichée par défaut. |
testId | string | Suffixe pour data-testid="inline-edit-<testId>" (défaut : fieldName). |
Slots :
display(scoped :{ value, isEmpty }) — surcharge le rendu en mode lecture (lienmailto:, badge statut, lien entreprise, etc.).
Émissions : saved (avec la nouvelle valeur) et cancelled.
4. Configuration des champs
ContactDetailHeader.vue
fieldName | type | Schéma |
|---|---|---|
firstName | text | UpdateContactSchema.shape.firstName |
lastName | text | UpdateContactSchema.shape.lastName |
status | select (CONTACT_STATUSES) | — (validation enum côté serveur) |
role | text | UpdateContactSchema.shape.role |
companyId | company (CompanyCombobox) | — |
email | email | UpdateContactSchema.shape.email |
linkedinUrl | url | UpdateContactSchema.shape.linkedinUrl |
phone | tel | UpdateContactSchema.shape.phone |
tags | tags (TagsInput) | — |
notes | textarea | UpdateContactSchema.shape.notes |
pages/companies/[id].vue
fieldName | type | Schéma |
|---|---|---|
name | text | UpdateCompanySchema.shape.name |
industry | text | UpdateCompanySchema.shape.industry |
size | select (COMPANY_SIZES) | — |
domain | text | UpdateCompanySchema.shape.domain |
notes | textarea | UpdateCompanySchema.shape.notes |
5. Validation
- Synchrone (client) : si
schemaest fourni,safeParsela valeur normalisée. En cas d'erreur, le premierissues[0].messageest affiché en rouge sous l'éditeur et l'édition reste ouverte (l'utilisateur peut corriger). - Réseau (serveur) : si
onSaverejette, la valeur précédente est restaurée et un toasttoast.error('Impossible de mettre à jour ce champ.')est affiché. L'édition se ferme. - La validation enum (
status,size) est faite par laclass-validatorcôté backend (cf.apps/api/src/contacts/dto/create-contact.dto.ts). Un PATCH avec une valeur invalide retourne 400 → toast d'erreur.
6. State flow
Le caller (ex. ContactDetailHeader.vue) fournit une fonction saveContactField :
ts
async function saveContactField(payload: Record<string, unknown>) {
const key = ['contact', contactId.value]
const previous = qc.getQueryData<any>(key)
qc.setQueryData<any>(key, (old: any) => (old ? { ...old, ...payload } : old))
try {
await updateContact(payload) // useUpdateContact(contactId)
} catch (e) {
qc.setQueryData(key, previous) // rollback
toast.error('Impossible de mettre à jour ce champ.')
throw e // remonte au composant pour exit edit-mode
}
}useUpdateContact invalide ensuite ['contact', id] + ['contacts'] (idem useUpdateCompany).
7. Tests
API (Jest)
apps/api/src/contacts/contacts.service.spec.ts- "met à jour un seul champ (PATCH partiel via inline-edit)"
- "accepte companyId=null pour dissocier l'entreprise (inline-edit)"
apps/api/src/companies/companies.service.spec.ts- "met à jour un seul champ (PATCH partiel via inline-edit)"
E2E (Playwright)
apps/e2e/tests/contacts.spec.ts→describe('Inline edit fiche contact')apps/e2e/tests/companies.spec.ts→describe('Inline edit fiche entreprise')
Convention de sélecteur introduite : data-testid="inline-edit-<field>" sur le wrapper. Première utilisation de data-testid dans ce repo, motivée par le fait que le texte visible est la valeur (qui change pendant le test) et rend les sélecteurs getByText fragiles.
8. Limitations
- Pas de file d'attente offline :
useOfflineMode().isOfflinedésactive le composant ; aucune écriture n'est mise en queue. - Pas de gestion de conflit multi-utilisateur : last-write-wins. Une édition concurrente écrase l'autre, sans détection.
- Pas d'audit log : les modifications inline ne sont pas tracées séparément des modifications via modale.
- Pas d'inline-edit dans les listes (
/contacts,/companies) : limité aux pages détail. - Race optimistic ↔ invalidation : sur réseau lent, la valeur optimiste peut clignoter quand le refetch arrive après le PATCH. Comportement acceptable, serveur est source de vérité.
