Releases: klee-contrib/focus4
v9.6.4
v9.6.3
v9.6.2
Cette release corrige les issues suivantes #82, #84 et #89 (détail en cliquant dessus) et met à jour React en 16.5 (ainsi que TS en 3.1 en interne).
La résolution de #89 a été un peu plus loin puisque le Form(List)Node
propose maintenant une propriété form.errors
, qui contient un objet listant toutes les erreurs des champs composant le nœud de formulaire (y compris ceux des sous-nœuds). Cet objet d'erreur est rejeté par le service de sauvegarde s'il y a un erreur de validation, permettant de voir en un coup d'oeil dans la console pourquoi le **** de formulaire ne se sauvegarde pas ! (il était temps).
Remarque : Un nœud simple valide correspond à un objet d'erreur vide {}
, tandis qu'un nœud liste valide correspond à une liste d'objets vide [{}, {}, {}, ...]
. Les sous-nœuds valides n'apparaissent donc pas dans l'objet d'erreur.
v9
Avant de commencer
Numéro de version
Cette v9 aura été en cours de développement pendant plusieurs mois (premier commit le 01/12/2017), les versions betas se sont succédées, les numéros de versions ont monté, monté, pour arriver à la release finale qui portera le numéro 9.6.0. Les versions 9.0 -> 9.2 n'auront été que des versions de transitions vers la version complète de la nouvelle API de formulaires. La première version utilisée dans un vrai projet aura été la 9.3, la 9.4 a apporté la refonte du typage des stores avec TS2.8, la 9.5 a été la première bêta publique utilisée dans 3 projets. Pour marquer le coup (et aussi parce que la 9.6 apporte vraiment quelque chose sur la 9.5), on passera donc directement de 8.7 à 9.6.
Pas de rétrocompatibilité
Autant être clair dès le début : il n'est pas prévu de faire monter de version les projets existants en v8. Les changements proposés sont trop importants pour les mitiger facilement, et les projets existants sont trop avancés pour justifier une telle mise à jour.
La maintenance de la v8 est assurément toujours maintenue, selon les besoins projets, et les features demandées par les projets (et les bugfixes) seront toujours développées en premier dessus (puis rebasées sur la v9). 90% des changements sont contenus dans le module entity
, ce qui veut dire que tout ce qui n'y a pas attrait est facilement reportable d'une version à l'autre.
De plus, à l'initiative des projets, si une quelconque feature présentée ici intéresse quelqu'un en v8, il est tout a fait envisageable de la backporter pour en faire bénéficier le plus de monde.
Documentation
La documentation pour cette nouvelle version est à jour dans les différents readme du repo. Cette release note à pour vocation de présenter les changements v8 -> v9, si vous n'avez jamais fait de v8 et moins vous pouvez sauter la suite et lire directement la doc.
Mise à jour des dépendances (majeures)
La v9 apporte :
- React 16.x
- MobX 4.x
- Typescript 2.8+
React 16 ne change rien (du moins pour l'instant) à Focus et la mise à jour et tout à fait transparente (à part les warnings de peer dependencies sur React Toolbox, mais en pratique il n'y a aucun problème).
MobX 4 vient avec quelques changements d'API, qui ont changé quelques implémentations dans Focus mais qui devraient être transparentes dans les usages simples des projets.
Typescript 2.8 ne change rien mais apporte des nouvelles fonctionnalités de typages qui seront utilisées dans les stores.
Rappel des majeures précédentes
Pour rappel, les dernières majeures (enfin, pour lesquelles la dénomination "majeure" avait du sens) étaient :
- (1.)5.0 : Refonte de la recherche
- (1.)6.0 : Refonte du routeur
- 7.0 : Migration vers React-Toolbox/PostCSS/Variables CSS
- 8.0 : Retrait définitif de MDL
On remarque que ces majeures ont touché à tous les composants clés de Focus à l'exception d'un seul : le formulaire. Et bien voilà, c'est l'heure d'y faire un tour :)
Présentation
Motivation
Précédemment, un formulaire était entièrement encapsulé dans une classe de composant abstraite (AutoForm
), initialisée à partir un noeud de store (créé par makeEntityStore()
) et une définition de services (chargement/sauvegarde). AutoForm
avait pour vocation d'être une solution "clé en main" immédiate pour construire un formulaire, complet avec de la validation automatique, sur le modèle des usages établis par les précédentes versions de Focus. Cette dernière motivation a été une raison jusqu'ici suffisante pour simplifier l'implémentation du formulaire du côté framework, en créant un couplage fort entre AutoForm
, this.entity
et this.fieldFor
.
Le problème, c'est que l'usage forcé d'AutoForm
bloque ou complexifie certains usages parfaitement valides (exemple : des sous-formulaires) et repose sur une API qui n'est pas forcément la plus limpide (héritage). L'usage forcé de this.fieldFor
restreint la validation automatique aux seuls champs posés par cette méthode, rendant inutilement complexe une validation impliquant une quelconque sous entité (ou sur un formulaire basé sur une liste, le problème est le même). Oui, ces usages n'étaient pas non plus couverts par les versions antérieures, mais ce sont des besoins valides et qui ont déjà été implémentés plusieurs fois au dessus d'AutoForm
dans des projets. Une bonne partie de la problématique avait déjà automatiquement résolue avec la possibilité de créer un this.entity
à partir de n'importe quelle structure de données, donc c'était réellement dommage de bloquer sur des problèmes à priori moins complexes (spoiler alert : en vrai c'était carrément plus dur).
Solution adoptée
Pour créer un formulaire, il faut maintenant suivre les étapes suivantes :
- Créer un store d'entité avec
makeEntityStore
(inchangé) - Créer, à priori dans un composant, le noeud de formulaire (
FormNode
) à partir d'un noeud d'EntityStore
:- Il correspond à
this.entity
, vous êtes encouragés à garder la même convention de nommage - C'est une copie du noeud de base, observable de la même façon, réinitialisée à chaque fois que le noeud de base change (inchangé)
- Il contient maintenant, ainsi que chacun de ses sous nœuds, une référence vers son nœud équivalent de base dans
sourceNode
et une méthodereset()
pour se réinitialiser dessus (nouveau, sauf pour la racine qui avait déjàreset()
) - Il contient le résultat de validation des différents champs contenus dans le
FormNode
(nouveau) :- Chaque
EntityField
est muni d'une propriétéerror
(la même qui était dans leField
avant) - Le
FormNode
, ses sous-nœuds, ainsi que chaqueEntityField
sont muni d'une propriétéform.isValid
(isValid
pourEntityField
), résultat de la validation des champs contenus dans ce nœud (ou juste du champ).
- Chaque
- Il contient l'état d'édition du
FormNode
et de tous ses constituants (nouveau). A un endroit donné, la valeur deisEdit
(node.form.isEdit
oufield.isEdit
) est l'intersection de :- La valeur du
isEdit
du parent (sauf la racine qui n'en est pas, naturellement) - La valeur du
isEdit
propre (modifiable) - Une éventuelle condition supplémentaire, sous forme de dérivation.
- Note : l'état principal est initialisé à
false
et tous les sous-états àtrue
, ce qui conserve le principe existant duisEdit
qui pilote toute l'édition.
- La valeur du
- Il correspond à
- Créer, à priori dans un composant, les actions du formulaire (
FormActions
), à partir d'un noeud issu d'unFormNode
(donc pas forcément le noeud en entier) et d'une définition de services :- C'est une extraction des méthodes d'
AutoForm
dans un objet séparé (rien de nouveau) - Il contient les méthodes suivantes :
load()
, muni du hookonFormLoaded()
save()
, muni du hookonFormSaved()
toggleEdit()
, muni du hookonToggleEdit()
clean()
, équivalent de ce qu'il y avait danscomponentWillUnmount()
- Il expose les propriétés suivantes :
panelProps
, les props à passer à unPanel
(inchangé)formProps
, les props à passer à unForm
(voir ci-dessous) (nouveau)
- C'est une extraction des méthodes d'
- Poser dans le rendu d'un composant le composant
Form
(nouveau)- Il pose le formulaire HTML (si
hasForm = true
, qui est le défaut) - S'il reçoit ses props depuis un
FormActions
, il appelera :load()
aucomponentWillMount()
save()
à la validation du formulaireclean()
aucomponentWillUnmount()
- Il s'agit une fois encore d'une extraction d'
AutoForm
, cette fois-ci de la partie "composant"
- Il pose le formulaire HTML (si
- (optionnel, inchangé) Poser un
Panel
et lui donneractions.panelProps
.
Remarque importante : L'ajout de la validation et de l'état d'édition ne concerne bien que le FormNode. Un nœud d'EntityStore
de base est totalement inchangé par cette nouvelle version. Par la suite, à chaque fois qu'on mentionne l'un ou l'autre sur un champ ou un nœud, ce qui est dit ne s'applique que s'il est question d'un FormNode
.
Nouveautés
node.replace
/ listNode.replaceNodes
Les nœuds simples (et l'EntityStore
lui-même) sont maintenant munis d'une méthode replace
qui remplace tout le contenu du store par l'intégralité du contenu qui lui est passé, contrairement à set
qui ne remplace que les propriétés renseignées. Cela correspond à peu près à faire clear()
puis set(data)
, à la différence près que c'est fait en une seule opération (ce qui marche beaucoup mieux dans un autorun
). Le fonctionnement de set
et de replace
est inversé : set
parcourt l'objet passé et met à jour en conséquence le nœud, alors que replace
parcourt le nœud et se met à jour avec ce qui lui a été passé (et si une propriété est vide, alors le champ est vidé).
Les nœuds de listes ont une méthode équivalente replaceNodes
, (replace
existe déjà pour l'array de StoreNode lui même), qui est en fait la même chose que ce qu'était listNode.setNodes
(anciennement set
, renommé pour l'homogénéité du nommage). Ce dernier a maintenant le même fonctionnement que set
sur un nœud simple, en mettant à jour les nœuds avec les propriétés passées plutôt que de remplacer. Si la liste passée à plus d'éléments que le noeud de liste, alors les éléments manquants seront créés.
Construction d'EntityStore
TS 2.8 apporte une fonctionnalité très puissante au système de types, lui permettant de décrire des transformations conditionnelles de types. Auparavant (depuis la 2.1 pour être précis), il était simplement possible de définir des transformations uniformes de types, comme Partial<T>
par exemple, qui transforme toutes les propriétés du type T en propriétés optionnelles, ou encore Readonly<T>
dont le nom est explicite. Dans Focus, on s'en servait pour transformer les objets...
v8.7.2
Typescript 2.7
La montée de version vers TS 2.7 a été faite, et on est maintenant compatible avec tous les flags du mode --strict
(en particulier --strictFunctionTypes
de la 2.6 et --strictPropertyInitialization
de la 2.7). Cela veut dire que vous pouvez également activer ces flags dans vos projets et compter sur Focus pour ne pas causer d'erreurs supplémentaires.
advancedSearchFor
, actionBarFor
...
Ces fonctions ont été ajoutées et sont désormais les manières standard de déclarer des composants de recherche et autres, sur le même modèle que les listes. La raison est la même (inférence du type de store impossible en JSX) et les implémentations sont les mêmes. De ce fait, l'export AdvancedSearch
depuis la racine focus4
a été remplacé par advancedSearchFor
Formulaires
Les formulaires appellent maintenant bien l'action submit
à la sauvegarde et les inputs ont bien le name
et l'id
sur les champs. Cela devrait être suffisant pour activer les suggestions à la saisie sur les champs, mais cela ne marche que sur Chrome...
disableDragAnimThreshold
Cette prop a été ajoutée sur les listes (jusqu'à la recherche...) pour désactiver les animations de drag and drop à partir d'un certain nombre d'éléments affichés, au cas où des lenteurs soient détectées à l'usage.
v8.7
Facettes multi-sélectionnables
La recherche supporte maintenant la multi-sélection de valeurs au niveau des facettes, si l'API serveur utilisée est compatible. Conformément à l'implémentation choisie, chaque facette doit être manuellement déclarée comme multi-sélectionnable dans la déclaration de facette côté serveur, et cette information doit être relayée au client via une nouvelle propriété isMultiSelectable
attendue sur l'objet FacetOutput
(à côté du code
, label
et values
). Cela permet au composant d'adapter son affichage pour gérer la sélection multiple via des checkbox.
Attention breaking change, selectedFacets
contient maintenant des listes de valeurs (string[]
) au lieu d'une valeur simple, et ce changement est reflété dans l'objet QueryInput
envoyé au serveur. On utilise des listes partout, y compris si la facette n'est pas multi-sélectionnable (pour garder une API homogène).
Masques de saisie
L'Input
de react-toolbox
a été surchargé dans focus4/components
pour ajouter la gestion de masques de saisie, via la nouvelle prop mask
. L'implémentation utilise inputmask-core et le contenu de la prop correspond aux options listées dans la doc.
Evolutions champs date et heure
Les InputDate
et InputTime
utilisent maintenant des masques de saisie pour la saisie manuelle des dates et heures. Le masque est déduit automatiquement depuis le format de date/heure demandé. Par conséquent, il n'est plus possible de préciser plusieurs inputFormat
en entrée (ce qui ne sert de toute façon plus à rien si on a un masque 😄 ).
De plus, le calendrier/l'horloge s'affiche maintenant au dessus du champ s'il n'y a pas la place en dessous, et le horloge ne déconne plus si on scrolle pendant qu'elle est ouverte.
Autocomplete isQuickSearch
Un nouveau mode à été ajouté au composant d'Autocomplete
pour gérer un mode "recherche rapide", dans le cas ou on ne veut pas le lier à un champ et que tout ce qu'on veut utiliser est l'évènement onChange
. Dans ce nouveau mode, l'Autocomplete ignore la valeur qui lui est passée et vide le champ texte à la sélection de valeur.
La méthode focus()
est maintenant exposée (accessible par ref
) et est également appelée sur le onChange
du mode "recherche rapide", pour pouvoir facilement sélectionner plusieurs valeurs.
Les libellés issue des keyResolver
et querySearcher
de l'Autocomplete est maintenant traduits.
L'Autocomplete encode maintenant bien les caractères spéciaux à l'appel du querySearcher
.
Tooltip sur les labels
Il est maintenant possible d'afficher des tooltips au niveau des libellés (sur la droite). Deux props on été ajoutées sur fieldFor
: showTooltip
et comment
, pour afficher la tooltip et renseigner son contenu.
Le champ comment
a également été ajouté sur FieldEntry
(à côté de isRequired
, name
...) pour permettre la génération du commentaire de la tooltip via le modèle, qui sera utilisé nativement par fieldFor
si non reprécisé (comme tout le reste). C'est pour ça qu'on a séparé showTooltip
et comment
, puisqu'idéalement tous les commentaires seront renseignés.
notfoundHandler
sur le routeur.
Dans la configuration du routeur, il est maintenant possible de préciser un handler personnalisé pour les routes non trouvées. Il s'exécute avant la redirection vers la page de 404 et peut retourner :
- Une url (string), URL sur laquelle rediriger
true
, pour ne rien faire et rester sur place.undefined
(ou rien), pour continuer sur la redirection d'erreur.
v8.6
list
+ search
= collections
Les deux "modules" list
et search
ont été regroupés au sein d'un même "module" collections
. La distinction était déjà un peu arbitraire, et certaines (nouvelles) fonctionnalités nécessitaient que les composants de listes aient connaissance de ce qu'est un SearchStore
.
Concrètement, cela veut dire que tout ce qui était dans focus4/list
ou focus4/search
est maintenant dans focus4/collections
(breaking change). Les imports/exports ont été un peu changés aussi, en particulier les types de recherches sont directement dans focus4/collections
au lieu de focus4/search/types
(cela impacte le générateur de services Kinetix). Si vous aviez une référence à un composant en particulier, sachez que focus4/list/components
est maintenant focus4/collections/components/list
, et idem pour la recherche.
En parlant d'imports, computed
est maintenant (enfin !) exporté à la racine de focus4
et SearchBar
n'est plus exporté de la racine.
Evolutions ListStore/SearchStore
- Un ListStore ne peut plus être muni d'un service de chargement (il ne peut agir que sur une liste locale maintenant). Cette fonctionnalité était inutilisée, surtout que le SearchStore pouvait déjà faire la même chose en permettant de réutiliser (côté serveur principalement) les mêmes services/DTOs.
- L'API ListStore/SearchStore a été un peu homogénéisée, en particulier l'interface commune expose maintenant une propriété
list
, correspondant à la liste entière dans un ListStore (potentiellement triée/filtrée) et le résultat non groupé d'un SearchStore. Il y a deux implications notables à ce changement :- breaking change :
ListStore#dataList
est maintenantListStore#list
(j'en profite également pour préciser queListStore#list
n'est à priori pas une liste observable (les éléments le sont par contre) puisque c'est une transformation deListStore#innerList
(avec tri + filtrage éventuel), qui elle est bien la liste de base qui est observable. La différence est bien reflétée dans le typage, les deux sont accessibles et le setter deListStore#list
appelle bien celui deListStore#innerList
) StoreList
etStoreTable
n'ont plus de propriétédata
et ne prenne toujours que le store en paramètre. Par défaut, ils iront lire la propriétélist
, sauf si c'est un SearchStore avec des groupes et qu'on précise la nouvelle propgroupCode
: dans ce cas, ils afficheront le groupe demandé.
- breaking change :
- Toute la logique de chargement/pagination a été déplacée dans les composants de listes, que ça soit pour une liste simple, un ListStore ou un SearchStore. Auparavant, la gestion du "Voir plus"/"Show more" était en double, une fois dans le
Results
de l'AdvancedSearch
pour gérer la pagination serveur et une fois dans le composant de liste pour gérer la pagination locale, qui n'était pas utilisables en même temps. Cela veut dire que :- Il est maintenant possible d'écrire un
storeListFor
oustoreTableFor
qui prend directement enSearchStore
en paramètre qui gère proprement le "Voir plus"/"Show more", en particulier la pagination serveur. - Il est maintenant possible d'utiliser la pagination locale et la pagination serveur en même temps. Par exemple, je défini un
perPage
de 25 et unSearchStore#top
de 50, l'action "Show more" (appellée manuellement ou automatiquement selonisManualFetch
) va tour à tour appeler la pagination locale et la pagination serveur, permettant de dissocier le chargement de l'affichage. A ce titre, une proplistPageSize
a été ajoutée sur l'AdvancedSearch
pour en bénéficier sur le mode non groupé.
- Il est maintenant possible d'écrire un
isItemSelectionnable
Lié aux stores de liste et de recherche, je mets quand même ce point à part parce que c'est un changement de nature différente.
breaking change isLineSelectionnable
sur les composants de liste/recherche à été remplacé par Store#isItemSelectionnable
. C'était une erreur de mettre le prédicat sur la liste puisque ça n'affectait pas l'action toggleAll
, permettant de sélectionner ainsi des items non sélectionnables.
Nesting d'EntityStore
Il est maintenant possible de spécifier dans makeEntityStore
un autre EntityStore dans la liste des noeuds simples. Cela ne change que très peu le fonctionnement des stores puisqu'un store partage la même API (clear()
, set()
) qu'un StoreNode
classique, et c'est même exactement la même chose que si c'était un Node qui n'avait que des sous-Nodes en propriétés.
Cela permet de créer une structure de données un peu plus "en arbre" et de bénéficier d'actions de set()
et surtout de clear()
qui agissent sur plusieurs stores, liés par la structure. Attention tout de même aux méthodes globales set()
des stores : leurs propriétés sont certes nommées mais ne sont pas typées (correspondance impossible entre le Node et le type standard, c'est pareil pour les listes d'ailleurs).
v8.5
[Field] labelRatio
/contentRatio
/disableInlineSizing
breaking
labelSize
et contentSize
ont été remplacés par labelRatio
et contentRatio
, qui indique directement la largeur en pourcentage (par défaut, labelRatio
vaut donc 33
et contentRatio
n'est pas défini). Si contentRatio
n'est pas défini, il vaut 100 - labelRatio
, s'il est définit alors il prend la valeur donnée (ce qui permet d'avoir un total différent de 100%, ce qui était avant impossible). Le CSS a été adapté pour permettre de revenir à la ligne si besoin, et les line-height
ont été homogénéisés. Les propriétés ont été volontairement renommées pour identifier tout de suite les corrections à faire dans les différents projets.
[Field/CSS] Theme des composants d'input/display
La propriété theme
du Field
permet maintenant de définir un CSS custom pour les composants d'input et de display (theme.display
est un objet qui correspond au theme du DisplayComponent, idem pour theme.input
). Ce theme est fusionné avec celui issue de displayProps
/inputProps
, permettant des surcharges à la fois dans le domaine (comme avant) et des surcharges locales (ce qui n'était pas possible avant car on devait surcharger displayProps
/inputProps
, et c'était moins pratique).
Corrections antérieures (8.4.x)
981c26d
d3ab6f6
f910078
dc7af8d
8573506
Corrections postérieures (8.5.x)
#35
Overflow: hidden sur la liste
v8.4
InputTime
Le composant d'InputTime
est maintenant disponible. Il est calé sur le modèle de l'InputDate
, c'est-à-dire qu'il permet la double saisie "champ texte"/"horloge" pour l'heure et prend bien un ISOString en entrée (au lieu d'un objet Date). Il est donc utilisable tel quel avec des champs Focus classiques.
Séparation liste et groupes dans la recherche
Les résultats de recherches liste et groupes sont maintenant bien séparés dans le store de recherche, dans SearchStore#list
et SearchStore#groups
(au lieu de SearchStore#results
). Ce changement reflète l'API serveur qui les a toujours séparés. Cela permet donc de conditionner proprement le rendu des résultats selon que l'on reçoive des groupes depuis le serveur, à la place de la demande. Et aussi de se débarrasser de tous les petits hacks par-ci par-là pour considérer une liste de 1 groupe comme une liste.
Cela veut dire que si vous utilisiez SearchStore#results
dans votre code, bah ça n'existe plus. Pour savoir ce qui a été renvoyé par le serveur, il suffit de regarder la taille de la liste ou de la liste de groupes (si l'une est renseignée, l'autre est bien vidée).
SearchStore#flatResultList
n'est par contre pas affecté et contient bien toujours la liste de tous les résultats, qu'ils soient groupés ou non.
Mode groupe sans actions (!useGroupActionBars
)
La recherche dispose maintenant d'un nouveau mode d'affichage de groupes avec un header simple, qui ne permet (si activée) que la sélection en masse du groupe. Ce mode rappelle l'affichage des groupes tel qu'il était dans la v2, et, comme ce dernier, il est possible de personnaliser ce composant (via la prop GroupHeader
).
Ce nouveau mode est le nouveau défaut, et l'ancien mode (avec les ActionBar
qui permettaient les actions sur le groupe) peut se réactiver avec la prop useGroupActionBars
.
Actions globales, de groupes et de lignes.
Les actions disponibles sur la recherche sont maintenant divisées en 3 :
lineOperationLists
, qui permet de préciser les actions disponibles au niveau de chaque ligne. C'est une fonction de la donnée de la ligne.groupOperationLists
, qui permet de préciser les actions qui disponibles au niveau de chaque groupe. C'est une fonction du groupe. N'est disponible qu'avecuseGroupActionBars
(puisqu'elles sont sur l'ActionBar du groupe)- (nouveau)
operationList
, qui permet de préciser les actions globales sur tous les éléments retournés. N'est disponible qu'en mode non groupé ou avecuseGroupActionBars
désactivé (l'ActionBar
globale est désactivée dans ce mode, remplacée par celles des groupes).
v8.3.5
EntityStore
Cette release ajoute la méthode pushNode(item)
sur StoreListNode<TNode>
, permettant d'ajouter un élément "normal" (i.e. pas un StoreNode
) dans un noeud de liste.
Cela veut dire qu'on peut maintenant (enfin) faire:
const store = makeEntityStore({}, {myList: {} as MyObjectNode}, [MyObjectEntity], {myList: "myObject"});
store.myList.pushNode({id: 1, label: "hello"});