# Custom Field Name Autocomplete — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Ajouter une autocomplétion sur les noms de champs personnalisés dans tous les éditeurs (machine + ModelType) pour permettre la réutilisation des noms existants tout en gardant la possibilité d'en créer de nouveaux. **Architecture:** - **Backend** : un endpoint utilitaire `GET /api/custom-fields/names` qui retourne la liste plate des noms distincts de la table `custom_fields`. - **Frontend** : extension de `SearchSelect.vue` avec un prop `creatable`, composable `useCustomFieldNameSuggestions` avec cache module-level, composant wrapper `CustomFieldNameInput.vue`, migration de 4 éditeurs. **Tech Stack:** Symfony 8 + API Platform + Doctrine DBAL, Nuxt 4 + Vue 3 Composition API + TypeScript, DaisyUI. **Référence spec:** `docs/superpowers/specs/2026-05-11-custom-field-name-autocomplete-design.md` --- ## Task 1: Backend — Controller `CustomFieldNamesController` **Files:** - Create: `src/Controller/CustomFieldNamesController.php` - [ ] **Step 1: Créer le controller** Créer `src/Controller/CustomFieldNamesController.php` avec le contenu : ```php '' ORDER BY name ASC SQL; $names = $this->connection->fetchFirstColumn($sql); return new JsonResponse($names); } } ``` - [ ] **Step 2: Vérifier que la route est bien exposée** Exécuter : ```bash docker exec -u www-data php-inventory-apache php bin/console debug:router | grep custom-fields/names ``` Attendu : une ligne contenant `GET /api/custom-fields/names` et `api_custom_fields_names`. - [ ] **Step 3: Tester manuellement le endpoint** ```bash curl -s -b "$(curl -s -c - -X POST http://localhost:8081/api/session/profile \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"admin"}' | grep PHPSESSID)" \ http://localhost:8081/api/custom-fields/names | head -c 500 ``` > Si la session est galère à monter en curl, on peut tester via le navigateur après login (DevTools → fetch). Attendu : un tableau JSON `["Numéro de série", "Tension", ...]` (ou `[]` si la base de dev est vide). - [ ] **Step 4: Lancer php-cs-fixer** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 5: Commit** ```bash git add src/Controller/CustomFieldNamesController.php git commit -m "feat(custom-fields) : ajoute endpoint GET /api/custom-fields/names Retourne la liste plate des noms de champs perso distincts (table custom_fields), pour alimenter une autocompletion cote frontend." ``` > ⚠️ Le pre-commit hook va lancer PHPUnit. Si des tests existants échouent (peu probable car on n'a touché à rien d'existant), résoudre le souci avant de continuer. --- ## Task 2: Backend — Test PHPUnit du endpoint **Files:** - Create: `tests/Api/Controller/CustomFieldNamesControllerTest.php` - [ ] **Step 1: Repérer un test existant à copier-coller pour le style** Lire `tests/Api/Controller/HealthCheckController*Test.php` ou un controller simple existant pour récupérer le pattern (auth helpers, `ApiTestCase`). Adapter selon ce qu'on trouve. ```bash ls tests/Api/Controller/ | head ``` - [ ] **Step 2: Créer le test** Créer `tests/Api/Controller/CustomFieldNamesControllerTest.php` : ```php createUnauthenticatedClient(); $client->request('GET', '/api/custom-fields/names'); self::assertResponseStatusCodeSame(401); } public function testReturnsEmptyArrayWhenNoCustomFields(): void { $client = $this->createViewerClient(); $client->request('GET', '/api/custom-fields/names'); self::assertResponseIsSuccessful(); $data = json_decode($client->getResponse()->getContent(), true); self::assertIsArray($data); } public function testReturnsDistinctSortedNames(): void { // Crée 3 machines avec des CustomField : "Tension", "Numéro de série", "Tension" (doublon) $machine1 = $this->createMachine(); $this->createCustomField(['name' => 'Tension', 'machine' => $machine1]); $this->createCustomField(['name' => 'Numéro de série', 'machine' => $machine1]); $machine2 = $this->createMachine(); $this->createCustomField(['name' => 'Tension', 'machine' => $machine2]); // doublon $client = $this->createViewerClient(); $client->request('GET', '/api/custom-fields/names'); self::assertResponseIsSuccessful(); $data = json_decode($client->getResponse()->getContent(), true); self::assertContains('Tension', $data); self::assertContains('Numéro de série', $data); // Pas de doublon self::assertSame(count(array_unique($data)), count($data)); // Tri alpha $sorted = $data; sort($sorted, SORT_STRING); self::assertSame($sorted, $data); } } ``` > Si la factory `createCustomField` n'a pas la signature attendue (1er argument = array), regarder `tests/AbstractApiTestCase.php` pour adapter aux helpers réels du projet. - [ ] **Step 3: Vérifier que les helpers utilisés existent** ```bash grep -n "createCustomField\|createMachine\|createViewerClient\|createUnauthenticatedClient" tests/AbstractApiTestCase.php | head ``` Si l'un des helpers manque ou a une autre signature, **adapter le test** plutôt que d'ajouter de nouveaux helpers. - [ ] **Step 4: Lancer le test ciblé** ```bash make test FILES=tests/Api/Controller/CustomFieldNamesControllerTest.php ``` Attendu : 3 tests OK. - [ ] **Step 5: Commit** ```bash git add tests/Api/Controller/CustomFieldNamesControllerTest.php git commit -m "test(custom-fields) : ajoute test PHPUnit pour endpoint /api/custom-fields/names" ``` --- ## Task 3: Frontend — Étendre `SearchSelect.vue` avec le prop `creatable` **Files:** - Modify: `frontend/app/components/common/SearchSelect.vue` - [ ] **Step 1: Ajouter le prop `creatable` au composant** Dans le bloc `defineProps` (lignes ~91-141), ajouter après le prop `serverSearch` : ```js creatable: { type: Boolean, default: false } ``` - [ ] **Step 2: Modifier `handleInput` pour emit en mode creatable** Remplacer la fonction `handleInput` (lignes ~284-289) par : ```js function handleInput () { if (!openDropdown.value) { openDropdown.value = true } if (props.creatable) { emit('update:modelValue', searchTerm.value) } emit('search', searchTerm.value) } ``` - [ ] **Step 3: Modifier `closeDropdown` pour ne pas reset en mode creatable** Remplacer la fonction `closeDropdown` (lignes ~297-304) par : ```js function closeDropdown () { openDropdown.value = false if (props.creatable) { return // garde le texte tapé tel quel } if (searchTerm.value.trim() === '' && selectedOption.value) { emit('update:modelValue', '') } else if (selectedOption.value) { searchTerm.value = resolveLabel(selectedOption.value) } } ``` - [ ] **Step 4: Ajouter une computed `creatableSuggestion`** Dans le bloc ` ``` > `SearchSelect` n'expose pas nativement un événement `@focus`. Vérifier dans la Task 3 si on l'a ajouté ou si on doit charger autrement. - [ ] **Step 2: Exposer `@focus` depuis `SearchSelect.vue`** Retourner sur `SearchSelect.vue` et vérifier si `@focus` est propagé. Si non, modifier le handler `handleFocus` (ligne ~267-272) pour également émettre : ```js const emit = defineEmits(['update:modelValue', 'search', 'focus']) function handleFocus () { openDropdown.value = true if (searchTerm.value === '' && selectedOption.value) { searchTerm.value = resolveLabel(selectedOption.value) } emit('focus') } ``` Ajouter `'focus'` à la liste des emits si pas déjà présent. - [ ] **Step 3: Vérifier le typecheck** ```bash cd frontend && npx nuxi typecheck ``` - [ ] **Step 4: Vérifier l'auto-import Nuxt** Le composant étant dans `components/common/`, Nuxt devrait l'auto-importer. Vérifier après build : ```bash cd frontend && npm run dev ``` Sans erreur de référence `CustomFieldNameInput is not defined` quand on l'utilisera dans les tâches suivantes. - [ ] **Step 5: Commit** ```bash git add frontend/app/components/common/CustomFieldNameInput.vue frontend/app/components/common/SearchSelect.vue git commit -m "feat(custom-fields) : ajoute CustomFieldNameInput wrapper Encapsule SearchSelect en mode creatable, branche useCustomFieldName- Suggestions, charge la liste au focus. Permet de remplacer un simple par dans les editeurs de champs perso." ``` --- ## Task 6: Frontend — Migrer `MachineCustomFieldDefEditor.vue` **Files:** - Modify: `frontend/app/components/machine/MachineCustomFieldDefEditor.vue` - [ ] **Step 1: Remplacer l'input du nom** Dans `frontend/app/components/machine/MachineCustomFieldDefEditor.vue`, localiser les lignes 36-41 : ```vue ``` Remplacer par : ```vue ``` - [ ] **Step 2: Vérifier le typecheck** ```bash cd frontend && npx nuxi typecheck ``` - [ ] **Step 3: Test manuel rapide** Ouvrir une machine en édition, ajouter un champ perso, vérifier que l'input : 1. Affiche un dropdown au focus avec les noms existants 2. Filtre quand on tape 3. Sélection d'une suggestion → input rempli 4. Texte libre + blur → garde le texte tapé - [ ] **Step 4: Commit** ```bash git add frontend/app/components/machine/MachineCustomFieldDefEditor.vue git commit -m "feat(custom-fields) : autocomplete sur le nom dans MachineCustomFieldDefEditor" ``` --- ## Task 7: Frontend — Migrer `MachineCustomFieldsCard.vue` **Files:** - Modify: `frontend/app/components/machine/MachineCustomFieldsCard.vue` - [ ] **Step 1: Remplacer l'input du nom** Dans `frontend/app/components/machine/MachineCustomFieldsCard.vue`, localiser les lignes ~53-59 : ```vue ``` ⚠️ **Attention** : cet input utilise `:value` + `@blur` (pas `v-model`) parce qu'il déclenche une mise à jour seulement au blur (avec un appel API). Remplacer par : ```vue ``` > Le `@update:model-value` se déclenchera à chaque changement (donc à chaque caractère tapé en mode creatable). Si ce comportement génère trop d'appels API, on peut wrapper avec un `debounce` côté `handleDefinitionUpdate`. Pour l'instant, on garde simple. - [ ] **Step 2: Vérifier le comportement de `handleDefinitionUpdate`** Vérifier que cette fonction est idempotente (rejouer un même nom = pas d'effet). Cherche la fonction dans le composant et confirme qu'elle compare l'ancienne/nouvelle valeur avant d'appeler l'API. ```bash grep -n "handleDefinitionUpdate" frontend/app/components/machine/MachineCustomFieldsCard.vue ``` Si elle ne dédoublonne pas, l'ajout d'un test rapide `if (field.name === value) return` peut éviter des PATCH inutiles. - [ ] **Step 3: Vérifier le typecheck** ```bash cd frontend && npx nuxi typecheck ``` - [ ] **Step 4: Test manuel** Sur la page d'une machine, modifier inline un nom de champ perso → vérifier que ça déclenche un PATCH unique (DevTools Network). - [ ] **Step 5: Commit** ```bash git add frontend/app/components/machine/MachineCustomFieldsCard.vue git commit -m "feat(custom-fields) : autocomplete sur le nom dans MachineCustomFieldsCard" ``` --- ## Task 8: Frontend — Migrer `PieceModelStructureEditor.vue` **Files:** - Modify: `frontend/app/components/PieceModelStructureEditor.vue` - [ ] **Step 1: Remplacer l'input du nom** Dans `frontend/app/components/PieceModelStructureEditor.vue`, localiser les lignes 97-102 : ```vue ``` Remplacer par : ```vue ``` - [ ] **Step 2: Vérifier le typecheck** ```bash cd frontend && npx nuxi typecheck ``` - [ ] **Step 3: Test manuel** Ouvrir un ModelType (catégorie composant ou skeleton), ajouter une pièce dans le skeleton, lui ajouter un champ perso → vérifier dropdown. - [ ] **Step 4: Commit** ```bash git add frontend/app/components/PieceModelStructureEditor.vue git commit -m "feat(custom-fields) : autocomplete sur le nom dans PieceModelStructureEditor" ``` --- ## Task 9: Frontend — Migrer `StructureNodeEditor.vue` **Files:** - Modify: `frontend/app/components/StructureNodeEditor.vue` - [ ] **Step 1: Remplacer l'input du nom** Dans `frontend/app/components/StructureNodeEditor.vue`, localiser les lignes 106-111 : ```vue ``` Remplacer par : ```vue ``` - [ ] **Step 2: Vérifier le typecheck** ```bash cd frontend && npx nuxi typecheck ``` - [ ] **Step 3: Test manuel** Ouvrir un ModelType de catégorie machine, naviguer dans la structure (composants/sous-composants), ajouter un champ perso à un node → vérifier dropdown. - [ ] **Step 4: Commit** ```bash git add frontend/app/components/StructureNodeEditor.vue git commit -m "feat(custom-fields) : autocomplete sur le nom dans StructureNodeEditor" ``` --- ## Task 10: Frontend — Invalidation du cache après save **Files:** - Modify: `frontend/app/composables/useMachineCustomFieldDefs.ts` - Modify: `frontend/app/components/model-types/ModelTypeForm.vue` - [ ] **Step 1: Repérer le save dans `useMachineCustomFieldDefs.ts`** ```bash grep -n "POST\|PATCH\|api(" frontend/app/composables/useMachineCustomFieldDefs.ts | head -20 ``` Localiser la(es) fonction(s) qui sauvegarde les champs perso (probablement `saveDefinitions`, `addCustomFields`, etc.). - [ ] **Step 2: Ajouter l'invalidation après save** Dans `useMachineCustomFieldDefs.ts`, en haut du fichier, après les imports existants : ```ts import { useCustomFieldNameSuggestions } from './useCustomFieldNameSuggestions' ``` Dans le corps du composable, ajouter (à placer près des autres `use*` calls) : ```ts const { invalidate: invalidateCustomFieldNames } = useCustomFieldNameSuggestions({ api: useApi() }) ``` Puis, après chaque save réussi (à la fin du `try` du POST/PATCH des definitions), appeler : ```ts invalidateCustomFieldNames() ``` > Identifier précisément les points de save dans le fichier — probablement 1 ou 2 endroits maximum. - [ ] **Step 3: Repérer le save dans `ModelTypeForm.vue`** ```bash grep -n "POST\|PATCH\|api(\|emit('saved')\|emit('save'" frontend/app/components/model-types/ModelTypeForm.vue | head -20 ``` - [ ] **Step 4: Ajouter l'invalidation après save** Dans le `