Yayaw
Documentation
Référence

Référence des formulaires

Construire des formulaires create/edit avec champs groupés, tableaux d'options natifs, validation et collections imbriquées

Formulaires, sections, multi-select et champs collection

Yayaw Table peut rendre des formulaires de création, d'édition et d'édition de masse à partir d'une configuration. Vous fournissez un FormConfig via getFormConfig, et la table ouvre le formulaire correspondant dans le drawer ou la modal intégrée quand l'utilisateur crée ou modifie une ligne.

La plupart des champs correspondent à une valeur primitive : text, number, select, switch, textarea, url, value-type ou custom. multiSelect édite un tableau de valeurs d'options string ou number, et collection édite un tableau d'objets avec une interface structurée. Les deux sont conçus pour les données de type JSON qui forceraient sinon chaque application consommatrice à construire son propre mini éditeur.

Utilisez TableConfig.presentation à la racine pour une surface commune de consultation, création, édition et édition groupée, avec un choix mobile facultatif. Le défaut est un drawer à droite sur tous les écrans. Consultez la présentation commune des fiches pour la configuration et la migration.

Contrat de formulaire commun à React et Vue

Les deux éditions génèrent les champs standards depuis les colonnes lorsqu'aucun formulaire correspondant n'est enregistré. Un FormConfig du catalogue personnalise ce comportement. Les instances de formulaire React, les factories et les rendus personnalisés restent compatibles ; Vue utilise ses composants et ses VNodes.

La configuration commune accepte hidden et disabled sous forme de booléens ou de prédicats de contexte, defaultValue, les schema de champ et de formulaire, transform et submitMode: "patch". Le contexte contient tableId, tableType, formType, mode, row, initialData et les values courantes. Utilisez setFieldValue pour modifier un autre champ.

const productForm = {
  id: "product",
  submitMode: "patch",
  fields: [
    { name: "name", label: "Nom", type: "text", required: true },
    { name: "active", label: "Actif", type: "switch", defaultValue: true },
    {
      name: "reason", label: "Motif", type: "text",
      hidden: ({ values }) => values?.active !== false,
    },
    {
      name: "lines", label: "Lignes", type: "collection",
      collectionMode: "inline",
      itemFields: [
        { name: "label", label: "Libellé", type: "text", required: true },
        { name: "quantity", label: "Quantité", type: "number", min: 0 },
      ],
    },
  ],
};

itemFields génère récursivement les éditeurs imbriqués, y compris les collections. collectionMode choisit l'édition intégrée ou en dialogue ; les implémentations renderItemForm restent disponibles. Les erreurs conservent le chemin de l'élément concerné et bloquent l'envoi.

Sélecteur avec table

Utilisez le champ déclaratif tablePicker lorsqu'une relation nécessite l'expérience complète de la table plutôt qu'un select fini. React et Vue rendent une table imbriquée en lecture seule avec données locales ou serveur, recherche, filtres, tri, pagination, vues enregistrées et sélection contrôlée. Les mutations et exports sont masqués dans ce mode ; list, aggregate et views restent disponibles.

createTablePickerField({
  name: "mediaIds",
  label: "Médias",
  tablePicker: {
    tableType: "media-picker",
    config: mediaPickerConfig,
    actions: { list: listMedia },
    getRowId: (row) => String(row.id),
    parseValue: Number,
  },
})

La sélection traverse les changements de recherche, filtres, tri, regroupement et pagination. parseValue conserve le type des IDs de relation ; sans lui, les valeurs sélectionnées sont des strings. Utilisez multiple: false pour une valeur scalaire, selectOnRowClick: false pour imposer la checkbox, ou maxHeight pour limiter la zone scrollable. Un sélecteur imbriqué utilise syncUrl: false par défaut afin de ne pas écraser les paramètres de la table principale. optionDependencies permet de reconstruire une configuration dépendante des autres valeurs du formulaire.

Initialisation, options et envoi

  • loadInitialValues(row, context, signal) charge les valeurs initiales de façon asynchrone, avec possibilité de réessayer après erreur. La fermeture, le changement de formulaire ou une nouvelle tentative annule les requêtes obsolètes ; une réponse tardive ne remplace pas le nouveau brouillon.

  • options(context) accepte un chargement asynchrone. optionDependencies limite les rechargements aux dépendances déclarées ; optionsScope distingue les contextes de tenant ou de permissions.

  • searchOptions(query, context, signal) utilise searchMinLength et searchDebounceMs. resolveOptions(values, context, signal) retrouve les libellés des valeurs existantes. createOption(label, context, signal) renvoie l'option enregistrée. Respectez le signal d'annulation et contrôlez les permissions côté serveur.

  • Les schémas de champ précèdent le schéma global. La préparation retire les champs masqués ou désactivés et, en modification partielle, les valeurs déclarées inchangées. transform(values, context) reçoit ce payload préparé et doit accepter un objet partiel en mode patch.

  • false, 0, "", [] et null sont conservés. Un schéma peut les refuser, mais la table ne les supprime pas implicitement. Les fieldErrors de l'action restent associés aux champs et le brouillon reste ouvert.

Utilisez form.resolveEditFormType(row) pour choisir un catalogue selon la ligne. Consultez les formulaires Vue et les actions groupées pour les variantes et les tentatives partielles.

Champs multi-select

Utilisez type: "multiSelect" quand la valeur du formulaire est un tableau de valeurs primitives choisies dans une liste finie. Yayaw Table rend chaque option comme une checkbox, écrit un nouveau tableau via TanStack Form, et préserve l'ordre des options configurées.

{
  type: "multiSelect",
  name: "capabilities",
  label: "Capabilities",
  options: [
    { label: "Native tables", value: "native_tables" },
    { label: "Runtime API", value: "runtime_api" },
  ],
}

Utilisez multiSelect pour des ensembles de capabilities, régions supportées, modules activés, ou d'autres tableaux finis où chaque valeur sélectionnée est une string ou un number. Utilisez plutôt collection quand chaque item du tableau a plusieurs champs ou demande son propre éditeur.

Sections de formulaire

Utilisez sections quand un formulaire admin généré doit présenter des groupes clairs sans React personnalisé. Les sections référencent des champs existants, donc la forme des données et le schéma de validation ne changent pas.

defineFormConfig({
  id: "article",
  schema: ArticleSchema,
  defaultValues: {},
  fields: [
    { type: "text", name: "title", label: "Title" },
    { type: "textarea", name: "summary", label: "Summary" },
    { type: "switch", name: "published", label: "Published" },
  ],
  sections: [
    {
      id: "content",
      title: "Content",
      description: "Editorial fields shown to authors.",
      fields: ["title", "summary"],
    },
    {
      id: "publishing",
      title: "Publishing",
      fields: ["published"],
    },
  ],
});

Les champs qui ne sont référencés par aucune section restent rendus après les sections déclarées, dans leur ordre initial. Yayaw Table ignore les noms de champs inconnus, tandis que la validation des modèles dynamic-data Yayaw signale les références de section inconnues avant déploiement.

Ce que permettent les champs collection

Utilisez type: "collection" quand une valeur de formulaire est un tableau et que les utilisateurs doivent gérer ses éléments visuellement.

Cas adaptés :

  • Menus de navigation stockés en JSON, avec liens, groupes, toggles et liens imbriqués.

  • Listes de fonctionnalités où chaque fonctionnalité a un label, une icône, un état actif et une description.

  • Règles de prix répétées, FAQ, méthodes de contact, éléments de galerie ou blocs de contenu localisés.

  • Tout champ “tableau d'enregistrements” où une simple textarea JSON serait trop risquée pour les utilisateurs.

L'interface collection intégrée fournit :

  • Une vue de type table avec des colonnes configurables.

  • Un état vide clair quand il n'y a pas encore d'élément.

  • Un bouton d'ajout unique ou plusieurs actions d'ajout, comme “Add link”, “Add group” et “Add theme toggle”.

  • Des actions de ligne pour éditer, supprimer, monter et descendre.

  • Une dialog interne d'édition d'élément avec Cancel et Save.

  • Des erreurs par ligne via validateItem.

  • Des erreurs globales via validateItems.

  • Le blocage du submit quand la collection est invalide.

  • La compatibilité avec la validation Zod du même formulaire.

  • L'édition de collections imbriquées via le composant exporté CollectionEditor.

Le champ collection est générique. Yayaw Table n'a pas besoin de connaître la signification métier d'un lien de menu, d'une fonctionnalité ou d'un élément de FAQ. Vous définissez les formes d'items avec createItem / createActions, vous rendez les contrôles métier avec renderItemForm, et vous imposez les règles avec les validateurs.

Modèle mental

La valeur du formulaire reste toujours la source de vérité.

  1. CollectionField lit la valeur courante depuis TanStack Form.

  2. Si cette valeur n'est pas un tableau, elle est normalisée en [].

  3. Chaque ajout, édition, suppression ou réordonnancement crée un nouveau tableau.

  4. Les modifications d'items créent de nouveaux objets au lieu de muter l'item existant.

  5. Le nouveau tableau est écrit via fieldApi.handleChange.

  6. Au submit, Yayaw Table valide la même valeur avec les validateurs collection et le schéma Zod configuré.

Il n'y a donc pas d'état JSON interne opaque à synchroniser avec votre application. Si la valeur du formulaire est { items_json: [...] }, l'éditeur collection est simplement une interface plus sûre pour ce tableau.

Exemple rapide

Cet exemple crée un petit éditeur de fonctionnalités avec ajout, édition, suppression, réordonnancement, validation par ligne et blocage du submit.

import { defineFormConfig } from "@/components/ui/yayaw-table/components/forms";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { z } from "zod";

const FeatureSchema = z.object({
  features: z.array(
    z.object({
      label: z.string().min(1),
      enabled: z.boolean(),
    })
  ),
});

export const featureForm = defineFormConfig({
  id: "features",
  schema: FeatureSchema,
  defaultValues: {
    features: [],
  },
  fields: [
    {
      type: "collection",
      name: "features",
      label: "Features",
      description: "Manage the product feature list.",
      addLabel: "Add feature",
      itemLabel: "feature",
      emptyLabel: "No features yet.",
      columns: [
        { id: "label", header: "Label" },
        {
          id: "enabled",
          header: "Enabled",
          render: (item) => (item.enabled ? "Yes" : "No"),
        },
      ],
      createItem: () => ({ label: "", enabled: true }),
      renderItemForm: ({ item, onChange, disabled }) => (
        <div className="space-y-3">
          <Input
            disabled={disabled}
            onChange={(event) =>
              onChange({ ...item, label: event.target.value })
            }
            value={String(item.label ?? "")}
          />
          <Switch
            checked={item.enabled === true}
            disabled={disabled}
            onCheckedChange={(enabled) => onChange({ ...item, enabled })}
          />
        </div>
      ),
      validateItem: (item) =>
        typeof item.label === "string" && item.label.length > 0
          ? []
          : ["Label is required"],
      validateItems: (items) =>
        items.length > 0 ? [] : ["Add at least one feature"],
    },
  ],
});

Référence API

CollectionFieldDefinition<TFormValues> étend la définition de champ classique avec les options d'un éditeur de tableau.

PropriétéRôle
type: "collection"Sélectionne le champ collection natif.
nameChemin du formulaire pour la valeur tableau.
label / descriptionTexte affiché au-dessus de l'éditeur.
disabledDésactive les actions collection et les contrôles du formulaire d'item.
addLabelLabel du bouton d'ajout par défaut.
itemLabelNom lisible de l'item utilisé dans les compteurs, titres de dialog et messages de validation.
emptyLabelTexte optionnel de l'état vide.
columnsColonnes de résumé. Une colonne peut lire item[column.id] ou fournir un render personnalisé.
createItem(items)Crée le nouvel item par défaut à partir des items courants.
createActionsListe optionnelle d'actions d'ajout nommées pour plusieurs types d'items.
getItemKey(item, index)Clé React stable optionnelle. Utilisez les IDs quand les items en ont.
renderItemFormRend le contenu de la dialog pour créer ou éditer un item.
validateItemRetourne les erreurs d'une ligne.
validateItemsRetourne les erreurs globales du tableau.
labelsSurcharge les labels intégrés comme Actions, Cancel, Save, Edit, Delete, Move up, Move down.
labelKeysClés de traduction pour ces mêmes labels intégrés.

renderItemForm reçoit un item contrôlé :

renderItemForm: (props: {
  item: Record<string, unknown>;
  index: number | null;
  disabled?: boolean;
  onChange: (item: Record<string, unknown>) => void;
}) => ReactNode;

Appelez onChange({ ...item, field: nextValue }) dès qu'un contrôle change. Ne mutez pas item directement.

Plusieurs types d'items

Utilisez createActions quand les utilisateurs doivent créer plusieurs formes d'items dans le même tableau.

const createLink = () => ({
  type: "link",
  label: "",
  href: "",
  placement: "primary",
  variant: "default",
  description: "",
  external: false,
});

const createGroup = () => ({
  type: "group",
  label: "",
  description: "",
  placement: "primary",
  items: [],
});

{
  type: "collection",
  name: "items_json",
  label: "Menu items",
  addLabel: "Add item",
  itemLabel: "menu item",
  columns: [
    { id: "label", header: "Label" },
    { id: "type", header: "Type" },
    { id: "placement", header: "Placement" },
  ],
  createItem: createLink,
  createActions: [
    { label: "Add link", createItem: createLink },
    { label: "Add group", createItem: createGroup },
    {
      label: "Add theme toggle",
      createItem: () => ({ type: "themeToggle", placement: "utility" }),
    },
    {
      label: "Add language toggle",
      createItem: () => ({ type: "languageToggle", placement: "utility" }),
    },
  ],
  renderItemForm: ({ item, onChange }) => {
    if (item.type === "group") {
      return <GroupItemForm item={item} onChange={onChange} />;
    }

    if (item.type === "link") {
      return <LinkItemForm item={item} onChange={onChange} />;
    }

    return <ToggleItemForm item={item} onChange={onChange} />;
  },
}

C'est suffisant pour modéliser un menu où les items top-level peuvent être des liens, des groupes, des toggles de thème ou des toggles de langue. Le champ collection orchestre seulement l'éditeur ; vos formulaires d'items décident des contrôles nécessaires à chaque type métier.

Collections imbriquées

Les collections imbriquées sont supportées avec le composant bas niveau CollectionEditor à l'intérieur d'un formulaire d'item. C'est utile pour les groupes de menu à un niveau : la collection principale édite les items de menu, et un item group contient une collection imbriquée pour group.items.

import { CollectionEditor } from "@/components/ui/yayaw-table/components/forms";
import { Input } from "@/components/ui/input";

function GroupItemForm({
  item,
  onChange,
}: {
  item: Record<string, unknown>;
  onChange: (item: Record<string, unknown>) => void;
}) {
  return (
    <div className="space-y-4">
      <Input
        onChange={(event) => onChange({ ...item, label: event.target.value })}
        value={String(item.label ?? "")}
      />
      <CollectionEditor
        addLabel="Add nested link"
        columns={[
          { id: "label", header: "Label" },
          { id: "href", header: "Href" },
        ]}
        createItem={() => ({
          type: "link",
          label: "",
          href: "",
          placement: "primary",
          variant: "default",
          external: false,
        })}
        emptyLabel="No nested links yet."
        itemLabel="nested link"
        label="Nested links"
        onChange={(items) => onChange({ ...item, items })}
        renderItemForm={({ item: nestedItem, onChange: onNestedChange }) => (
          <div className="space-y-3">
            <Input
              onChange={(event) =>
                onNestedChange({
                  ...nestedItem,
                  label: event.target.value,
                })
              }
              value={String(nestedItem.label ?? "")}
            />
            <Input
              onChange={(event) =>
                onNestedChange({
                  ...nestedItem,
                  href: event.target.value,
                })
              }
              value={String(nestedItem.href ?? "")}
            />
          </div>
        )}
        validateItem={(nestedItem) =>
          nestedItem.type === "link" ? [] : ["Only links are allowed"]
        }
        value={item.items}
      />
    </div>
  );
}

Le détail important est le onChange imbriqué : il écrit le prochain tableau imbriqué dans l'item parent avec onChange({ ...item, items }).

Couches de validation

Les collections ont trois couches de validation qui peuvent fonctionner ensemble :

  1. validateItem(item, index) retourne les erreurs d'une ligne. Ces erreurs sont visibles sous la ligne et dans la dialog d'item.

  2. validateItems(items) retourne les erreurs du tableau entier, par exemple “Add at least one item”.

  3. Le schema du formulaire valide toujours la valeur finale avec Zod.

validateItem et validateItems sont branchés à TanStack Form comme validateurs du champ. Si l'un des deux retourne des erreurs, form.handleSubmit() n'appelle pas l'action de submit.

Utilisez les validateurs collection pour les messages métier et UI, et gardez Zod comme contrat structurel final. Par exemple :

validateItem: (item) => {
  if (item.type === "link" && !item.href) {
    return ["Href is required"];
  }

  if (item.type === "group") {
    const nestedItems = Array.isArray(item.items) ? item.items : [];
    return nestedItems.every(
      (nestedItem) =>
        typeof nestedItem === "object" &&
        nestedItem !== null &&
        "href" in nestedItem
    )
      ? []
      : ["Group items must be links"];
  }

  return [];
},
validateItems: (items) =>
  items.length > 0 ? [] : ["Add at least one menu item"],

Formulaires en modal et drawer

Les champs collection fonctionnent dans les mêmes surfaces CatalogueForm que les autres champs. Ils peuvent être utilisés dans le drawer latéral par défaut ou dans une modal plus large :

defineTableConfig({
  form: {
    layout: {
      mode: "modal",
      width: "80vw",
    },
  },
});

Utilisez une modal quand les formulaires d'items contiennent plusieurs contrôles, des collections imbriquées ou des colonnes de résumé larges.

Traduction et labels

Les champs collection utilisent des labels anglais par défaut pour les actions intégrées :

  • Actions

  • Cancel

  • Save

  • Edit

  • Delete

  • Move up

  • Move down

Vous pouvez les surcharger champ par champ avec labels :

{
  type: "collection",
  labels: {
    actions: "Actions de ligne",
    save: "Enregistrer l'item",
    cancel: "Annuler l'édition",
  },
  // ...
}

Ou utiliser labelKeys si votre application résout les labels depuis les traductions :

{
  type: "collection",
  labelKeys: {
    actions: "menu.fields.items.actions",
    save: "menu.fields.items.save",
    cancel: "menu.fields.items.cancel",
  },
  // ...
}

Quand garder custom

Gardez type: "custom" quand le champ n'est pas un éditeur de tableau, quand il demande une mise en page complètement différente, ou quand il intègre un composant métier qui possède déjà son UX.

Utilisez type: "collection" quand la donnée reste un tableau d'objets et que l'application doit seulement définir :

  • Comment créer de nouveaux items.

  • Quelles colonnes résument les items.

  • Quels contrôles afficher dans le formulaire d'item.

  • Quelles règles rendent un item ou le tableau entier invalide.

Cette séparation garde le comportement générique ajouter/éditer/supprimer/réordonner dans Yayaw Table, tout en laissant les champs métier dans l'application consommatrice.

Pièges fréquents

  • Muter item ou items en place. Créez toujours un nouvel objet ou tableau avant d'appeler onChange.

  • Oublier un getItemKey stable quand les items ont des IDs durables. Le fallback par index fonctionne, mais les IDs gardent une identité de ligne plus claire.

  • Mettre les règles métier uniquement dans Zod. Utilisez aussi validateItem / validateItems quand l'utilisateur a besoin d'un feedback par ligne avant le submit.

  • Mettre un arbre métier profondément imbriqué dans un seul éditeur. Les collections imbriquées sont surtout adaptées à une sous-liste claire, comme les liens d'un groupe.

  • Reconstruire une collection avec type: "custom" alors que le comportement collection intégré couvre déjà le workflow tableau.

Docs liées

Champs générés et JSON

Déclarez les types standards une fois sur les colonnes ; la matrice des types décrit les éditeurs générés. Enregistrez un formulaire pour ajouter un schéma, des permissions, une mise en page, une relation ou un champ personnalisé. Le type: "json" est également disponible directement dans un catalogue de formulaires. Il conserve un brouillon invalide, affiche une erreur de validation et transmet la valeur JSON parsée après correction. Les sélections multiples conservent les valeurs de type chaîne, nombre ou booléen, y compris les choix existants inconnus.

Form blocks

Définissez TableConfig.form.blocks pour organiser les champs générés automatiquement sans les redéclarer. FormConfig.blocks d’un formulaire enregistré prend priorité sur les blocs du tableau ; les blocs prennent priorité sur sections. Les formulaires sans blocs gardent leurs sections. L’édition groupée conserve ses cases par champ et ne rend pas les blocs de création/édition.

form: {
  blocks: [
    { type: "content", id: "help", title: "Product", text: "Review before saving.", tone: "info" },
    {
      type: "section", id: "identity", title: "Identity", columns: 2,
      blocks: [
        { type: "field", name: "name" },
        { type: "field", name: "price" },
        { type: "custom", id: "preview", span: "full" },
      ],
    },
    {
      type: "actions", id: "tools",
      actions: [{
        id: "normalize", label: "Normalize name", variant: "outline",
        onClick: (context) => context.setFieldValue("name", String(context.values?.name ?? "").trim()),
      }],
    },
  ],
}
BlocConfiguration
fieldname référence un champ déclaré ou généré existant.
sectionid stable, title/description optionnels, columns: 1 | 2 | 3, blocks imbriqués.
contentid stable, text, title optionnel, tone: "default" | "info" | "warning". Le texte est échappé.
actionsid stable et liste actions.
customid stable, callback natif render(context) ou slot Vue form-{id}.

Chaque bloc accepte span: 1 | 2 | 3 | "full" ; la largeur est limitée aux colonnes du parent. Un formulaire étroit revient à une colonne. Chaque champ apparaît une fois : les références inconnues ou répétées sont ignorées, et les champs absents du layout sont ajoutés à la suite. Le catalogue de champs garde la visibilité, les permissions, la validation et la soumission ; contenus et boutons n’ajoutent aucune clé au payload.

Les actions possèdent id, label, onClick(context, signal), et peuvent définir labelKey, hidden, disabled, validate et variant (default, outline, secondary, destructive ; outline par défaut). Les prédicats hidden/disabled reçoivent le contexte. validate: true valide avant l’action sans soumettre implicitement. Une action en cours désactive son bouton et ignore les clics répétés. Les erreurs apparaissent dans le formulaire et le clic suivant réessaie. Fermer ou changer de ligne, désactiver ou masquer l’action annule son signal et empêche les écritures tardives dans les champs.

Le contexte des actions et blocs custom étend celui des champs avec les values courantes, setFieldValue(name, value), validate(): Promise<boolean>, submit(): Promise<void>, disabled, isSubmitting et isValidating. Le code asynchrone doit respecter l’AbortSignal fourni. Les boutons custom doivent utiliser type="button" et l’état disabled fourni. Les callbacks React retournent un nœud React ; ceux de Vue retournent des VNodes et peuvent être remplacés par le slot nommé présenté dans les formulaires Vue.

Utilisez titleKey, descriptionKey, textKey et labelKey avec FormConfig.translations.keys ou les traductions du provider. Les layouts contenant des callbacks restent dans le code applicatif. Les valeurs des champs et les snapshots des vues sont persistés séparément.

Consultation d’une entrée

Avec le rowClickMode par défaut, un clic sur une ligne ouvre la vue enregistrement et son menu d’actions de ligne porte une entrée de lecture seule Infos ; les champs dérivent des colonnes quand details n’est pas fourni. Les modes de clic explicites edit, link et none conservent leur comportement. Passez details={false} (:details="false" en Vue) pour retirer entièrement la surface de consultation intégrée. Le même composant RecordDetails s’affiche en panneau, en modale ou dans une page avec presentation: "drawer" | "modal" | "inline" ou l’objet responsive. Dans un tableau, TableConfig.presentation à la racine contrôle ensemble la consultation et les formulaires.

import type { RecordDetailsConfig } from "@/components/ui/yayaw-table/utils/record-details";

const details: RecordDetailsConfig = {
  presentation: "drawer",
  title: row => String(row.name),
  updatedAt: row => row.updatedAt as string,
  updatedBy: row => row.updatedBy as string,
  activity: row => row.audit as DetailActivity[],
  sections: [{
    id: "overview",
    title: "Informations générales",
    fields: [
      { id: "name", label: "Nom", type: "text" },
      { id: "budget", label: "Budget", type: "number",
        numberFormat: { style: "currency", currency: "EUR" } },
      { id: "active", label: "Actif", type: "boolean" },
    ],
  }],
};

<DataTable {...tableProps} details={details} onRevertActivity={revertActivity} />

Importez DetailActivity depuis le même module. Sans sections, les champs sont déduits des accesseurs, types et libellés d’options des colonnes. Des sections explicites peuvent inclure des champs absents du tableau. hidden exclut un champ des informations et des différences avant/après ; la visibilité des colonnes du tableau est indépendante de cette projection. L’application fournit uniquement les champs et les événements autorisés.

Tous les types de données du tableau et des formulaires intégrés sont pris en charge : texte, texte long, nombre, booléen, date/date-heure, choix, relations, images, liens, email, téléphone, fichiers, collections, code/JSON, valeurs dynamiques et champs personnalisés. Les mots de passe sont masqués ; 0, false et une valeur absente restent distincts. Les relations acceptent des objets avec label/name ou des identifiants résolus par les options fournies ; l’application charge les options distantes. Les fichiers utilisent { name, url } et les collections conservent toutes les données de chaque élément. Le composant React autonome accepte renderField(field, value, row) ; retournez undefined pour le rendu par défaut. Vue propose les slots #detail-<field-id> dans le tableau et le composant autonome. Les libellés français et anglais sont intégrés ; adaptez-les avec locale et details.labels.

Modifier ouvre le formulaire existant. Supprimer nécessite le gestionnaire d’action et les permissions du tableau/de la ligne, ouvre une modale nommant l’entrée et place initialement le focus sur Annuler. Les actions en cours bloquent les doubles envois. Un échec conserve la confirmation pour réessayer ; un succès ferme la fiche et actualise le tableau. Le composant React autonome utilise onClose, onEdit, onDelete, onDeleted et onReverted ; Vue utilise canEdit, canDelete, onDelete et les événements close, edit, deleted, reverted. Montez les fiches autonomes avec une clé stable propre à l’entrée.

Historique conservé et annulation

L’application gère l’historique. activity(row) fournit les événements avec id, actor: { name }, at, action et éventuellement changes: [{ field, before, after }]. updatedAt et updatedBy sont des métadonnées indépendantes de l’entrée. Les composants ne chargent ni ne stockent l’historique.

onRevertActivity(row, event) active l’annulation et retourne { success: boolean, error?: string }. details.canRevert(event, row) permet de la restreindre ; reversible: false la désactive pour un événement. La création sans différence de champs et les événements d’annulation ne sont pas annulables.

Une annulation restaure les valeurs et ajoute un nouvel événement portant reverts: originalEvent.id, l’auteur authentifié et les valeurs avant/après inversées. La ligne d’origine reste présente et porte le badge Annulée. L’interface bloque les répétitions et les modifications suivies d’événements fournis plus récents touchant les mêmes champs. Le serveur doit encore vérifier les permissions, la version actuelle de l’entrée et les conflits, puis restaurer les valeurs et ajouter l’événement dans une seule transaction. Ne faites jamais confiance à un auteur fourni par le client et ne supprimez pas l’événement initial. Un échec conserve l’original et affiche son erreur. Actualisez la ligne et l’historique canoniques avant de résoudre le callback, ou via onReverted / @reverted ; le succès du callback seul ne peut pas ajouter la ligne à l’interface.

L’exemple partagé de campagne contient 30 champs couvrant tous les types actuels et trois présentations. Modifier, Supprimer, Réinitialiser et Annuler agissent sur des données fictives en mémoire. Dans le dépôt Table, consultez examples/record-details-react.tsx et lancez bun run vue:dev, puis ouvrez /?example=record-details pour l’exemple Vue.

Tester les comportements dans l’exemple Yayaw

L’exemple interactif ouvre la fiche d’un produit depuis l’action Voir ou le clic Activer. Choisissez Latéral, Modale ou Dans la page dans Présentation de la fiche. Les autorisations de modification et de suppression s’appliquent aussi dans la fiche ; la suppression demande toujours une confirmation.

L’exemple propose des réglages enregistrés dans l’URL :

RéglageParamètre URLValeur par défaut
Fiche de consultationexcfg-details1
Présentationexcfg-detail-presentationdrawer (modal / inline)
Historique videexcfg-empty-activity0
Annulation des modificationsexcfg-undo1
Premier produit en lecture seuleexcfg-lock-record0
Simuler une erreurexcfg-mutation-error0
Simuler une réponse lenteexcfg-mutation-delay0

Une case cochée écrit 1, une case décochée écrit 0. Copiez l’URL pour partager une configuration. Les réglages de modification et de suppression déjà définis dans le CMS conservent leurs paramètres ; sinon, ils utilisent excfg-edit et excfg-delete. Réinitialiser les réglages restaure les valeurs configurées et efface ces paramètres. Réinitialiser les données restaure les produits et leur historique fictif.

Chaque produit possède au départ une modification de prix attribuée, immédiatement annulable. Les modifications par formulaire, en ligne et en masse ajoutent les valeurs avant/après et actualisent l’auteur et la date. Une annulation ajoute un événement qui référence l’original et le conserve. Les annulations répétées ou devenues obsolètes sont refusées. Le réglage Historique vide change seulement l’aperçu et conserve les événements de la démo. Une réponse lente retarde les mutations de 1,5 seconde. Une erreur simulée laisse les données intactes : fermez la fenêtre si nécessaire, désactivez le réglage, puis réessayez.

Ces réglages concernent uniquement l’exemple dans l’interface ; ils ne définissent ni des feature flags gérés ni des autorisations de production. Les produits, l’historique et les modifications restent en mémoire et se réinitialisent au rechargement. Les pages CMS déjà publiées reçoivent les nouveaux réglages sans remplacer leur contenu éditorial.