Files
Inventory/docs/superpowers/plans/2026-05-11-custom-field-name-autocomplete.md
Matthieu 73c06169f3 docs(custom-fields) : corrige la source de verite (table custom_fields unique)
L'investigation initiale supposait des customFields JSON dans les
skeleton_*_requirements ; en realite SkeletonStructureService traduit
les customFields du payload ModelType en entites CustomField stockees
dans la table custom_fields. Le SQL est donc un simple SELECT DISTINCT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:33:06 +02:00

26 KiB

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

declare(strict_types=1);

namespace App\Controller;

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[AsController]
final class CustomFieldNamesController
{
    public function __construct(private readonly Connection $connection)
    {
    }

    #[Route(
        path: '/api/custom-fields/names',
        name: 'api_custom_fields_names',
        methods: ['GET']
    )]
    #[IsGranted('ROLE_VIEWER')]
    public function __invoke(): JsonResponse
    {
        $sql = <<<'SQL'
            SELECT DISTINCT name
            FROM custom_fields
            WHERE name IS NOT NULL AND name <> ''
            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 :

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
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
make php-cs-fixer-allow-risky
  • Step 5: Commit
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.

ls tests/Api/Controller/ | head
  • Step 2: Créer le test

Créer tests/Api/Controller/CustomFieldNamesControllerTest.php :

<?php

declare(strict_types=1);

namespace App\Tests\Api\Controller;

use App\Tests\AbstractApiTestCase;

final class CustomFieldNamesControllerTest extends AbstractApiTestCase
{
    public function testReturns401WhenUnauthenticated(): void
    {
        $client = $this->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
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é
make test FILES=tests/Api/Controller/CustomFieldNamesControllerTest.php

Attendu : 3 tests OK.

  • Step 5: Commit
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 :

creatable: {
  type: Boolean,
  default: false
}
  • Step 2: Modifier handleInput pour emit en mode creatable

Remplacer la fonction handleInput (lignes ~284-289) par :

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 :

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 <script setup>, après la computed displayedOptions (ligne ~173), ajouter :

const creatableSuggestion = computed(() => {
  if (!props.creatable) return null
  const term = searchTerm.value.trim()
  if (!term) return null
  // Affiche "Créer ..." uniquement si aucune option exacte ne matche (case-insensitive)
  const exists = baseOptions.value.some(option => {
    const label = resolveLabel(option).toLowerCase()
    return label === term.toLowerCase()
  })
  return exists ? null : term
})
  • Step 5: Afficher la ligne "Créer ..." dans le template

Localiser le bloc dropdown (lignes ~40-81). Juste après la <ul> qui contient les options (donc juste avant la fermeture du <div v-if="openDropdown">), ajouter :

<button
  v-if="creatableSuggestion"
  type="button"
  class="w-full text-left px-3 py-2 hover:bg-base-200 focus:bg-base-200 focus:outline-none text-xs text-base-content/70 border-t border-base-200 flex items-center gap-2"
  @click="confirmCreatable"
>
  <IconLucidePlus class="w-3 h-3" aria-hidden="true" />
  Créer « {{ creatableSuggestion }} »
</button>

Ajouter l'import en haut :

import IconLucidePlus from '~icons/lucide/plus'
  • Step 6: Ajouter la fonction confirmCreatable

Après clearSelection (ligne ~295), ajouter :

function confirmCreatable () {
  if (creatableSuggestion.value) {
    emit('update:modelValue', creatableSuggestion.value)
  }
  openDropdown.value = false
}
  • Step 7: Ajuster la sync searchTermmodelValue en mode creatable

Localiser le watch sur modelValue (lignes ~194-202). Remplacer par :

watch(
  () => props.modelValue,
  () => {
    if (props.creatable) {
      if (searchTerm.value !== props.modelValue) {
        searchTerm.value = String(props.modelValue ?? '')
      }
      return
    }
    if (!openDropdown.value) {
      searchTerm.value = selectedOption.value ? resolveLabel(selectedOption.value) : ''
    }
  },
  { immediate: true }
)

En mode creatable, modelValue et searchTerm reflètent la même chose (le texte tapé) — on évite juste la boucle infinie en testant l'égalité.

  • Step 8: Vérifier le typecheck
cd frontend && npx nuxi typecheck

Attendu : 0 errors.

  • Step 9: Lancer ESLint
cd frontend && npm run lint:fix
  • Step 10: Test de non-régression manuel

Vérifier qu'un usage existant de SearchSelect (par exemple sur la page frontend/app/pages/index.vue ou similaire — chercher <SearchSelect) fonctionne toujours sans le prop creatable : le comportement strict doit être identique à avant.

cd frontend && grep -rln "SearchSelect" app/ | head -5

Ouvrir un de ces écrans en dev et vérifier que :

  • La sélection d'une option marche

  • Le blur sans sélection reset au label précédent (= mode strict inchangé)

  • Step 11: Commit

git add frontend/app/components/common/SearchSelect.vue
git commit -m "feat(search-select) : ajoute prop creatable pour autoriser la saisie libre

En mode creatable=true, le composant emit le texte tape en temps reel
et ne reset plus au blur. Une ligne 'Creer XYZ' apparait quand le texte
ne matche aucune option. Mode strict (defaut) inchange."

Task 4: Frontend — Composable useCustomFieldNameSuggestions

Files:

  • Create: frontend/app/composables/useCustomFieldNameSuggestions.ts

  • Step 1: Vérifier le pattern useApi existant

cat frontend/app/composables/useApi.ts | head -30

Noter la signature exacte (<T>(path, opts?) => Promise<T> ou autre) pour l'adapter.

  • Step 2: Créer le composable

Créer frontend/app/composables/useCustomFieldNameSuggestions.ts :

import { ref } from 'vue'

const cache = ref<string[] | null>(null)
const loading = ref(false)

interface Deps {
  api: ReturnType<typeof useApi>
}

export function useCustomFieldNameSuggestions(deps: Deps) {
  const { api } = deps

  async function load(force = false): Promise<string[]> {
    if (cache.value && !force) return cache.value
    if (loading.value) return cache.value ?? []
    loading.value = true
    try {
      const result = await api<string[]>('/api/custom-fields/names')
      cache.value = Array.isArray(result) ? result : []
      return cache.value
    } catch (err) {
      console.error('[useCustomFieldNameSuggestions] failed to load', err)
      cache.value = cache.value ?? []
      return cache.value
    } finally {
      loading.value = false
    }
  }

  function invalidate(): void {
    cache.value = null
  }

  return {
    suggestions: cache,
    loading,
    load,
    invalidate,
  }
}

Note : cache et loading sont déclarés au niveau du module (hors de la fonction) → cache partagé entre toutes les instances.

  • Step 3: Vérifier le typecheck
cd frontend && npx nuxi typecheck

Attendu : 0 errors. Si le ReturnType<typeof useApi> pose souci (selon comment useApi est typé), remplacer par un type explicite plus simple :

interface Deps {
  api: <T>(path: string, opts?: RequestInit) => Promise<T>
}
  • Step 4: Commit
git add frontend/app/composables/useCustomFieldNameSuggestions.ts
git commit -m "feat(custom-fields) : ajoute composable useCustomFieldNameSuggestions

Cache module-level partage entre toutes les instances. Lazy load au
premier appel a load(). invalidate() permet de forcer un refresh apres
creation/modification d'un champ perso."

Task 5: Frontend — Composant wrapper CustomFieldNameInput

Files:

  • Create: frontend/app/components/common/CustomFieldNameInput.vue

  • Step 1: Créer le composant

Créer frontend/app/components/common/CustomFieldNameInput.vue :

<template>
  <SearchSelect
    :model-value="modelValue"
    :options="options"
    :placeholder="placeholder"
    option-value="name"
    option-label="name"
    creatable
    :size="size"
    @update:model-value="onUpdate"
    @focus="ensureLoaded"
  />
</template>

<script setup lang="ts">
import { computed } from 'vue'
import SearchSelect from './SearchSelect.vue'
import { useCustomFieldNameSuggestions } from '~/composables/useCustomFieldNameSuggestions'

const props = withDefaults(defineProps<{
  modelValue: string
  placeholder?: string
  size?: 'xs' | 'sm' | 'md' | 'lg'
}>(), {
  placeholder: 'Nom du champ',
  size: 'xs',
})

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const { suggestions, load } = useCustomFieldNameSuggestions({ api: useApi() })

const options = computed(() => (suggestions.value ?? []).map(name => ({ name })))

function ensureLoaded(): void {
  void load()
}

function onUpdate(value: string | number): void {
  emit('update:modelValue', String(value ?? ''))
}
</script>

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 :

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
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 :

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
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
<input v-model='field.name'> par <CustomFieldNameInput v-model='field.name'>
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 :

<input
  v-model="field.name"
  type="text"
  class="input input-bordered input-sm"
  placeholder="Nom du champ"
>

Remplacer par :

<CustomFieldNameInput
  v-model="field.name"
  placeholder="Nom du champ"
  size="sm"
/>
  • Step 2: Vérifier le typecheck
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
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 :

<input
  :value="field.name"
  type="text"
  class="input input-bordered input-sm"
  placeholder="Nom du champ"
  @blur="handleDefinitionUpdate(field, 'name', ($event.target as HTMLInputElement).value)"
/>

⚠️ 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 :

<CustomFieldNameInput
  :model-value="field.name"
  placeholder="Nom du champ"
  size="sm"
  @update:model-value="(value) => handleDefinitionUpdate(field, 'name', value)"
/>

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.

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
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
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 :

<input
  v-model="field.name"
  type="text"
  class="input input-bordered input-xs"
  placeholder="Nom du champ"
>

Remplacer par :

<CustomFieldNameInput
  v-model="field.name"
  placeholder="Nom du champ"
  size="xs"
/>
  • Step 2: Vérifier le typecheck
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
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 :

<input
  v-model="field.name"
  type="text"
  class="input input-bordered input-xs"
  placeholder="Nom du champ"
/>

Remplacer par :

<CustomFieldNameInput
  v-model="field.name"
  placeholder="Nom du champ"
  size="xs"
/>
  • Step 2: Vérifier le typecheck
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
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

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 :

import { useCustomFieldNameSuggestions } from './useCustomFieldNameSuggestions'

Dans le corps du composable, ajouter (à placer près des autres use* calls) :

const { invalidate: invalidateCustomFieldNames } = useCustomFieldNameSuggestions({ api: useApi() })

Puis, après chaque save réussi (à la fin du try du POST/PATCH des definitions), appeler :

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
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 <script setup> de ModelTypeForm.vue :

import { useCustomFieldNameSuggestions } from '~/composables/useCustomFieldNameSuggestions'

const { invalidate: invalidateCustomFieldNames } = useCustomFieldNameSuggestions({ api: useApi() })

Puis, après la sauvegarde réussie du ModelType (typiquement après le await api(...) qui POST/PATCH /api/model_types/...) :

invalidateCustomFieldNames()
  • Step 5: Vérifier le typecheck
cd frontend && npx nuxi typecheck
  • Step 6: Test manuel

Scénario :

  1. Ouvrir une machine, ajouter un champ perso « Test invalidation 2026 » et save.
  2. Ouvrir une autre machine ou un ModelType.
  3. Tenter d'ajouter un champ perso → taper « Test invalid » → vérifier que « Test invalidation 2026 » apparaît dans les suggestions.
  • Step 7: Commit
git add frontend/app/composables/useMachineCustomFieldDefs.ts frontend/app/components/model-types/ModelTypeForm.vue
git commit -m "feat(custom-fields) : invalide le cache de suggestions apres save

Apres chaque save reussi de champs perso (machine ou ModelType), on
invalide le cache useCustomFieldNameSuggestions pour que les noms
nouvellement crees apparaissent dans les futures autocomplete."

Task 11: Validation finale

Files: aucun changement, juste vérification end-to-end.

  • Step 1: Lancer le typecheck complet
cd frontend && npx nuxi typecheck

Attendu : 0 errors.

  • Step 2: Lancer le linter complet
cd frontend && npm run lint:fix

Attendu : 0 errors (ou seulement des fixes auto).

  • Step 3: Test end-to-end manuel

Démarrer l'environnement local (make start si pas déjà fait), puis :

  1. Machine : créer une machine, ajouter 2 champs perso « Numéro de série » et « Tension ». Save.
  2. ModelType : créer un ModelType de catégorie composant, ajouter une pièce dans le skeleton, ajouter à cette pièce un champ perso. Vérifier que « Numéro de série » et « Tension » apparaissent dans les suggestions.
  3. Structure : créer un ModelType de catégorie machine, naviguer dans la structure, ajouter un champ perso à un composant. Vérifier les suggestions.
  4. Création libre : taper un nom inédit, voir la ligne « Créer ... », cliquer ou faire blur → garder le texte.
  5. Sélection : cliquer sur une suggestion → input se remplit avec le nom exact.
  • Step 4: Vérifier le commit log
git log --oneline -15

Confirmer qu'on a bien 1 commit par task, avec des messages cohérents.

  • Step 5: Push (optionnel, à confirmer avec l'utilisateur)
git push

⚠️ Ne PAS push sans demande explicite de l'utilisateur.


Récapitulatif des fichiers

Créés

  • src/Controller/CustomFieldNamesController.php
  • tests/Api/Controller/CustomFieldNamesControllerTest.php
  • frontend/app/composables/useCustomFieldNameSuggestions.ts
  • frontend/app/components/common/CustomFieldNameInput.vue

Modifiés

  • frontend/app/components/common/SearchSelect.vue
  • frontend/app/components/machine/MachineCustomFieldDefEditor.vue
  • frontend/app/components/machine/MachineCustomFieldsCard.vue
  • frontend/app/components/PieceModelStructureEditor.vue
  • frontend/app/components/StructureNodeEditor.vue
  • frontend/app/composables/useMachineCustomFieldDefs.ts
  • frontend/app/components/model-types/ModelTypeForm.vue