Skip to content

Releases: klee-contrib/focus4

v9.6.4

13 Nov 19:54
Compare
Choose a tag to compare

Correctifs

#92 #94

Montées de version (notables)

React 16.6

v9.6.3

19 Oct 12:28
Compare
Choose a tag to compare

Fixes

#90 et #91

v9.6.2

15 Oct 21:30
Compare
Choose a tag to compare

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

22 Jul 17:20
986842b
Compare
Choose a tag to compare
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éthode reset() 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 le Field avant)
      • Le FormNode, ses sous-nœuds, ainsi que chaque EntityField sont muni d'une propriété form.isValid (isValid pour EntityField), résultat de la validation des champs contenus dans ce nœud (ou juste du champ).
    • Il contient l'état d'édition du FormNode et de tous ses constituants (nouveau). A un endroit donné, la valeur de isEdit (node.form.isEdit ou field.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 du isEdit qui pilote toute l'édition.
  • Créer, à priori dans un composant, les actions du formulaire (FormActions), à partir d'un noeud issu d'un FormNode (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 hook onFormLoaded()
      • save(), muni du hook onFormSaved()
      • toggleEdit(), muni du hook onToggleEdit()
      • clean(), équivalent de ce qu'il y avait dans componentWillUnmount()
    • Il expose les propriétés suivantes :
      • panelProps, les props à passer à un Panel (inchangé)
      • formProps, les props à passer à un Form (voir ci-dessous) (nouveau)
  • 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() au componentWillMount()
      • save() à la validation du formulaire
      • clean() au componentWillUnmount()
    • Il s'agit une fois encore d'une extraction d'AutoForm, cette fois-ci de la partie "composant"
  • (optionnel, inchangé) Poser un Panel et lui donner actions.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...

Read more

v8.7.2

06 Feb 14:16
Compare
Choose a tag to compare

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

02 Feb 16:10
Compare
Choose a tag to compare

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

12 Jan 12:16
Compare
Choose a tag to compare

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 maintenant ListStore#list (j'en profite également pour préciser que ListStore#list n'est à priori pas une liste observable (les éléments le sont par contre) puisque c'est une transformation de ListStore#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 de ListStore#list appelle bien celui de ListStore#innerList)
    • StoreList et StoreTable 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 prop groupCode : dans ce cas, ils afficheront le groupe demandé.
  • 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 ou storeTableFor qui prend directement en SearchStore 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 un SearchStore#top de 50, l'action "Show more" (appellée manuellement ou automatiquement selon isManualFetch) va tour à tour appeler la pagination locale et la pagination serveur, permettant de dissocier le chargement de l'affichage. A ce titre, une prop listPageSize a été ajoutée sur l'AdvancedSearch pour en bénéficier sur le mode non groupé.

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

03 Jan 15:28
Compare
Choose a tag to compare

[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

06 Dec 18:17
Compare
Choose a tag to compare

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'avec useGroupActionBars (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 avec useGroupActionBars désactivé (l'ActionBar globale est désactivée dans ce mode, remplacée par celles des groupes).

v8.3.5

29 Nov 17:29
Compare
Choose a tag to compare

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"});