@msbci/form-editor
@msbci/form-editor
Visual form builder — 3-panel editor with drag & drop. Create and edit forms defined by @msbci/form-core schemas.
Installation
npm install @msbci/form-editor @msbci/form-renderer @msbci/form-core
Usage
import { FormEditor } from '@msbci/form-editor'
<FormEditor
theme={myTheme}
dataSources={myConnectors}
adapter={{
loadForm: (id) => api.getForm(id),
saveForm: (form) => api.saveForm(form),
loadFormTypes: () => api.getFormTypes(),
saveFormType: (type) => api.saveFormType(type),
}}
onChange={(schema) => console.log('Form updated', schema)}
onSave={() => console.log('Saved')}
/>
IEditorAdapter
The editor makes no direct API calls. All persistence goes through the adapter:
interface IEditorAdapter {
loadForm?: (id: string) => Promise<IFormDefinition>
saveForm?: (form: IFormDefinition) => Promise<void>
loadFormTypes?: () => Promise<IFormType[]>
saveFormType?: (type: IFormType) => Promise<void>
}
Pass adapter to FormEditor — the host app controls all backend communication.
Features
| Feature | Description |
|---|---|
| 3-panel layout | Toolbox (left) / Canvas (center) / Properties (right) |
| Drag & drop | Drag fields from toolbox to canvas pages (dnd-kit) |
| Undo / Redo | 50-step history with action descriptions |
| ConditionBuilder | Visual AND/OR condition constructor with 15 operators |
| DataSourceSelector | Select connector + map dependency keys to variables |
| Live preview | Switch to Preview mode to test with FormRenderer, in either of two modes (see below) |
| JSON view | Inspect raw form schema in the editor |
| Export / Import | JSON export with schema version envelope |
| Zustand store | Full CRUD: pages, rosters, variables, selection, view |
Les deux apercus (v1.12.0+)
L'onglet d'apercu porte une bascule.
- Rendu reel (par defaut) : le formulaire se comporte comme pour l'utilisateur qui le remplit — conditions evaluees, pages a franchir dans l'ordre, validation exigee.
- Mise en page : toutes les pages et tous les champs sont affiches, aucune condition n'est evaluee, la navigation est libre. C'est le mode du concepteur qui veut voir la disposition de la quatrieme page sans remplir les trois premieres.
L'editeur ne rend pas le formulaire : il passe designPreview au
PreviewComponent que l'hote lui a donne. La propriete est facultative —
@msbci/form-renderer >= 1.15.0 la comprend directement, et un composant
d'apercu ecrit avant ce lot l'ignore sans rien casser.
// Le renderer accepte la propriete telle quelle.
<FormEditor adapter={adapter} PreviewComponent={FormRenderer} />
// Composant d'apercu maison : il suffit de la transmettre.
function Preview({ formSchema, designPreview }) {
return <FormRenderer formSchema={formSchema} designPreview={designPreview} theme={myTheme} />
}
ConditionBuilder
Build conditions visually — no expression syntax required:
- Choose action: Show, Hide, Validate, Require, Read-only, Set Value
- Add rules: Variable → Operator → Value
- Combine with AND / OR logic
- Expression preview auto-generated
- Error message field for validation rules
- Target variable + value for setValue actions
DataSourceSelector
Connect select/multiselect/radio fields to registered data sources:
- Dropdown of available
IDataSourceConnectorinstances - Add dependency keys (e.g.
parentId,countryId) - Map each key to a form variable
- Cascading updates handled automatically by the renderer
Theme
Uses the same IMosobiTheme system as @msbci/form-renderer:
<FormEditor theme={myTheme} dataSources={connectors} adapter={adapter} />
v1.15.0
Un tableau ne pouvait pas etre deplace vers une autre page. Un champ et un panneau le pouvaient, un tableau non : le canevas a glisser-deposer n'a jamais traduit ce geste pour un conteneur.
- Cause. Le gestionnaire de glissement declarait
moveItem— reordonner dans une page — etmoveVariable— deplacer entre pages, mais pour un champ seulement. Un panneau echappait au defaut parce qu'il est un champ dans le modele. Un tableau, lui, ne rencontrait aucune branche : depose sur une autre page, il retombait dans le cas des champs, que son type excluait, et le geste se terminait sans rien appeler. Meme asymetrie que la suppression corrigee en 1.13.0, et pour la meme raison. - Correctif. Nouvelle action
movePageItemToPage(fromPageCode, toPageCode, itemCode, index?). Elle porte n'importe quel element de premier niveau — tableau, panneau, champ — avec tout son contenu, renumerote les deux pages, et se defait par une seule annulation. Elle rend les references que le deplacement fait basculer en avant ; un appelant qui ignore ce retour reste correct. - Le glissement seul ne suffisait pas. Sur un formulaire de huit pages, la page d'arrivee est hors ecran : tirer un tableau de la page 2 a la page 7 suppose un defilement pendant le glissement. Le panneau de proprietes porte donc un choix « deplacer vers la page... », la ou vivent deja les gestes structurants sur l'element selectionne, et qui reste visible quel que soit le defilement du canevas. Reperes de test
roster-move-to-pageetvariable-move-to-page; nouveau composant exporteMoveToPageField. - La reference devenue inatteignable est nommee, pas devinee. Deplacer un champ derriere la condition qui l'interroge n'est signale nulle part ailleurs : ni le validateur — le code existe toujours — ni le rendu. Le panneau affiche le constat sous la liste (
variable-move-warning,roster-move-warning), sans bloquer le deplacement. - Les positions ne derivent plus.
moveVariablelaissait l'orderd'origine en place cote source et recopiaitnewOrdertel quel cote cible : la page de depart gardait des trous, la page d'arrivee accumulait desorderen double, et l'ordre d'affichage changeait d'une relecture a l'autre. Les deux listes sont desormais renumerotees. - Un champ regroupe dans un panneau peut changer de page : il en sort et rejoint le premier niveau de la page choisie. Une colonne de tableau n'expose pas le choix — elle appartient au gabarit de ligne et suit son tableau.
- Deux reglages d'affichage sur les proprietes d'un tableau : masquer le titre de chaque instance (le libelle repete suivi du numero), et, une fois masque, afficher un numero de ligne a gauche. Le second n'apparait que si le premier est actif, et reafficher le titre le retire : le modele ne porte pas de combinaison sans usage. Les reglages ne sont proposes que sur les variantes rendues en cadres — le tableau etendu numerote deja ses lignes, une variante a options porte le libelle de l'option sur chaque ligne. Reperes
roster-hide-instance-titleetroster-show-row-number. - Pourquoi les tests ne l'ont pas vu.
moveVariableetait couverte et passait. Aucun test ne verifiait qu'un tableau atteignait une action de deplacement entre pages : la couverture du magasin donnait le change, exactement comme pour la suppression en 1.13.0. - Impact : additif. Cinq libelles s'ajoutent au paquet de traductions, remplis par les valeurs par defaut pour un hote qui ne surcharge rien ;
movePageItemToPageest facultative surDragEndActions, un hote qui compose lui-memeDndManageravec des actions anterieures reste fonctionnel. Un hote qui n'appelle rien de nouveau retrouve le comportement anterieur, aux positions corrigees pres. 23 tests ajoutes, 286 passants.
v1.14.0
Un conteneur ne pouvait etre recopie qu'a la main, element par element. Ce lot pose une icone de duplication sur l'en-tete d'un tableau, d'un panneau et d'une page.
- Le geste vit sur l'en-tete du conteneur, la ou le concepteur le lit, et non dans le panneau de proprietes : recopier quatre rubriques de meme profil se fait d'affilee, sans passer par une selection a chaque fois. Reperes de test
canvas-page-duplicate-<code>,canvas-roster-duplicate-<code>,canvas-panel-duplicate-<code>; libellescanvas.duplicatePage,canvas.duplicateRoster,canvas.duplicatePanel. - Deux actions nouvelles sur le magasin :
duplicatePage(pageCode)etduplicatePageItem(pageCode, itemCode). Elles delegent la duplication a@msbci/form-core— codes generes uniques a l'echelle du formulaire, references internes reecrites vers la copie, references sortantes intactes — et se defont par l'annulation comme toute autre modification. - La copie atterrit juste apres l'original et devient l'element selectionne, pour que le concepteur puisse la renommer immediatement dans le panneau de proprietes.
- Le clic ne declenche plus un deplacement. L'en-tete d'un tableau et celui d'un panneau portent le capteur de glissement : l'evenement de pointeur du bouton est retenu, faute de quoi le clic aurait ete pris pour un debut de deplacement. L'intitule reste saisissable pour reordonner, le bouton lui est exterieur.
- Impact : additif. Trois libelles s'ajoutent au paquet de traductions, remplis par les valeurs par defaut pour un hote qui ne surcharge rien ; aucune signature existante modifiee. Un hote qui n'appelle pas les nouvelles actions retrouve exactement le comportement anterieur, a l'icone pres. 11 tests ajoutes, 263 passants.
v1.13.0
Un tableau ne pouvait pas etre supprime. Un champ et une rubrique le pouvaient, un tableau non : aucune commande de l'interface par defaut n'appelait l'action de suppression du magasin, qui existait pourtant et fonctionnait.
- Cause. Le canevas historique portait une croix de suppression sur l'en-tete d'un tableau. Le canevas a glisser-deposer, devenu celui par defaut, ne l'a jamais eue. Une rubrique n'a pas ete touchee parce qu'elle est un champ : sa suppression vit dans les proprietes du champ, atteignable comme avant. Un tableau, lui, n'avait de commande nulle part. Le refus n'etait pas silencieux : rien n'etait appele.
- Correctif. Les proprietes d'un tableau portent un bouton de suppression, au meme endroit et de la meme forme que celui d'un champ ou d'une page. Repere de test
roster-delete, libelleproperties.deleteRoster. Les quatre variantes se suppriment : cases a cocher, liste, collection, collection etendue. - La selection ne designe plus un element disparu. Supprimer un tableau la ramene sur sa page, comme le font deja la suppression d'un champ et celle d'une page. Sans cela, une selection pointant sur le tableau supprime — ou sur l'un de ses champs — survivait a l'arbre.
- Pourquoi les tests ne l'ont pas vu. L'action
removeRosterdu magasin etait couverte, et elle passait. Aucun test ne verifiait qu'une commande de l'interface l'atteignait. La couverture du magasin donnait le change. - Impact : additif (
properties.deleteRoster; les libelles se passent en fragment, un hote qui ne le fournit pas recoit celui par defaut). 9 tests ajoutes, 252 passants.
v1.12.0
- Deux apercus au lieu d'un. L'onglet d'apercu porte une bascule : rendu reel, le comportement d'un utilisateur qui remplit le formulaire, et mise en page, qui affiche tout sans evaluer une seule condition. Le concepteur verifie ainsi la disposition de la quatrieme page sans avoir a remplir les trois premieres. Le rendu reel reste le mode actif au depart.
- Le contrat de
PreviewComponentgagne une propriete facultativedesignPreview.@msbci/form-renderer>= 1.15.0 l'accepte telle quelle : un hote qui passePreviewComponent={FormRenderer}obtient les deux modes sans rien ecrire. Un composant d'apercu ecrit avant ce lot ignore la propriete et se comporte comme avant — la bascule reste alors sans effet, mais rien ne casse. - Trois libelles ajoutes :
formEditor.previewModeLabel,formEditor.previewModeReal,formEditor.previewModeDesign(FR et EN). Reperes de testpreview-mode-toggle,preview-mode-real,preview-mode-design. - Impact : additif. 4 tests ajoutes, 243 passants.
v1.11.1
- Republication de suivi. Aucun changement de code ni de comportement : le paquet est republie pour dependre de
@msbci/form-corev1.13.0, qui ajoute le typeIStoredFileet les lecteurs bivalents de pieces jointes. L'editeur n'en fait aucun usage — il concoit des formulaires, il n'en remplit pas. - Le paquet publie empechait le processus Node qui l'importait de se terminer. L'obfuscation posait une protection anti-debogage a intervalle, c'est-a-dire un
setIntervaljamais arrete : la boucle d'evenements restait occupee, et tout script, outil en ligne de commande ou tache d'integration continue qui importait le paquet restait suspendu jusqu'a son delai d'expiration, sans message et sans cause visible. La protection est retiree — elle vise la console d'un navigateur et ne protegeait rien sur une cible Node, tandis que les instructionsdebuggerqu'elle injectait interrompaient le debogage legitime d'un consommateur. Ce qui rend le code couteux a relire est conserve : tableau de chaines encode, aplatissement du flot de controle, code mort, renommage des identifiants. Le correctif est commun aux cinq paquets. 239 tests passants, inchanges.
v1.11.0
- Un enregistrement refuse ne laissait aucune trace a l'ecran. Le rejet de promesse partait dans le vide : le concepteur voyait le bouton reprendre son etat normal et repartait en croyant son travail enregistre, alors que le serveur l'avait refuse — parce qu'un collegue avait enregistre entre-temps, ou parce que le remaniement faisait disparaitre un champ deja repondu. Nouvel etat de store
saveErroret nouvelle boite de dialogueSaveErrorDialog, montee parFormEditor, qui dit le refus et sa cause. - Face a un enregistrement concurrent, rien n'est recharge d'office. Recharger effacerait precisement le travail qui vient d'etre refuse. La boite explique que quelqu'un d'autre a enregistre, date la version concurrente, rappelle que les modifications sont toujours a l'ecran, et propose trois issues : telecharger ses modifications en JSON (rien n'est perdu, la reprise reste possible), recharger la version enregistree apres confirmation explicite, ou continuer sans enregistrer. Le formulaire local n'est jamais touche sans un geste du concepteur.
- La version du formulaire fait l'aller-retour. Le formulaire charge porte
updatedAt(voir@msbci/form-corev1.12.0) et l'editeur le renvoie tel quel a l'enregistrement.IEditorAdapter.saveFormpeut desormais renvoyer{ updatedAt }— la valeur que le serveur vient d'ecrire — pour enchainer les enregistrements sans recharger. Un adaptateur qui ne renvoie rien reste valide : l'editeur oublie alors la version detenue et le controle ne reprend qu'au prochain chargement, plutot que de refuser a tort un enregistrement legitime. - Ce que l'adaptateur doit propager. L'editeur reconnait un conflit de version au
codeFORM_VERSION_CONFLICTporte par l'erreur levee, et lit l'heure de la version concurrente dans son tableauerrors. Un adaptateur qui ne transmet que le message affiche le refus tel quel, sans les issues propres au conflit — mieux que le silence d'avant, moins bien qu'un adaptateur a jour. - Nouvelles actions de store :
clearSaveError()etreloadFromServer(). Nouveaux libellessaveDialog.*, en francais et en anglais, overridables partiellement comme tout le dictionnaire. - Impact : additif (un etat et deux actions de plus, un type de retour elargi sur
saveForm, une section de libelles de plus). Un hote qui ne change rien voit desormais les refus au lieu de les ignorer, ce qui etait le defaut. 8 tests ajoutes, 239 passants.
v1.10.0
- Le plafond de taille d'une piece jointe etait hors de portee du concepteur. Les blocs « Configuration fichier » et « Configuration image » reglaient le nombre de pieces et les types acceptes, mais pas leur poids : seul le plafond par defaut pouvait s'appliquer. Nouveau champ Poids maximal par fichier (Mo) dans les deux blocs (
data-testidfile-config-max-bytesetimage-config-max-bytes), qui ecritfileConfig.maxBytes/imageConfig.maxBytesen octets. Laisse vide, le champ rappelle sous lui le plafond applique par defaut, sans quoi le concepteur croirait n'avoir aucune limite. - Libelles :
properties.fileConfigMaxSizeetproperties.fileConfigMaxSizeHint(defaultSize)s'ajoutent au dictionnaire, en francais et en anglais. Comme tout libelle, ils s'overrident partiellement : un hote qui n'en fournit pas retombe sur le dictionnaire par defaut. - Impact : strictement additif (un reglage de plus sur deux blocs existants, deux libelles de plus). Aucun formulaire existant modifie a l'ouverture. 5 tests ajoutes, 231 passants.
v1.9.0
- Le concepteur ne proposait aucun champ de signature. Le type
'signature'figure desormais dans la boite a outils (groupe « Avance ») et dans le selecteur de type du panneau de proprietes, avec son panneau de reglages dedie : forme de saisie (acceptation nominative horodatee ou trace manuscrite), texte d'engagement multilingue, case d'acceptation exigee ou non, et plafond du trace en Ko en mode manuscrit. - Le panneau rappelle la portee du champ, la ou la decision se prend. Un concepteur qui pose une signature dans une piece administrative doit savoir, au moment ou il la pose, que le champ enregistre un engagement nominatif horodate et n'est pas une signature electronique qualifiee : il n'atteste ni l'identite du signataire, ni l'integrite du document. Le rappel est affiche dans le panneau, traduisible comme le reste (
labels.properties.signatureDisclaimer). - Le rapport de coherence couvre desormais la reference morte. Le controle de conception remonte les codes dupliques depuis la v1.7.1 ; ce lot ajoute la verification de bout en bout d'une expression referencant un code inexistant. Une reference morte ne declenche jamais sa condition et ne se voyait qu'a la saisie, sur un dossier deja depose.
- Impact : strictement additif — une entree de boite a outils, un panneau de reglages qui n'apparait que sur une variable de type
signature, des libelles nouveaux dans les deux langues. Aucun formulaire existant n'est modifie a l'ouverture. 7 tests ajoutes, 226 passants.
v1.8.0
- On ne pouvait plus rien ajouter sous un conteneur place en fin de page. Quand un panneau ou un tableau etait le dernier element d'une page, tout depot vise en dessous tombait dans le conteneur. Deux causes se conjuguaient. Le corps de page n'avait, sous son dernier element, qu'un remplissage de quelques pixels : il n'y avait litteralement rien a viser. Et l'arbitrage des collisions se faisait par
closestCenter, qui compare des centres : un conteneur haut a son centre plus proche que tout ce qui l'entoure, donc il emportait le depot meme lorsque le curseur visait nettement en dehors de lui. - Une zone de depot de fin de page, toujours visible et assez haute pour etre visee, ferme chaque page. Elle s'eclaire au survol, sert de repere de fin de page, et depose au niveau de la page quel que soit le conteneur qui la precede. Un element deja range dans un panneau ou un tableau et relache dessus en ressort ; un element de premier niveau y passe en derniere position. Son libelle passe par le mecanisme de libelles existant (
labels.canvas.pageEndDropZone), traduisible comme le reste. - L'arbitrage des collisions passe a
pointerWithin, avec repli surclosestCenterlorsque le curseur ne survole rien ou n'existe pas (deplacement au clavier). Seul ce qui est reellement sous le curseur est candidat, et l'imbrication est classee du plus proche au plus lointain : c'est la recommandation dednd-kitdes qu'il y a des conteneurs. La reciproque est preservee et testee — un depot vise sur un panneau ou un tableau y entre toujours. La fonction est exportee (editorCollisionDetection) pour un hote qui compose son propreDndContext. - Un nom de type de formulaire multilingue faisait echouer le rendu.
IFormType.nameest unLocalizedString: rendu tel quel dans la liste des types et dans le selecteur des proprietes du formulaire, un nom traduit s'affichait[object Object]ou faisait lever React. Il est resolu dans la langue par defaut du formulaire avant affichage. - Impact : additif.
OverDropDataaccepte un type de cible supplementaire ('page-end'),IEditorLabelsun libelle supplementaire — les libelles etant fusionnes a partir d'un objet partiel, un hote qui surcharge les siens n'a rien a changer. Un formulaire existant s'ouvre et s'edite a l'identique. 10 tests ajoutes, 219 passants.
v1.7.1
- Realignement sur
@msbci/form-core1.8.0. Le coeur porte desormais le moteur des champs calcules, la validation du formulaire complet et la correction des fonctions d'agregation sur les lignes incompletes. L'editeur n'a aucun changement fonctionnel dans cette version : la publication existe pour que l'editeur et le rendu partagent une seule et meme version du coeur, la dependance etant epinglee a l'exacte version publiee. 209 tests passants, inchanges.
v1.7.0
- Aucun moyen de delimiter une rubrique a l'interieur d'une page. Le panneau etait un bloc de texte que l'on posait entre deux champs ; regrouper six rubriques imposait six pages de navigation. Le canevas rend desormais le panneau comme un conteneur : ses champs apparaissent a l'interieur, sa zone de depot accepte un champ glisse depuis la boite a outils comme un champ deja pose sur la page, et un bouton ajoute un champ directement dans la rubrique. Un champ regroupe se ressort en le glissant sur la page, et se reordonne a l'interieur du panneau.
- Le refus se voit avant le relachement, pas apres. Un tableau, un panneau ou le panneau lui-meme glisse sur une zone de panneau la teinte en rouge et affiche la raison, et le depot ne produit aucune mutation. La regle est celle du modele (
SchemaValidatorde@msbci/form-core) : l'editeur ne doit pas pouvoir composer un arbre que la persistance refusera. Le magasin applique le meme controle, de sorte qu'un appel direct aaddVariableoumoveVariableToPanelavec une cible interdite laisse le formulaire inchange. - Surface du magasin etendue sans rupture.
addVariable,updateVariableetremoveVariableprennent un parametrepanelCodefacultatif, en dernier ; hors panneau, leur appel garde exactement la forme qu'il avait. Nouvelle actionmoveVariableToPanel(pageCode, variableCode, { panelCode?, index? }): elle couvre l'entree dans un panneau, la sortie (panelCodeabsent) et le reordonnancement interne, et retrouve seule le conteneur d'origine du champ. La selection porte unpanelCodeoptionnel, dont le panneau de proprietes se sert pour editer un champ regroupe. - Impact : strictement additif — parametres optionnels, une action nouvelle, aucun contrat existant modifie. Un panneau sans enfants s'affiche et se comporte comme avant. 16 tests ajoutes, 209 passants.
v1.6.0
- Les codes etaient verrouilles dans l'editeur. Le champ « Code » d'une page, d'un tableau et d'une variable etait rendu avec un
onChangevide : le concepteur subissait le code genere (TEXT_8WFK,ROSTER_7OGO) alors que ce code est ce qui designe l'element dans les conditions, les expressions, les exports et les circuits de validation en aval. Les trois champs sont desormais saisissables via le nouveau composantCodeField(exporte). - Refus explicites, jamais silencieux. La forme est deleguee a
checkCodeFormatde@msbci/form-core— la regle vit aupres de la grammaire qui la fonde plutot que d'etre redite ici — et le motif est affiche sous le champ : caracteres invalides, jeton reserve, suffixe de ligne ambigu, code vide. La saisie est normalisee en majuscules a la frappe, de sorte qu'un code valide se tape sans effort. Le suffixe numerique reste tolere sur une page et sur un tableau, qu'aucune expression ne designe. - Unicite signalee avant l'enregistrement. Le controle s'appuie sur le
SchemaValidatordeja utilise par le rapport de coherence de la barre d'outils, applique au formulaire tel qu'il serait apres renommage : un doublon supplementaire vaut refus, affiche a la frappe. Le concepteur ne decouvre plus la collision au moment d'enregistrer. - Les references suivent le renommage. Nouvelles actions de store
renameVariableCode,renameRosterCodeetrenamePageCode. La premiere passe parrenameVariableCodeInFormdu coeur et reecrit les sept emplacements qui citent le code. C'est ce qui rend la fonction utilisable : l'editeur enregistre l'arbre complet du formulaire, et ce chemin ne reecrit rien cote serveur — sans cette reecriture, renommer laisserait derriere lui des conditions pointant dans le vide. La selection courante est reportee sur le nouveau code pour que le panneau de proprietes ne perde pas sa cible, et le renommage entre dans l'historique (annulable). - Le code genere reste le defaut. Rien n'est demande a la creation d'un champ : un concepteur qui ne veut pas s'occuper des codes n'a rien a faire. Le code herite d'un modele de variable reste verrouille — il appartient au referentiel partage, le modifier localement detacherait la variable.
- Libelles : nouvelle section
codeEditing(regle de forme, quatre motifs de refus, message de collision), en francais et en anglais.labelsrestant unDeepPartial, un hote qui ne la fournit pas retombe sur les valeurs par defaut. - Impact : aucun contrat existant modifie, aucune prop rendue obligatoire. Un formulaire enregistre avant ce lot reste ouvrable, modifiable et rendu a l'identique. 9 tests ajoutes, 193 passants. Requiert
@msbci/form-core>= 1.6.0.
v1.5.0
- Rouvrir une condition en mode visuel la detruisait — perte de donnees. Cause :
parseExpressionToGroupetait un fragment inacheve qui retournait toujours un groupe vide ; la moindre interaction reconstruisait alors une expression vide et ecrasait la condition existante. Le seul garde-fou etait un avertissement consultatif. Correctif : le convertisseur s'appuie surparseConditionExpressionde@msbci/form-core. Une expression reconnue est restituee a l'identique dans le constructeur ; une expression non reconnue verrouille le mode visuel — elle est affichee, conservee telle quelle, et seul un bouton explicite (« repartir d'une condition visuelle vide ») permet de l'abandonner. Changer l'action d'une condition verrouillee ne touche plus a son expression. - Les regles de validation n'etaient pas editables. Seule la case « obligatoire » existait ; le seul point d'edition etait une zone de texte JSON brut reservee aux gabarits de bibliotheque. Nouveau composant
ValidationRulesEditor(exporte, avecavailableRuleTypes), cable dans le panneau de variable : longueur, bornes, motif, format, nombre minimum de fichiers, avec message d'erreur localisable. Le catalogue est filtre par type de variable et n'offre que les regles que le moteur applique reellement — une regle inerte n'est pas proposee. - Les conditions de page et de tableau n'avaient aucune interface. Nouveau composant
ConditionsField(exporte, avec le hookuseAvailableVariables), cable dansPagePropertiesetRosterProperties. Le constructeur de conditions, jusque-la reserve aux variables, est desormais atteignable pour une page et pour un tableau — dont le coeur sait maintenant evaluer les conditions. - Les options et les bornes de lignes d'un tableau n'etaient pas editables.
RosterPropertiesn'exposait que quatre champs sur huit. L'editeur d'options existant est cable pour les variantes a lignes fixes (check,list) — sans options, le rendu basculait silencieusement en mode pilote — et les bornescollectionConfig.min/maxsont editables pour les variantes a ajout libre. - Aucun controle de coherence a la conception. Le validateur de schema du coeur n'etait appele par aucun code de production. Nouveau bouton de barre d'outils
SchemaIssuesButton(exporte) : il affiche en continu le nombre d'incoherences et les detaille dans un panneau (code duplique, expression referencant un code inexistant, pilote qui ne designe rien, champ calcule sans expression, liste sans options). Il ne bloque jamais l'enregistrement — un brouillon incoherent reste enregistrable, c'est la publication qui doit etre exigeante. - Libelles : nouvelles entrees
properties.validationRule*,properties.rosterMinRows/rosterMaxRows/rosterRowsUnbounded,conditionBuilder.lockedNotice/lockedRewriteet la sectionschemaIssues, en francais et en anglais.labelsrestant unDeepPartial, un hote qui ne les fournit pas retombe sur les valeurs par defaut. - Impact : aucun contrat existant modifie, aucune prop rendue obligatoire. Un formulaire enregistre avant ce lot reste ouvrable, modifiable et rendu a l'identique. 12 tests ajoutes, 184 passants.
v1.4.0
- Les actions de condition « obligatoire si », « lecture seule si » et « definir la valeur si » produisaient une expression nue. Cause :
groupToExpressionn'enveloppait l'expression que pourshow,hideetvalidate. En face, le moteur ne reconnaissait pas la forme nue et retombait sur la visibilite : le concepteur choisissait « Obligatoire si » et obtenait « Afficher si » — le champ disparaissait quand la condition etait fausse. C'est exactement le motif « si oui, remplir ci-dessous » d'une notice administrative. - Correctif :
requireproduitrequire(...),readonlyproduitreadonly(...), etsetValueproduitsetValue(valeur, cible, condition)en consommant la cible et la valeur saisies dans le panneau. Modifier la cible ou la valeur reconstruit l'expression, qui restait auparavant perimee. - Le changement d'action ne declenche plus deux mises a jour successives, dont la premiere laissait l'expression desynchronisee de l'action.
- Impact : aucune prop ni signature publique modifiee. Les conditions deja enregistrees sous forme nue restent interpretees correctement par
@msbci/form-core1.4.0, qui decide sur l'action declaree. 4 tests ajoutes, 172 passants.
v1.3.15
- Bouton de suppression de champ —
VariablePropertiesexpose un bouton "Supprimer le champ" en bas du panneau, appelant l'actionremoveVariabledu store (annulable via undo). Évite d'avoir à viser la petite croix sur le canvas. Fonctionne aussi pour les champs liés à un template. Labelproperties.deleteVariableajouté (FR + EN). removeVariableréinitialise désormais la sélection sur la page lorsque le champ supprimé était sélectionné (cohérent avecremovePage).
v1.3.14
- Documentation : retrait des références à un hôte spécifique dans le changelog au profit d'une formulation générique (
l'application hôte). Aucun changement de code ni d'API.
v1.3.13
- Garde-fou retrait de scope —
FormPropertiesinterdit de retirer un scope d'un formulaire tant qu'une variable du formulaire référence un de ses templates (templateId) ou une de ses sources de données (dataSourceId). Le bouton×est désactivé avec une infobulle (labels.properties.scopeInUse) ; les scopes inutilisés restent retirables. Évite les références orphelines que la bibliothèque de variables ne peut plus résoudre. - Label
properties.scopeInUseajouté (FR + EN). - Aucun changement d'API.
v1.3.12
- Bouton de suppression de page —
PagePropertiesexpose un bouton "Supprimer la page" appelant l'actionremovePagedu store (annulable via undo). Labelproperties.deletePageajouté (FR + EN). - Création de scope sans tenant codé en dur —
ScopeManagerDialogn'envoie plustenantId: 'default': le serveur dérive le tenant du contexte de la requête, afin que le scope atterrisse dans le même tenant que les formulaires qui le référencent.IEditorAdapter.createScoperendtenantIdoptionnel (ajout non cassant).
v1.3.10 – v1.3.11
- Chrome thématisable — prop
themepropagée au chrome de l'éditeur ;theme.colors.primarycolore les accents (consommé par l'application hôte). - Prop
hideFormTypes— masque la section "Type de formulaire" du panneau de propriétés pour les intégrations qui ne l'utilisent pas. - Labels injectables selon la langue de l'application — l'hôte passe
labels(frLabels/enLabels) pour aligner l'éditeur sur la locale courante. - Republication corrigeant le protocole
workspace:*dans le tarball publié (installation autonome viapnpm publish).
v1.3.9
- Toolbox alignée sur
VariableTypecomplet —ToolboxSimpleetToolbox(DnD) listent désormais les 22 types sans omission. Ajouts :ToolboxSimple:listradio(Sélection),photo(Médias),panel+richtext(Avancé).Toolbox(DnD) :email(Texte),listradio(Sélection),photo(Médias),rating+panel+richtext(Avancé).
- Icônes assignées :
≡(listradio),📷(photo),▭(panel),📝(richtext) — cohérence avec le mix ASCII/émoji existant. - Tous les labels viennent de
labels.toolbox.types.*(clés déjà ajoutées en v1.3.8, FR + EN). - Aucun changement d'API ; 167 tests inchangés.
v1.3.8
VariablePropertiesaligné surVariableTypecomplet (form-core v1.3.3) — le<select>Type liste désormais les 22 types dans l'ordre Texte → Sélection → Date/Heure → Médias → Avancé. Conséquence : sélectionner une variable de typeimage(ouemail,photo,listradio,panel,richtext) affiche bien le bon type dans le panneau au lieu de retomber surtext(et le risque associé d'écraser le type via un clic involontaire est éliminé).- Labels FR/EN ajoutés pour
listradio,photo,panel,richtextdanslabels.toolbox.types.*. Single source of truth utilisée à la fois par Toolbox(Simple) et VariableProperties. - Aucun changement d'API, 167 tests inchangés.
v1.3.7
- Hotfix toolbox DnD : le type
imagen'apparaissait pas dansToolbox(DnD), bien que la variable soit fonctionnelle dans le renderer et exposée dansToolboxSimple. La section MÉDIAS contient désormais Fichier · Image · GPS dans les deux variantes ;fileetgpssont sortis de Avancé pour cohérence avec ToolboxSimple. - Aucun changement d'API, 167 tests inchangés.
v1.3.6
- Sections de configuration multi-upload dans
VariableProperties:- Section « Configuration fichier » (visible si
type === 'file') —maxFiles,minFiles,accept. Le selectmaxFilespropose[1, 2, 3, 5, 10, illimité]mappés sur[1, 2, 3, 5, 10, undefined]. - Section « Configuration image » (visible si
type === 'image') —maxImages,minImages, case à cocherallowCamera. - Les deux sections nettoient l'objet à chaque édition : un payload vide est remonté à
undefinedpour rester invisible côté schéma.
- Section « Configuration fichier » (visible si
- Labels FR/EN ajoutés :
properties.fileConfig*(6 clés) etproperties.imageConfig*(4 clés). - Pas de nouveau test côté editor (la couverture vit dans form-core et form-renderer). 167 tests inchangés.
v1.3.5
- ScopeManagerDialog — nouveau dialog modal qui pilote le CRUD des
IScope. Bouton "Scopes" ajouté dans la barre d'outils (data-testid="toolbar-scopes") entre et+ Page. Création (nom + code auto), édition inline du nom au clic, suppression avec confirmation. Si le serveur renvoie 409 (scope rattaché à des forms/templates/dataSources), le message d'erreur est affiché inline en rouge dans la zone de confirmation. - VariableTemplateManager — sous-dialog ouvert depuis le bouton "Gérer les variables →" de chaque scope (data-testid
scope-manage-variables-{code}). Rechargeadapter.loadTemplates(scopeId)au mount (liste authoritative du scope, indépendamment du formulaire courant). Updates optimistes après chaque mutation (pas de refetch). - Formulaire d'édition template progressif :
- Toujours visible :
code(read-only en édition),name(LocalizedField, read-only en édition),variableType,description,placeholder, flagsisRequired/isReadonly/isHidden. - Conditionnels :
OptionsEditorsitype ∈ [select, multiselect, radio, checkbox, listradio],expressionsicalculated,dataSourceId(dropdown alimenté paravailableDataSources) si[select, multiselect]. - Section repliable "Avancé" :
validationRules,conditions,styleen JSON (validation client avant submit).
- Toujours visible :
IEditorAdapterétendu :createScope,updateScope,deleteScope,createTemplate,updateTemplate,deleteTemplate— toutes optionnelles. Les patchesupdateTemplateexcluent typedef-sidecodeetname(immuables côté serveur).- Labels FR/EN :
toolbar.scopes,toolbar.manageScopesTitle, blocsscopeManager.*etvariableTemplateManager.*(titre paramétrable, confirmations, badges read-only, ARIA labels par row). - 16 nouveaux tests RTL (
scopeManager.test.tsx×8 +variableTemplateManager.test.tsx×8) — 167 tests passants.
v1.3.4
- DataSourceConfigManager — nouveau dialog modal (
/dialogs/DataSourceConfigManager.tsx) qui pilote le CRUD desIDataSourceConfigcôté admin. Liste filtrable par scope (badge de provenance), formulaire d'ajout/édition (code, name, URL, method, queryParam, value/label fields, headers password-masked, dépendances). Bouton "Sources" ajouté dans la barre d'outils (data-testid="toolbar-datasources"). IEditorAdapterétendu :loadDataSources(scopeId),createDataSource(scopeId, data),updateDataSource(id, data),deleteDataSource(id)— toutes optionnelles.- Store : nouveau
availableDataSources: IDataSourceConfig[]+ settersetAvailableDataSources(). Hors undo/redo (UI state). Chargé parFormEditorviaPromise.all(scopeIds.map(loadDataSources))+ dédup parcode(premier scope gagne, comme les templates). DataSourceSelectorlit désormais le store en plus de la prop legacyavailableConnectors— les configs admin l'emportent sur les connecteurs hardcodés du host.- Labels FR/EN :
toolbar.dataSources,toolbar.manageDataSourcesTitle, bloc completdataSourceManager.*(titre dialog, formulaire, badges, confirmations). - 6 nouveaux tests RTL (
dataSourceConfigManager.test.tsx) — 151 tests passants.
v1.3.3
- Régression zone vide canvas corrigée — un clic sur la zone vide entre les pages déclenche désormais
select({ type: 'form' })et ouvre les propriétés du formulaire (gardee.target === e.currentTargetpour ne pas capturer les clics enfants) - Nouveau bouton Propriétés du formulaire dans la barre d'outils (header) — accès direct au panneau, indépendamment de l'état du canvas (
data-testid="toolbar-form-properties") - Multi-scopes :
IFormDefinition.scopeIds[]exposé via unScopesSelectordansFormProperties(ajout par dropdown, suppression par ×, réordonnement par ▲/▼ — l'ordre = priorité) - Toolbox Variables : affiche le message
noScopesiscopeIdsest vide ou absent FormEditor: chargement des templates viaPromise.all(scopeIds.map(loadTemplates))+ déduplication parcode(premier scope gagne)- Nouveau adapter optionnel
loadScopes?: () => Promise<IScope[]>+ étatavailableScopes(hors undo/redo) consommé par leScopesSelector - Labels FR/EN :
toolbar.formProperties,properties.scopes,properties.scopesHint,properties.noScopesAvailable,properties.moveScopeUp,properties.moveScopeDown,properties.removeScope - 10 nouveaux tests RTL (
scopesMigration.test.tsx) — 145 tests passants
v1.3.2
- Nouveau
ConditionsModal— édition des conditions dans un modal dédié (700 px, focus trap maison, fermeture uniquement via ✕ / Annuler / Appliquer ; l'overlay n'est volontairement pas cliquable) - Mode Saisie manuelle dans le modal :
<textarea>éditable avec coloration syntaxique sans dépendance externe (technique mirror textarea/div, tokenizer maison pourshow/hide/setValue/validate/require/readonly/ConditionEval,${VARIABLE}, opérateurs, strings, nombres) - Bug fix : la condition
ConditionEval(...)d'une règlevalidateest désormais éditable librement via le mode manuel — l'admin peut taper l'expression complète sans passer par le constructeur visuel - Toggle
[Constructeur visuel] [Saisie manuelle]— warning explicite si l'expression manuelle est trop complexe pour le builder VariableProperties: remplacement duConditionBuilderinline par un bouton « Configurer les conditions » + aperçu monospace des formules en lecture seule- 10 nouveaux tests RTL (
conditionsModal.test.tsx) — 641 tests passants
v1.3.1
DndManagerinterne —<FormEditor />activera désormais le DnD complet sans wrapper externe- Canvas DnD : drag-handle ⠿ dédié sur le header des pages (a11y + keyboard sensor)
- Canvas DnD : bouton « + Ajouter un champ au roster » + zone de drop sur le corps du roster
createDragEndHandler(actions, getForm)exporté comme factory pure (testable sans React)resolveTargetcorrigé :rosterCodetransmis correctement lors d'un drop sur un roster- Compat :
Toolbox/Canvas(DnD) deviennent les défauts ;ToolboxSimple/CanvasSimplerestent disponibles ;Wrapperdéprécié (warning console one-shot)
v1.3.0
- Section ROSTER dans la toolbox (4 types : check, list, collection, collection extended) — drag & drop
- Onglet Variables : référentiel par scope, recherche par code+nom, ajout par clic ou drag
- Variable liée : bannière , champs
code/namegrisés, surcharge locale par champ setVariableOverride,resetVariableOverride— double écriture overrides + miroir sur la variableaddVariableFromTemplate(pageCode, template, rosterCode?)— dénormalisation du templateIEditorAdapter.loadTemplates?: (scopeId) => Promise<IVariableTemplate[]>(optionnel)availableTemplates+setAvailableTemplates— état UI hors undo/redo- Bouton « + Add Roster » supprimé (la toolbox est désormais le seul point d'entrée)
IFormPage.items[]source de vérité pour le canvas — variables et rosters réordonnables ensemble viamoveItem- 34 nouveaux tests (115 total)
v1.2.0
LocalizedInput: saisie bilingue avec onglets FR/EN dans le panneau propriétésLocalizedField: wrapper label +LocalizedInput- Section « Langues » dans
FormProperties: configurerlangConfig, ajouter/retirer des langues - Confirmation avant suppression d'une langue (
stripLangFromForm) - DefaultLang verrouillée (UX safety net)
flattenLocalizedexporté publiquement- 15 nouveaux tests (81 total)
What's new in v1.1.0
ConditionBuildergains an optionalrosterContextprop. When provided, conditions are validated against the roster scope (variables in the same row, backward-jump detection, self-reference rules, circular dependencies) viaRosterConditionEnginefrom@msbci/form-corev1.1.0. Errors are surfaced inline under the condition preview.VariablePropertiesauto-detects when a variable lives inside a roster and forwards the scope context toConditionBuilder. No change required in host applications.- Aligned with
@msbci/form-corev1.1.0 (4 new variable types, extendedIFieldResponseMetadata,IFormRoster.collectionConfig).
License
Copyright (c) 2026 MOSOBI — All rights reserved. Commercial license required. Contact: dev@mosobi.com