Skip to content

Releases: klee-contrib/focus4

v10.3

19 Mar 16:18
Compare
Choose a tag to compare

Types de domaines

Le type "réel" du champ, jusqu'ici matérialisé par la proprieté fieldType d'un champ et pouvant prendre les valeurs "string", "number" ou "boolean", est désormais porté par le domaine. Une quatrième valeur, "object", a été ajoutée pour tous les types non primitifs qui ne sont pas parmi ces trois là.

Cela veut dire qu'on ne défini plus de type "Typescript" sur le domaine, mais un type "réel" à la place. Concrètement, il faut donc remplacer tous vos domain<string>()({....}) par domain({type: "string", ...}). Cela implique donc que le domaine ne pourra pas avoir connaissance du type précis du champ associé, au dela de string, number, boolean ou any (qui correspond à "object").

En revanche, le type du champ est désormais toujours contraint par le type du domaine, c'est-à-dire qu'un champ de type nombre doit forcément avoir un domaine associé de type "number", y compris pour les champs créés à la volée via makeField et formNodeBuilder.add. En l'absence d'un domaine fourni, ces fonctions prendront un domaine de type "string" par défaut, et ce domaine par défaut ne connait d'ailleurs plus les composants par défaut (Input, Select, Autocomplete, Display et Label) (c'était déjà le cas pour metadata() dans add et patch mais pas makeField). Vous êtes donc fortement incités à toujours spécifier un domaine lorsque vous créez un champ.

En contrepartie, il n'est plus nécessaire de spécifier fieldType sur les champs si le type est exactement celui du domaine. Et le domaine étant toujours forcé, il ne devrait plus jamais y avoir de problème avec le type de la valeur retournée par un onChange (genre un string à la place d'un nombre par exemple).

Il est donc nécessaire de mettre à jour la génération, au moins pour arrêter de renseigner "string", "number" et "boolean" dans fieldType. Si vous voulez le renseigner, que ça soit pour spécifier le type du domaine (une liste de codes à la place de string par exemple, ou un type précis genre number[] à la place du any du domaine associé), ou bien pour continuer à le faire systématiquement même quand c'est pas utile, il faudra maintenant toujours le générer sous la forme {} as TypeDuChamp, comme c'était le cas avant la v9.9. Du coup désolé, j'aurais dû être plus malin à cette époque là et directement faire ce qui est fait aujourd'hui.

Par ailleurs, la méthode metadata() du FormEntityFieldBuilder ne permet plus de renseigner le domaine. Il est nécessaire de passer par la nouvelle méthode domain() qui permet de passer le domaine. Puisqu'il contraint le type du champ, il est nécessaire de l'appeller en premier dans la chaine de méthodes pour patcher ou ajouter un champ dans un FormNode.

Remarque : makeField permet toujours de renseigner l'équivalent de metadata et domain en même temps, principalement parce qu'il y a moins de contrainte sur le typage du champ créé. Et aussi pour ne pas changer l'API qui est largement utilisée. Mais il est probable qu'un jour on passe à une API équivalent à celle de add dans un FormNode, avec du fluent et tout. Mais pas tout de suite

Généralisation de l'usage de ReferenceList

Tous les composants d'affichage / select prennent désormais une ReferenceList comme paramètre, au lieu de la liste + valueKey + labelKey. Ca change pas grand chose en général puisque la grande majorité des champs sont posés par selectFor, mais ça veut aussi dire qu'il faut proscrire des initialisations du type [] as ReferenceList, puisque tous les composants derrières supposent qu'ils ont accès à l'ensemble de l'API. Pour contourner le problème, on expose désormais une méthode emptyReferenceList(), qui permet de faire ces initialisations là proprement.

J'en profiter pour rappeler également que pour vos listes de références stockées dans vos composants qu'il est également indispensable de les stocker dans un @observable.ref (au lieu d'un @observable), sinon vous allez avoir de mauvaises surprises.

Réécritures des composants de listes

Les composants de listes ont été intégralement réécrits, dans le but de rationnaliser l'ensemble et de passer sur une implémentation moderne. Le résultat principal de cette réécriture fait qu'il n'y a plus de StoreList et de StoreTable, la gestion d'un store (de liste ou de recherche) est désormais native sur la liste et le tableau de base. Il faudra donc simplement remplacer tous vos storeListFor et storeTableFor par listFor et tableFor, pour le même résultat. Il est également désormais possible de passer un store à une Timeline, qui le gère de la même façon que les deux autres listes.

De plus, les composants ont été séparés de manière plus marquées, ce qui se traduit dans la façon dont le CSS est maintenant réparti. Concrètement, tout le CSS de la liste est dans "list", celui du tableau dans "table", de la timeline dans "timeline" et le commun (pour les boutons du bas genre "voir plus") dans "listBase"). Cela remplace les deux anciens CSS "list" et "line" qui mélangeait un peu les deux de façon peu claire.

Petite réorganisation de la recherche avancée

Suite à ces réécritures, quelques nouveautés sont apparues dans la recherche avancée :

  • Il est désormais possible de choisir un composant de liste, parmi les 3 disponibles, ou bien le votre si vous êtes chauds. Par défaut, c'est toujours la liste classique qui est utilisée. Cela implique par contre que les props du composant de lignes doivent maintenant être renseignées dans une nouvelle prop listProps, puisqu'elles peuvent dépendre du composant choisi. En particulier, itemKey et LineComponent sont dedans, il faudra donc tous les déplacer. Dans le même genre, lineOperationList est désormais operationList dans listProps par exemple.
  • Le composant ListWrapper, qui permet principalement d'afficher un bouton de création global et un sélecteur de mode (list/mosaic) sur la recherche avancée, a été complètement intégré à la recherche avancée. Son intérêt étant déjà très limité, tout est plus simple comme ça. Et il est possible qu'il soit étendu plus tard pour passer d'autre contexte dans la recherche...

Header de table

Le header d'un table triable a été revu, pour être plus joli et plus clair :

image

En particulier, tout le titre est désormais cliquable pour changer le sens de tri et les colonnes triables sont indiquées par des pointillés.

Mise à jour mobx-react

Grâce à la refonte des listes qu'il m'a enlevé le principal blocage à la mise à jour, on utilise désormais la v6 de mobx-react. Y a rien qui change à l'usage, à part le fait que les différents hooks sont désormais inclus dedans (useObserver, useLocalStore...). Il n'y a donc plus besoin d'aller les chercher dans mobx-react-lite, et d'ailleurs il ne faut plus le faire.

Pour célébrer l'occasion, l'import focus4 expose désormais useObserver et useLocalStore, pour vous motiver à passer aux hooks.

autoSelect sur Autocomplete

Le composant d'Autocomplete dispose maintenant d'une nouvelle prop autoSelect, qui a pour effet de,dès que la recherche ne renvoie qu'une seule valeur, de la sélectionner automatiquement (sans cliquer dessus ou appuyer sur Entrée). Cette fonctionnalité est pratique pour forcer une sélection si le champ d'autocomplétion n'est pas obligé de matcher sur une valeur par exemple.

Corrections diverses

  • #150
  • Les erreurs sur BooleanRadio et SelectRadio sont désormais affichées correctement
  • getLabel() fonctionne correctement si la valeur vaut 0
  • FormActionsBuilder.params(undefined) fonctionne désormais comme attendu (et indiqué dans la doc d'ailleurs)
  • Le prop noForm sur <Form> est désormais à l'endroit, c'est à dire que le formulaire HTML est posé par défaut et est enlevé avec noForm = true. Attention du coup puisque ça veut dire qu'avant vous n'aviez aucun formulaire par défaut et maintenant vous allez en avoir partout !
  • Il n'est plus nécessaire de spécifier le edit() d'un sous noeud pendant un (make/use)FormNode (ça plantait avant si on le faisait pas)

Refonte du module legacy

Le meilleur pour la fin, et celui qui ne concerne finalement pas grand monde (on me souffle dans l'oreille qu'il n'y a que moi qui m'en sers) : le module legacy a été réécrit pour utiliser les composants de champs "classiques" (ceux de forms) au lieu d'une copie à peine modifiée de tous les composants, et en particulier le Field en lui même, pour pouvoir gérer les différences entre les deux types de formulaires (les nouveaux et les anciens).

Ce module ne contient donc plus que le bon vieux AutoForm, ainsi qu'un composant FieldWrapper qui permet de convertir des champs de StoreNode en champs de FormNode à la volée, ce qui est nécessaire pour faire la compatibilité entre l'AutoForm et les composants "classiques". Via le FieldWrapper, il est possible de reproduire ce qui été fait à la main avec des ref pour récupérer les erreurs, et également de patcher des champs à la volée. L'usage direct de fieldWrapperFor n'est pas recommandé dans le cas général, puisque c'est ce que fait this.fieldFor, this.selectFor, ... , qu'il faut continuer à utiliser comme si de rien n'était. Et il paraît même qu'il est possible d'utiliser les même champs via le FieldWrapper sur des vieux formulaires en v2.... :)

Les APIs de this.fieldFor, this.selectFor, this.autocompleteFor et this.displayFor ont été homogénéisées avec leurs équivalents sans this, puisque maintenant elles utilisent les mêmes composants derrière de toute manière. Leur APIs sont donc équivalentes à la fonction de base + un metadata() + un domain().

v10.2

17 Feb 11:55
Compare
Choose a tag to compare

Refonte interne des modules CSS (#182)

L'une des critiques principales émise à l'encontre des modules CSS a toujours été le fait que toutes les classes définies dans un module sont au même niveau, ce qui ne permet pas de différencier, dans une optique BEM par exemple, quelles classes sont des "blocks", des "elements" ou des "modifiers". Le module CSS en lui-même correspond déjà au niveau "block", et par convention la classe "block" dans le module est la classe qui a le même nom que le module. Il manquait donc essentiellement la notion de "modifier".

C'est donc chose faite. Le CSS des différents modules Focus (et donc pas celui issu de react-toolbox, désolé) a donc été adapté pour avoir des classes qui ont des noms de modifiers. Par exemple, dans le Panel, .top s'appelle désormais .title--top (ça a toujours été un modifier sur .title), ou .loading qui devient .panel--loading. Un certain nombre de classes ont donc été renommées. Elles sont visibles dans la PR, et si le besoin se présente je pourrais prendre la peine de les lister ici.

De plus, les exports de CSS et de types ont été renommés : {module}Styles devient {module}Css et {Module}Style devient {Module}Css.

En interne

Un nouveau générateur de types TS pour les modules CSS a été mis au point, qui permet de typer précisément les classes générées pour en particulier pouvoir déterminer quelles classes sont modifiers de quels elements.

Exemple :

import {CSSElement, CSSMod} from "@focus4/styling";

interface Actions { _f6135: void }
interface Content { _196cd: void }
interface Panel { _353e5: void }
interface Progress { _3cd99: void }
interface Title { _4bf8f: void }

export interface PanelCss {
    actions: CSSElement<Actions>;
    content: CSSElement<Content>;
    panel: CSSElement<Panel>;
    "panel--editing": CSSMod<"editing", Panel>;
    "panel--loading": CSSMod<"loading", Panel>;
    progress: CSSElement<Progress>;
    title: CSSElement<Title>;
    "title--bottom": CSSMod<"bottom", Title>;
    "title--top": CSSMod<"top", Title>;
}

declare const panelCss: PanelCss;
export default panelCss;

Dans Focus (et à terme dans les projets), le module CSS est transformé dans un objet particulier, qui crée une fonction par element, qui prend un objet en paramètre pour spécifier les modifiers associés,

Exemple :

<div className={theme.panel({loading, editing})}>
    <div className={theme.title({top: true})}>
        <h3>{i18next.t(title)}</h3>
    </div>
    <div className={theme.content()}>{children}</div>
    <div className={theme.title({bottom: true})}>{buttons}</div>
</div>

Toutes les propriétés theme des composants Focus/RT peuvent prendre en paramètre soit l'objet theme classique, soit ce fameux objet à la place.

L'objectif final est d'exposer ces fonctionnalités aux projets. Pour l'instant, il n'y a pas encore de chemin clair pour brancher la génération et consommer facilement le CSS comme ça en dehors de Focus.

CSS Timeline + addItemHandler

Ca ressemble à ça maintenant.

image

En particulier, le CSS de la timeline est plus simple et ne force plus tout un gros bloc dégueulasse (juste les traits violets)

IDs multiples de champs

Il peut arriver parfois dans un formulaire d'avoir plusieurs champs qui ont le même nom. Cela arrivera forcément dans un formulaire sur une liste par exemple. Les noms de champs étant utilisés comme ID dans le HTML, cela pouvait poser différents problèmes, comme les libellés qui ne pointaient pas sur le bon input, ou bien un bon gros warning du navigateur qui explique que c'est interdit.

Par conséquent, à la création d'un nouveau champ, Focus va automatiquement incrémenter l'ID d'un champ qui existe déjà sur la page, indépendemment du fait qu'il soit en édition.

labelRatio/valueRatio sur le Form

Il est désormais possible de passer ces paramètres sur le composant <Form>, qui seront passés par contexte à tous les champs qui sont dedans. Il reste possible de les surcharger par champ, comme d'habitude.

De plus, forceActionDisplay est désormais directement sur FormActions, et non plus sur un object formContext.

Suppression des anciens makeFormActions/makeFormNode (pré 10.1)

Tout a été supprimé, il n'est désormais plus possible d'utiliser les anciennes syntaxes. La doc a été mise à jour en conséquence (!!).

v10.1

06 Nov 19:47
Compare
Choose a tag to compare

makeFormNode

makeFormNode a été reconstruit à partir de FormNodeBuilder, FormListNodeBuilder et FormEntityFieldBuilder, pour offrir une API de type "fluent" pour modifier le FormNode a sa création, qui remplace la fonction "initializer" et des appels répétés à patchField.

Remarque : L'ancienne façon de construire un FormNode est toujours supportée, néanmoins elle sera retirée dans le futur (proche)

Ci-dessous, un exemple de modification (celui du starter-kit) :

c =>
        c
            // On change le domaine et le isRequired du champ.
            .patch("denominationSociale", (f, node) =>
                f.metadata(() => ({
                    domain: DO_COMMENTAIRE,
                    isRequired: !!node.capitalSocial.value
                }))
            )
            .patch("capitalSocial", (f, node) =>
                f
                    .value(() => (node.denominationSociale.value && node.denominationSociale.value.length) || 0)
                    .metadata({validator: {type: "number", max: 20000}})
                    .edit(() => node.statutJuridiqueCode.value !== "EARL")
            )
            .patch("adresse", s => s.edit(false))

            // On ajoute un champ supplémentaire calculé.
            .add("email", (f, node) =>
                f
                    .value(() => node.denominationSociale.value, value => (node.denominationSociale.value = value))
                    .metadata({
                        domain: DO_LIBELLE_100,
                        label: "structure.email",
                        validator: {type: "email"}
                    })
            )

Cette nouvelle API permet de typer intégralement toutes les modifications, que ça soit des ajouts de champs ou même des modifications de champs (par exemple : composant d'input), et ce quelque soit le niveau (sous objet, sous liste...). Auparavent, aucune modification ne pouvait être tracée, et il n'était possible d'ajouter des champs qu'à la racine et dans les sous-listes (et ces dernières n'étaient pas tracées).

De plus, l'API est plus simple à prendre en main car elle ne dépend plus de fonctions supplémentaires à importer comme patchField.

FormNodeBuilder

Il permet de construire un FormNode à partir d'un StoreNode. Il sera le paramètre de toute fonction de configuration sur un FormNode (celle de makeFormNode, ou de patch et items qu'on vera plus bas).

Il dispose des méthodes suivantes :

edit(value)

La fonction edit, qui prend en paramètre soit un booléen, soit une fonction retournant un booléen, permet de modifier l'état d'édition initial (si booléen), ou bien de forcer l'état d'édition (si fonction). Le fonctionnement est identique à ce que faisait patchNodeEdit, et les valeurs par défaut sont les mêmes (false pour le noeud racine et true pour tout le reste).

edit(value, ...props)

Il est également possible de passer des propriétés de l'objet à la fonction edit. Dans ce cas, la valeur initiale/fonction sera appliquée aux champs/listes/objets demandés au lieu de l'objet en lui-même. Il est donc parfaitement possible d'utiliser edit plusieurs fois, tant que ça s'applique à des propriétés différentes.

Exemple :

s
    .edit(!this.props.egfId) // objet en entier
    .edit(false, "id", "isValide", "totalEngagementFinancier", "totalVersementsRecus") // valeur par défaut sur certains champs
    .edit(() => !this.props.egfId, "typeEngagementPartenaireCode") // édition forcée sur un champ

La façon standard de modifier l'état d'édition d'un membre d'objet est de passer par patch (ou add), décrits en dessous, mais cette version de edit() permet de spécifier le même état d'édition à plusieurs champs à la fois (désactiver l'édition sur plusieurs champs est un usage très courant). C'est aussi une version plus courte pour un seul champ si l'édition est la seule chose à modifier dessus.

add(name, fieldBuilder)

La fonction add permet d'ajouter un nouveau champ au FormNode en cours de construction. Elle prend comme paramètres :

  • name, qui est le nom du champ à créer
  • fieldBuilder, une fonction qui sera appelée avec un FormEntityFieldBuilder et le FormNode courant pour paramétrer le champ.

patch(name, builder)

La fonction patch permet de modifier un membre du FormNode, que ça soit un champ, une sous-liste ou un sous-objet. Elle prend comme paramètres :

  • name, qui est le nom du champ/sous-objet/sous-liste. Ce nom est typé et changera la signature en fonction de ce à quoi il correspond.
  • builder, qui peut être un fieldBuilder, listBuilder ou un objectBuilder en fonction du membre choisi. Ces fonctions seront appelé avec le builder correspondant (FormEntityFieldBuilder, FormNodeBuilder ou FormListNodeBuilder), ainsi que le FormNode courant.

build()

La fonction build permet de construire le FormNode. Elle est appelée à la fin de makeFormNode et à priori il n'y aura jamais besoin de l'appeler manuellement.

FormListNodeBuilder

Il permet de construire un FormListNode à partir d'un StoreListNode. Il sera le paramètre de toute fonction de configuration sur un FormListNode (celle de makeFormNode ou de patch)

Il dispose des méthodes suivantes :

edit(value)

Idem FormNodeBuilder

items(objectBuilder)

La fonction items permet de modifier les items de la liste (qui sont, pour rappel, des FormNodes). Elle prend comme unique paramètre objectBuilder, pour préciser la configuration. Comme toujours, cette fonction est donc appelée avec un FormNodeBuilder ainsi que le FormListNode courant.

build()

Idem FormNodeBuilder.

FormEntityFieldBuilder

Il permet de construire un FormEntityFIeld à partir d'un EntityFIeld. Il sera le paramètre de add et patch sur le FormNodeBuilder.

Il dispose des méthodes suivantes :

edit(value)

Idem FormNodeBuilder

value(get, set?)

La fonction value permet de remplacer la valeur d'un champ (ou bien de la définir pour un champ ajouté) par une valeur calculée, avec un setter éventuel (auparavant, cette possibilité n'était pas offerte au "patch"). Elle prend comme paramètres :

  • get, pour spécifier le nouveau getter du champ
  • set, pour spécifier le nouveau setter du champ

metadata($field)

La fonction metadata permet de remplacer les métadonnées d'un champ (ou bien de les définir pour un champ ajouté). Le fonctionnement est identique à ce que faisait patchField. Elle prend un seul paramètre, $field, qui contient soit toutes les métadonnées à remplacer (champ et contenu du domaine), soit un fonction qui les renvoie.

Création via makeFormNode.

Les signatures correspondantes de makeFormNode sont donc :

  • makeFormNode(this, node, objectBuilder, initialState)
  • makeFormNode(this, listNode, listBuilder, initialState)

Remarques

  • Tous les FormNodes sont désormais créés vides. Pour être cohérent, les anciennes signatures de makeFormNode précisent également isEmpty à true par défaut.
  • Tout comme clear(), replace(), set(), reset() renvoie également le FormNode lorsqu'il est appelé au lieu de void
  • initialState peut être soit un objet contenant les champs du Store(List)Node, soit une fonction qui retourne ce même objet, ce qui permet de gérer le cas d'une initialisation conditionnelle.

makeFormActions

De la même façon que le FormNode, les FormActions sont maintenant construites via un FormActionsBuilder, qui ressemble à quelque chose comme

a =>
        a
            .params(() => homeViewStore.withView(({page, id}) => !page && id && +id))
            .load(loadStructure)
            .save(saveStructure)

Remarque : tout comme makeFormActions, l'ancienne syntaxe reste utilisable (pour l'instant)

FormActionsBuilder

Il dispose des méthodes suivantes :

params(()=>params)/params(params)

Il fonctionne à peu près de la même façon que getLoadParams, à ceci près qu'il est désormais possible de :

  • Passer une valeur fixe au lieu d'une fonction, si les paramètres ne changent jamais
  • Renvoyer une valeur unique au lieu d'un array s'il n'y a qu'un seul paramètre (via la fonction ou directement)
    Cela veut dire que tout ceci est désormais identique :
params(() => [1])
params(() => 1)
params([1])
params(1)

Il est possible d'appeler params() tel quel sans arguments pour indiquer que le service de chargement ne prend pas de paramètres.

Si vous devez renvoyer plusieurs paramètres non fixes, alors il faudra marquer l'array renvoyé comme as const (cf. load juste en dessous). Exemple : params(() => [this.props.id, "test"] as const)

Remarque : Si params est appelé avec undefined ou si la fonction passée renvoie undefined, la service de chargement ne sera pas appelé. C'est cohérent avec le fonctionnement précédent, sauf qu'auparavant, puisqu'on était obligé de retourner un array, le choix de retourner undefined était délibéré. Par exemple, pour un composant qui prend un id en props et qui gère de la création/consultation, il fallait spécifier () => this.props.id ? [this.props.id] : undefined pour que le service de chargement ne soit pas appelé en consultation. Désormais, () => this.props.id (ou même this.props.id directement) se comporte exactement de la même manière. Si vous voulez vraiment que le service soit appelé avec undefined, alors il faut utiliser () => [this.props.id] as const (ou [this.props.id]).

load(service)

Permet de préciser le service de chargement. params() doit être appelé avant car il type désormais les paramètres de load.

save(service, name?)

Permet de préciser un service de sauvegarde. Il est toujours possible d'en spécifier plusieurs, pour se faire il suffit de renseign...

Read more

v10

20 Oct 11:51
Compare
Choose a tag to compare
v10

Après une série de version "mineures" qui avaient tout plein de breaking changes et qui ont été mal suivies par les projets (après c'est surtout parce qu'elles sont tombées en fin de cycle), voilà maintenant qu'une vraie version majeure arrive pour enfoncer le clou...

... sauf que cette fois-ci y a rien qui change ! Ou presque...

Modularisation de focus4 (#121)

C'est l'heure pour moi de revenir en arrière toute sur la façon dont Focus est construit et architecturé. Jusqu'à présent il n'y avait qu'un seul module, focus4, qui était buildé de la façon la simple possible avec un simple appel à tsc et quelques copies de fichiers pour conserver le CSS et ses fichiers de définitions TS. C'était un choix délibéré pour ne pas à avoir à traiter la complexité induite par 2 ou 4 modules (v2/v3) pseudo indépendants, aussi bien pour la maintenance que pour l'utilisation.

Cette réarchitcturation de Focus a été motivée par deux raisons principales :

  • Pouvoir donner la possibilité à des projets de ne récupérer que la partie de Focus qui les intéresse, surtout si ce n'est pas pour faire une application React DOM derrière (genre du React Native ou même du Angular/Vue derrière qui ont leurs bindings MobX, si besoin).
  • Gérer plusieurs "versions" de Focus via un même repo, en partageant le plus de code possible. C'est évidemment fortement lié au point précédent puisque ça veut dire qu'on aura de quoi développer par la suite des modules spécifiques React Native/Angular/Vue si un projet en a besoin, mais pas que, puisqu'aujourd'hui on a déjà deux versions de Focus (v8 et v9) qui sont a 80-90% identiques mais fondamentalement incompatibles.

6 (+2) modules

La v2 en avait 2, la v3 en avait 4, donc par extension logique la v4 a été divisée en 6 (+2) nouveaux modules ! Les différents modules sont les suivants :

  • @focus4/core : il contient tous les petits bouts "génériques" de Focus comme le fetch, le routeur, et les petits stores. Il ne dépend d'aucun autre module, ni de React.
  • @focus4/stores : il contient les stores d'entités, de formulaires, de références, et de listes. Il dépend de core mais toujours pas de React.
  • @focus4/styling: il contient les différents outils liés au CSS ainsi que toutes les variables (y compris celles de React Toolbox, on y reviendra plus tard). Il dépend de core et de React (comme tous les modules suivants).
  • @focus4/forms: il contient les composants de saisie, de champs et de formulaires. Il dépend de stores et styling.
  • @focus4/collections: il contient tous les composants de liste de et recherche. Il dépend de forms, puisqu'il existe un formulaire de saisie de critères de recherche.
  • @focus4/layout: il contient tous les composants de layout. Il dépend de styling (mais pas de components ou collections).

Construction d'un module et changements sur le CSS

Un module est désormais proprement packagé en un seul fichier (avec rollup) et le CSS est proprement extrait. Cela veut dire que tous les imports ne peuvent se faire que depuis la racine du module ("@focus4/core" par exemple). Tout est donc exporté depuis la racine. Pour le CSS, cela veut dire que les modules sont déjà compilés (avec des noms de classes non surchageables du coup) et qu'il est possible d'importer les objets générés contenant les classes depuis le module. Par conséquent :

  • Il faut importer le CSS à la main dans votre projet, et de préférence dans un fichier CSS unique avec tous les autres CSS des différents modules pour récupérer les variables CSS, toutes exposées par le module styling (@focus4/styling/lib/variables.css).
  • Il n'y a plus besoin de configurer les modules CSS dans webpack pour utiliser Focus (à moins que vous vouliez en utiliser également). Postcss avec au moins postcss-preset-env et postcss-color-function sont toujours nécessaires. Si vous voulez compiler les variables CSS (nécessaire si vous ciblez IE, sinon pas besoin), il faut ajouter postcss-import et importer le fichier de variables dans tous les CSS qui les utilisent, ainsi que postcss-custom-properties (en version 7.0.0...). Si vous voulez utiliser des modules dans votre projet c'est à vous d'ajouter une config pour le faire (par exemple la même que create-react-app avec .module.css)
  • Puisqu'il n'est plus possible de compiler soi-même les modules, vous ne pouvez plus donc vous arranger pour avoir des noms stables sur les classes (déso). A la place vous êtes obligés de faire comme prévu par le framework en ajoutant vos classes dans les différentes props theme.

@focus4/toolbox

Le 7ème module de Focus est une "réimplémentation" de react-toolbox qui profite du fait que la librairie a eu le bon sens de découpler leur solution de CSS des composants. Ainsi, ce module wrappe totalement RT et package son CSS comme les autres modules, en plus de l'injecter avec des HoC avec des forward refs et des hooks tous propres (cela veut dire qu'il ne faut plus utiliser innerRef mais ref sur les composants RT désormais).

Par conséquent, il faut remplacer tous vos imports de "react-toolbox/lib/****" par "@focus4/toolbox". Ce sont les mêmes composants sinon, à l'innerRef/ref près. Il manque certains composants (ceux qui sont remplacés dans les autres modules de Focus), et il y en a en plus, principalement issus de l'ancien components. Le détail est dans le readme.

@focus4/legacy

Le package legacy est un module supplémentaire à destination des utilisateurs de la v8 qui souhaiteraient se mettre à jour. L'existence de ce package implique que la v8 ne sera plus maintenue, hormis besoin projet prioritaire insolvable autrement...

Ce module implémente les formulaires < v9 à partir des stores v9+. Il permet donc de bénéficier de toute la "modernité" de Focus v9+ sans avoir à réécrire tous vos formulaires pour s'y accommoder.

Néanmoins :

  • La génération de modèle a été mise à jour, c'est désormais la même que la v9+.
  • La gestion des stores (mais pas des formulaires) étant désormais la même que la v9+, cela veut dire que :
    • makeEntityStore et new SearchStore() ont une API claire et qui marche facilement
    • On a maintenant accès à node.replace() et nodeList.replaceNodes() (du coup nodeList.set() n'existe plus), à utiliser à la place des clear/set dégueulasses
    • On a accès à buildNode qui a une API simple pour créer des noeuds à la volée (avant il fallait sortir buildEntityEntry qui prend 4 paramètres imbitables)
    • On a accès à makeField, qui peut remplacer astucieusement les fields construits à la main à base de {$entity, value}. D'ailleurs, $entity à été renommé en $field en v9.
  • Les définitions de domaines ont été mises à jour comme en v0+
  • Il est indispensable d'importer les différents composants de @focus4/legacy pour utiliser des formulaires < v9. En particulier, displayFor, fieldFor et selectFor. Ca ne marchera pas sinon, et les APIs sont de toute façon un peu différentes. AutoForm n'existe que dans ce module donc il n'y aura pas de confusion.
  • Le referenceStore est désormais celui de la v9+. Les listes de références contiennent $valueKey, $labelKey et getLabel, ce qui rend facultatif de renseigner valueKey/labelKey sur les selectFor legacy. Le nouveau selectFor ne permet pas de le faire (il faut utiliser makeReferenceList), ainsi que stringFor, qui n'a pas de version legacy.

De plus, étant déprécié depuis un moment déjà, l'applicationStore a été retiré. Il est désormais obligatoire de construit ses headers à la main.

focus4

Pour simplifier la vie de tout le monde qui souhaite toujours utiliser l'ensemble de Focus dans ses projets et qui n'a pas envie de se poser trop de questions sur la modularisation, le module focus4 existe toujours et se contente de réexporter les différents modules selon la même structure qu'avant. Pour migrer vers la v10, il devrait donc suffire de remplacer tous vos imports de RT par des @focus4/toolbox et, à part les petits ajustements sur le build, tout devrait marcher comme avant.

A noter que l'import "focus4" embarque désormais tout le CSS ainsi que les icônes material (qui étaient dans layout avant). Il pose aussi le shim de core-js, de façon à ce que tout ce que vous ayez à faire et d'ajouter un import "focus4" en haut de votre fichier racine index.js.

Autres évolutions diverses

  • node.set/replace/clear (et les équivalents listes) retournent désormais le noeud au lieu de void. Cela peut permettre de chainer (si besoin) les appels, et surtout d'écrire quelque chose comme const node = buildNode(Entity).set(data).
  • registerHeader et setHeaderProps ont été séparés dans la relation Header/Scrollable. Le Header ne s'enregistre plus dans le Scrollable que lorsqu'il est monté (ou si canDeploy change). Pour le reste, par exemple si le contenu change, il sera simplement mis à jour. Il n'y aura donc plus, dans aucun cas, d'animation sur le Header quand son contenu change. Grosso modo, cela veut dire qu'il est maintenant obligatoire de faire autant de Header que nécessaire, au lieu d'utiliser le même et de changer son contenu par module. C'était déjà la méthode suggérée depuis la (8|9).10 et la refonte, mais c'est désormais la seule façon correcte de faire.
  • Il est désormais possible, dans une définition d'entité, de spécifier, en plus de "field", "object" et "list", "recursive-list" (sans aucune autre propriété) pour créer une sous-liste de l'entité courante. C'était possible en pré-v9 puisqu'on reférençait les entités par leur nom (et non l'entité elle-même) et plus possible après à cause de dépendances circulaires, donc cette fonctionnalité restore cette possibilité.
  • Le typage de makeField a évolué pour avoir moins de problèmes de typage, en séparant le type du champ et celui du domaine.

v9.10 / v8.10

09 May 21:25
Compare
Choose a tag to compare

Cette version concerne aussi bien la v9 que la v8, et d'ailleurs seule cette dernière a fait l'objet d'une release propre, étant donné que la 9.9.0 est toujours en attente...

Cette release propose une résolution à #101, et son objectif est de dégager tous les comportements qui se basaient sur window (le header, le scrollspy, les popins, le scroll infini…) et de les lier à un nouveau composant appelé Scrollable, qui propose son ScrollableContext.

Concrètement, ce que ça signifie pour vous c'est que :

  • il va probablement falloir refaire votre CSS global de layout qui aura peu de chance de fonctionner tel quel. En particulier il n'y a plus un seul position: fixed dans le framework.
  • Il va falloir un peu modifier certains imports
  • Il va falloir refaire vos tableFors
  • Et beaucoup de choses qui ne marchaient pas/étaient impossibles avant vont désormais marcher/être possible.

C'est parti !

Layout = MainMenu + Scrollable

L'existence du composant <Scrollable> implique qu'on ne va désormais plus scroller sur la fenêtre entière mais dans un <Scrollable>. Par conséquent, le Layout a été modifié de façon à ce qu'il prenne toujours 100vw * 100vh, avec le Menu est placé à gauche et le Scrollable à droite. Le menu est donc naturellement fixé à gauche (sans avoir de position: fixed), et il peut sans problème avoir un overflow (s'il y a trop d'items ou si la fenêtre est trop petite) : une scrollbar va apparaître et la partie scrollable sera naturellement réduite. Il n'y a donc plus besoin de calculer et garder en dur la taille du menu pour placer correctement le contenu.

De plus, le composant <LayoutContent> a été retiré car il n'est plus nécessaire (il servait à prendre en compte la taille du menu auparavant). Il posait également des marges, qui sont désormais posées par d'autres composants tels que le ScrollspyContainer ou l'AdvancedSearch. Si vous n'utilisez pas de composant de layout global comme ces ceux-là, un composant appelé simplement <Content> le remplace, dont le simple but est de poser des marges.

Le ErrorCenter a été retiré, clairement il n'était pas très utile (la plupart des erreurs restaient en console) et il affichait souvent des faux positifs.

De façon plus anecdotique, <LayoutFooter> a été également retiré pour la même raison. Si besoin, vous pouvez simplement poser un <footer> HTML standard pour le même résultat.

Pour matérialiser ce changement de Layout, il faut maintenant passer votre composant de menu dans la prop menu du Layout, au lieu de le poser dedans comme un enfant.

Refonte du MainMenu (#102)

C'est le seul point qui ne concerne pas directement <Scrollable> donc on va le traiter en premier.

L'API du MainMenu est inchangée, mais son implémentation a évolué : il n'est plus nécessaire que les <MainMenuItem> soit des enfants directs du <MainMenu>, ou bien d'un autre item (si sous menu).

De plus, la gestion du sous menu, qui vient avec une nouvelle animation (il y aura beaucoup de nouvelles animations dans cette version), étant désormais déléguée au MainMenuItem lui-même, il devient possible (même si ce n'est pas forcément une bonne idée) d'avoir autant de niveau de sous menus que nécessaire.

API du ScrollableContext

Le Scrollable propose une API "publique" dans son contexte, dans le sens ou vous pouvez l'utiliser dans votre projet mais qu'elle existe surtout pour les composants de layout internes de Focus :

  • registerHeader(Header, headerProps, nonStickyHeader, canDeploy) : méthode utilisée par le <HeaderScrolling> pour s'enregistrer dans le <Scrollable>. A priori il n'y a aucune raison de l'utiliser directement puisque c'est presque la seule utilité du composant associé.
  • registerIntersect(node, onIntersect) : cette méthode permet d'enregistrer un handler d'évènement sur un nœud HTML, qui sera appelé régulièrement quand la section visible de ce noeud dans le viewport du <Scrollable> change. En général, via un évènement de scroll, mais pas nécessairement. Cela permet de faire des choses (on verra ce que fait Focus avec plus tard) lorsqu'un élément entre ou quitte le viewport par exemple.
  • scrollTo(options): même chose que window.scrollTo, pour le <Scrollable>. Vous pouvez toujours essayer d'utiliser window.scrollTo mais ça ne marchera plus, puisque la fenêtre est fixe.
  • portal(node, parentNode?) : cette méthode permet de poser un Portal React vers le <Scrollable>. Il y a deux possibilité d'usage :
    • Soit parentNode n'est pas spécifié, dans ce cas le composant sera ajouté au nœud racine du <Scrollable> (la partie fixe).
    • Soit parentNode est renseigné, dans ce cas le nœud sera également ajouté à la partie fixe, mais il sera positionné automatiquement au même niveau que le parentNode, jusqu'à ce que le nœud arrive au sommet du <Scrollable> : il sera bloqué en sticky à ce moment-là.

Contenu et props du <Scrollable>

Il est assez rare que vous ayez à poser votre propre <Scrollable> puisqu'il est posé par le <Layout> ainsi que par les <Popin>. En effet, le fait que chaque <Popin> pose son <Scrollable> implique que tout ce qui va être présenté ici marche également dans une popin ! Bon en effet c'était quand même le but premier recherché…

Néanmoins, les props du Scrollable se retrouve dans les props du Layout et de la Popin, donc ça vaut le coup d'en parler un peu :

  • hideBackToTop : par défaut, le Scrollable affiche un bouton de retour en haut de page, comme le Scrollspy et l'AdvancedSearch le faisait avant. Désormais, il n'existe qu'ici (ce qui est assez logique tout compte fait), et il est possible de le désactiver avec cette prop.
  • backToTopOffset: Offset avant l'apparition du bouton de retour en haut. Par défaut : 200.
  • className: Permet de poser une classe CSS sur le nœud racine. Correspond à theme.container.
  • scrollBehaviour: Comportement du scroll pour scrollTo.
  • resetScrollOnChildrenChange: Réinitialise le scroll (à 0) dès que les children changent. Cette prop est activée pour le scrollable du Layout mais pas pour les autres. En pratique un changement de children sur le layout correspond à un changement de page pour lequel on voudra toujours commencer en haut de page, d'où ce comportement.

Ce qu'on remarque surtout, c'est que ces props et les comportements associés existaient déjà avant, mais ils étaient dans des composants spécialisés (surtout le Scrollspy). Maintenant, tout ceci est standard dans <Scrollable>.

Header

Le header se pose toujours dans le Layout (ou dans un <Scrollable> comme la popin désormais), mais son mode replié s'affiche désormais dans la partie fixe du Scrollable une fois que le header déplié est entièrement sorti de l'écran (via registerIntersect). Il apparaît à l'écran avec une petite animation :

A noter aussi la petite animation lorsque le bouton back to top apparait 😄

De plus, l'animation des boutons d'actions est déclenchée automatiquement par contexte (react-pose c'est vraiment de la bombe), donc si vous voulez changer le composant d'actions pour mettre le votre vous aurez aussi la possibilité de faire votre propre animation.

D'ailleurs, vous pouvez aussi complètement réimplémenter le header facilement, puisque toute la logique est dans le <Scrollable> et que tout ce que fait le <HeaderScrolling> est un useEffect qui appelle context.registerHeader...

A noter qu'il est toujours possible de spécifier canDeploy à false pour avoir un header toujours sticky. Ce header-là n'a pas d'animation (même si vous changez de page avec le même composant de header et juste la prop qui change).

ScrollspyContainer

Le ScrollspyContainer a été déplacé dans le module focus4/layout.

Il utilise désormais registerPortal pour placer le menu en sticky, comme on peut le voir sur l'animation suivante :

A noter qu'il est impossible de reproduire ce comportement dans IE ou Edge à cause d'un délai trop important entre le scroll et le déclenchement de l'évènement associé (aucun problème sous Chrome ou Firefox), donc un mode "dégradé" ou le scroll est "debouncé" avec une animation pour déplacer le menu a été mis au point. Les deux navigateurs étant sur leur lit de mort, tant pis s'ils ont un mode dégradé.

De plus, via registerIntersect, il est possible de correctement déterminer quel Panel est actif à l'écran, ce qui permet (enfin !) de faire marcher ceci :

Sur un sujet similaire, l'AdvancedSearch propose une nouvelle valeur pour facetBoxPosition, "sticky", qui reproduit le comportement du menu du Scrollspy pour la liste des facettes. A noter qu'il vous faudra tout de même surcharger son CSS pour déterminer sa hauteur maximale et l'overflow auto qui va avec.

Scroll infini sur les listes

Le scroll infini fonctionne désormais avec registerIntersect sur le n-ième dernier élément de la liste. Dès qu'il apparaît à l'écran, la suite de la liste est chargée (ce qui résout également #111). La prop offset a donc été remplacée par pageItemIndex qui correspond à ce "n" (par défaut placé à 5).

Il est également disponible sur les listes de groupes de la recherche avancée, via les deux nouvelles props groupListPageSize (défaut à 10) et groupPageItemIndex (défaut à 2).

A noter que pour des limitations techniques (merci React…) il est nécessaire d'utiliser listFor et autres (à la place du composant directement) pour bénéficier du contexte du ListWrapper dans une liste (le truc utilisé dans la recherche avancée pour...

Read more

v9.9

01 May 12:21
Compare
Choose a tag to compare
v9.9 Pre-release
Pre-release

Le but de cette release est d'essayer de combler les derniers points qui posent problème autour des formulaires. En ce sens, elle ne concerne donc que la v9.

Cette version n'est pas encore finalisée car elle est en attente de deux correctifs (côté typescript et côté mobx-react), mais cela ne vous empêchera pas de l'utiliser.

<Input type="number" />

Le composant d'Input standard supporte maintenant un type "number", qui va permettre de saisir des nombres proprement et de gérer des contraintes de formatage. En particulier, les props suivantes ont été ajoutées :

  • type, qui peut valoir "string" ou "number". Le type "string" correspond à l'existant et reste le seul qui supporte les masques de saisies.
  • hasThousandsSeparator, qui va afficher les séparateurs de milliers de la locale courante (via la config de numeraljs) dans le champ d'input.
  • maxDecimals, qui va permettre de limiter le nombre de décimales qu'il est possible de saisir. On peut bien sûr y mettre 0 si on ne veut que des entiers
  • noNegativeNumbers, pour empêcher la saisie de nombres négatifs (qui est autorisée par défaut)

Ces options n'ont pas de lien avec le process de validation, donc il conviendra dans la plupart des cas de renseigner les contraintes dans les props de l'Input et dans le validateur.

A noter que si la locale est "fr", les points sont automatiquement remplacés par des virgules à la saisie, ce qui permet d'utiliser les deux.

fieldType: "number" | "string" | "boolean"

Dans une définition d'entité, il est désormais possible (et fortement conseillé) de renseigner le fieldType d'un champ comme étant "string", "number" ou "boolean", au lieu de mettre systématiquement {} as number. Ce type sera passé à tous les composants d'input (et en particulier le composant standard d'Input qui vient de se doter d'un mode de saisie pour les nombres), ce qui leur permet désormais de retourner directement le bon type dans leurs onChange, au lieu de string systématiquement. A noter que seuls "string" et "number" sont gérés par les composants existants (Input, Autocomplete et les différents Select), et que tout ce qui n'est pas "number" sera considéré comme "string".

Domaines et validateurs

Il faut désormais créer un domaine via la fonction domain<T>(), qui s'utilise de la manière suivante :

const DO_DATE = domain<string>()({
    InputComponent: InputDate,
    inputProps: {
         inputFormat: "DD/MM/YYY"
    }
});

Cela permet de connaître le type du champ dans le domaine, d'arrêter de devoir écrire le type à la main (ce qui est pénible lorsqu'on met des composants custom), et cela va permettre certaines des évolutions suivantes :

displayFormatter à la bonne signature et les bons types d'Input sont choisis

displayFormatter est maintenant (value: T | undefined) => string, au lieu d'avoir un value: any qui pouvait changer selon si la valeur venait d'un onChange ou non (le problème se posait pour les nombres). Un domaine de type number choisira un Input de type "number".

Attention cependant, le type du domaine n'existe que pour Typescript et la vraie valeur de "type" qui sera passée à l'Input sera celle de $field.fieldType. Dans le cas général c'est transparent, mais attention à makeField auquel il faudra préciser le fieldType, sinon cela ne marchera pas comme attendu (et il y aurait une erreur TS).

Exemple :

makeField(() => this.myNumber, {fieldType: "number"} as const, value => this.myNumber = value, true)

Les validateurs sont maintenant liés au type du domaine

FunctionValidator<T> est disponible pour tous les types, NumberValidator pour les nombres et StringValidator, RegexValidator, EmailValidator et DateValidator pour les strings.

NumberValidator remplace les regex

NumberValidator propose désormais les options min, max et maxDecimals (qui remplace isInteger) pour valider les nombres. Il n'est de toute façon plus possible d'utiliser des regex puisqu'elles sont limitées aux strings et qu'on traite enfin les nombres comme des nombres de bout en bout.

inputFormatter et unformatter ont été retirés

S'il y a quelque chose à faire de cet ordre, c'est au composant d'input de s'en charger. En particulier, la quasi-totalité des usages ce ces fonctions sont maintenant gérés nativement dans le composant d'Input.

patchField merge désormais les validateurs

Un validateur ajouté par patchField est maintenant ajouté en plus des éventuels validateurs existants, au lieu de les remplacer. Il n'y a pas vraiment de raison de vouloir retirer un validateur existant et bien souvent on s'embête à vouloir garder les deux. C'est maintenant le comportement standard et on ne peut pas d'en échapper.

cloneField

La fonction cloneField(field, isEdit) a été ajoutée et est un raccourci pour makeField(() => field.value, field.$field, value => field.value = value, isEdit) qui est un pattern utilisé de temps en temps qui pouvait être simplifié.

Dépréciation de util en faveur de @disposeOnUnmount + réaction

Vous êtes désormais priés de ne plus utiliser @classAutorun et consorts et de vous rapatrier sur le décorateur standard @disposeOnUnmount qui s'utilise ainsi :

@disposeOnUnmount
r = autorun(async () => store.replace(await load(store2.id)));

// ou
disposeOnUnmount(this, autorun(/* … */));

Le problème des décorateurs existants est qu'ils ne couvrent qu'une partie des possibilités de réactions, le font plutôt mal (@classReaction en particulier) et qu'ils ne se transfèreront pas vers les nouveaux usages de React dans le futur.

Attention : le correctif attendu sur mobx-react concerne @disposeOnUnmount qui se comporte mal si vous avez plusieurs instances du même composant. Vous pouvez rester sur @classAutorun si vous êtes concernés en attendant la release finale

makeFormNode(this, …) et makeFormActions(this, …)

Sur le même sujet, chacune de ces deux méthodes crée une réaction (synchronisation et chargement), qu'il conviendra de disposer lorsque le composant qui l'a créé sera démonté. Jusqu'à présent, c'était fait dans le <Form> associé aux actions, qui disposait les deux. Maintenant qu'on peut complètement dissocier formNode, actions et Form (depuis la v9 et surtout la 9.7), ce moyen de procéder peut poser beaucoup de problèmes en pratique lorsqu'on commence à vraiment les dissocier (surtout si le <Form> n'est pas dans le même composant que les autres).

Il n'y a pas beaucoup de solution pour contourner le problème, à moins de lier explicitement makeFormNode et makeFormActions à leur composant, et la manière la plus standard (pour ne pas déjà contredire le point précédent) est de passer this en premier paramètre de ces fonctions. En interne, elles appellent simplement disposeOnUnmount(this, reaction) une fois l'objet créé.

(La migration est simple, ça se fait très bien avec un search and replace)

Saves multiple sur makeFormActions

Il est maintenant possible, via la propriété save de ActionConfig, de renseigner plusieurs services de sauvegarde dans les actions. Ils seront accessibles directement sur l'objet d'actions. Cela permet de partager l'état de chargement entre toutes les actions et de ne pas avoir a reproduire ce que fait le save pour d'autres actions "secondaires". Tous les saves ont exactement la même implémentation, ils ne diffèrent que par le service qu'ils appellent.

Exemple :

const actions = makeFormActions({save: {
    default: saveObject, // Le service `default` est obligatoire, ce sera le save par défaut
    delete: deleteObject // Oui, une suppression peut être considérée comme une sauvegarde
});

actions.save();
actions.delete();

referenceStore.item.getLabel() et typage valueKey/labelKey

Les items d'un ReferenceStore sont maintenant munis d'une méthode getLabel qui permet de résoudre un libellé à partir d'un code.

De plus, à condition de typer correctement les types de référence générés, valueKey et labelKey peuvent à nouveau être typé par le nom de la propriété qui correspond. Cela permet de vérifier l'appel à getLabel, ainsi l'usage de makeReferenceList pour être sur que valueKey et labelKey pointent sur des propriétés qui existent.

Mise à jour de la génération

Certaines nouvelles fonctionnalités nécessitent des évolutions de la génération de code. Il faut :

  • Renseigner "number", "string" et "boolean" dans le fieldType quand les types correspondent
  • Typer littéralement fieldType (comme type qui vaut aujourd'hui "field" as "field"), ainsi que valueKey et labelKey dans les liste de référence.

TS 3.4 ajoute une fonctionnalité d'assertion en "const", qui permet de considérer une propriété ou tout en objet comme constant, en ajoutant des readonly partout et surtout en utilisant des types littéraux pour tous les champs, ce qui correspond exactement à ce qu'on souhaite avec nos types générés.

Attention : Il est nécessaire de rester sur la RC (3.4.0-rc) de TS 3.4 car une régression violente c'est introduite dans la version finale qui fait exploser le typage de tous les stores (en v9). Le fix a été réalisé et mergé sur master chez eux mais il n'a pas été encore release.

L'évolution de la génération devrait donc générer maintenant les entités comme

const MyEntity = {
    name: "myEntity",
    fields: {/* … */}
} as const

et les références comme

const myType = {labelKey: "libelle", valueKey: "id", type: {} as MyType} as const

Cela forcerait cependant tout le monde à se mettre à jour vers TS 3.4, mais à priori c'est une mise à jour simple pour tous les projets concernés (qui devraient déjà tous être en 3.3 !).

Breaking changes

Quasiment tout ce dont je parle est un breaking change, mais y a rien de vraiment méchant cette fois-ci (...

Read more

v9.8.3

10 Mar 23:03
Compare
Choose a tag to compare

Fixes :

v9.8.1

18 Feb 00:17
Compare
Choose a tag to compare

Passer un getter de la forme () => boolean à makeFormNode, et à patchField/makeField/patchNodeEdit dans sa fonction d'initialisation remplace désormais l'état interne d'édition du nœud/champ au lieu de s'y ajouter. Cela veut dire qu'appeler node.form.isEdit = true/false ou field.isEdit = true/false n'a plus d'effet dans ce cas, puisque l'état est contrôlé depuis "l'extérieur". En revanche, cela ne permet toujours pas de bypasser l'état d'édition du parent, qui est toujours requis.

Cela ne devrait impacter personne puisque c'était le fonctionnement attendu/compris, et on peut maintenant passer () => true à des nœuds pour qu'ils restent toujours en édition, ce qui n'était pas possible avant (seul () => false fonctionnait).

v9.8 / v8.8

20 Jan 21:13
Compare
Choose a tag to compare

Une partie des travaux présentés dans cette release date en réalité de la 9.7.1, mais n'ayant de toute façon jamais fait l'objet d'une vraie release, on va dire que tout fait partie de la 9.8.0)

Cette release contient un bon nombre d'améliorations et de nouvelles fonctionnalités autour des listes et de la recherche, suite au feedback et aux demandes des projets en cours.

Performances et animations (#96, #99, entre autres)

La liste (ainsi que l'ActionBar dans une moindre mesure) a été partiellement réécrite dans un objectif d'amélioration des performances, en particulier au niveau des animations. La librairie d'animation a été remplacée (react-motion par react-pose), la nouvelle proposant une API simplifiée (plus besoin de calculer des hauteurs à la main, y compris pour vous avec le detailHeight) et de meilleures performances.

Pour assurer des performances correctes et réparer une erreur de conception qui date depuis longtemps, la prop itemKey est maintenant une fonction de data et est désormais obligatoire.

FacetBox : sections et composants d'affichage personnalisés

Sections

La FacetBox peut maintenant être découpée en sections via la nouvelle prop facetSections, qui est une liste de la forme {name: string, facets: string[]}. Cela permet de contrôler l'ordre d'affichage des facettes et de le regrouper par catégories directement depuis la SPA (l'ordre était auparavant forcé par le serveur). Il est possible d'avoir une section sans nom (""), et si elle est renseignée, toutes les facettes non incluses dans une section seront ajoutées dedans.

Il sera probablement question, dans le futur, de proposer de pouvoir "plier" les facettes et les sections, mais on en est pas encore là.

Composants customs (#81)

Via la prop customFacetComponents, il est maintenant possible de remplacer le composant d'affichage standard d'une facette par le votre, qui doit respecter la même API que le composant standard. Libre à vous ensuite d'implémenter le comportement que vous voulez, que ça soit un (ou plusieurs) slider(s), un autocomplete… C'est à vous également de gérer le caractère multi-sélectionnable de la facette.

ActionBar : groupableFacets

Une nouvelle prop a été ajoutée sur l'ActionBar, groupableFacets, qui permet de préciser la liste des facettes sur lesquelles il est possible de grouper. Auparavant, toutes les facettes apparaissaient obligatoirement dans la liste sans pouvoir en enlever.

SearchChip avec chipThemer et chipKeyResolver (#64)

Il est maintenant possible, à travers la recherche avancée, de pouvoir personnaliser les Chips en fonction de leur position, du champ qu'ils représentent ainsi que de la valeur (si applicable) :

  • chipThemer(type, code, value?) => ChipTheme permet de modifier le rendu de n'importe quel chip, par exemple pour afficher une facette en couleur uniquement dans le Summary, ou encore afficher une valeur de filtre en particulier en gras…
  • chipKeyResolver(type, code, value) => Promise<string> permet de fournir une fonction de résolution de clé pour n'importe quelle valeur affichée dans un Chip. Le besoin initial était de pouvoir résoudre des IDs dans des filtres (qui ne bénéficient pas de libellés donnés par le serveur comme les facettes), mais on peut également s'en servir pour remplacer un libellé existant de facette. Le chipKeyResolver sera utilisé pour tous les chips si renseigné, mais il suffit de ne rien retourner pour les cas où on ne veut pas surcharger des libellés.

Améliorations diverses sur les listes

LoadingState

L'état de chargement (le "chargement..." qui apparait en dessous de la liste lors d'une recherche) est maintenant porté par la List (et non la recherche), et est pilotable par le nouvelle prop isLoading si on n'utilise pas un store de recherche. Cela veut dire qu'on n'affiche plus l'empty state pendant le chargement.

Il est également possible, comme pour l'empty state, de surcharger le rendu du loading state par son propre LoadingComponent.

Modes d'affichage pour les boutons d'actions (évoqué dans #95)

L'API des operationLists a été modifiée et étendue pour permettre plus de libertés sur les types de boutons, et propose désormais des "modes" d'affichage. La propriété mode remplace showIcon et isSecondary et peut prendre les valeurs suivantes :

  • "icon" : affiche un IconButton simple
  • "label" : affiche un Button simple
  • "icon-label" : affiche un Button simple avec l'icône (valeur par défaut)
  • "icon-tooltip" : affiche un IconButton avec une tooltip
  • "secondary" : affiche l'action dans la liste des actions secondaires

Le mode mosaïque utilise toujours des boutons "FAB" et ne respectera donc pas les modes d'affichage demandés. Par contre, la tooltip sera bien posée.

Réinitialisation de la pagination quand la liste change (#100)

La pagination interne d'une liste (qui est indépendante de la pagination serveur de la recherche) sera maintenant réinitialisée si le contenu affiché de la liste a été modifié (la pagination ne sera pas réinitialisée si on ne fait qu'ajouter des données à la suite de la liste existante).

Améliorations diverses

Tooltip sur ButtonMenu (évoqué dans #95)

Il est maintenant possible d'afficher une tooltip sur le bouton d'un ButtonMenu.

Personnalisation du boutons d'actions secondaires du header (demande originale de #95)

Via la nouvelle prop secondaryButton du HeaderActions, il est possible de personnaliser entièrement le rendu du bouton des actions secondaires, au même titre qu'il était déjà possible de personnaliser les boutons d'actions principales et les actions secondaires. Cela inclut la possibilité d'y mettre une tooltip.

Bugfixes

#98, 7a26acb

TLDR : Breaking changes

  • itemKey est maintenant une fonction obligatoire pour toutes les listes
  • detailHeight n'existe plus sur la liste (c'est fait automatiquement maintenant)
  • showIcon et isSecondary sont remplacé par mode dans OperationListItem

8.8.0

Focus v4 a beaucoup évolué depuis le lancement de la v9, non rétro-compatible avec le v8, et même si certains de ses changements resteront à jamais inaccessible aux projets bien avancés en v8, il doit quand même y avoir moyen de reporter une bonne partie des améliorations réalisées en dehors du cadre des formulaires.

Cette version 8.8.0, à jour sur la version courante de la v9 (aujourd'hui 9.8.0) a été publiée après de nombreux tests. En particulier, elle contient :

  • React 16 (obligatoire avec react-pose de toute manière)
  • Toutes les autres mises à jour de dépendances, y compris celle de MobX qui a eu des breaking changes.
  • Toutes les évolutions sur les composants et les collections, donc au moins tout ce qui a été présenté dans cette release note.

En plus de ne pas toucher au module entity, cette version ne modifie aucun comportement qui est dérivé de code généré, donc les améliorations sur l'API de fetch et du store de référence sont également absentes.

Le @autobind sur AutoForm a été retiré car il posait des problèmes avec la mise à jour de MobX et de chaînes d'héritage trop longues. Faites attention à bien rebinder ses méthodes à l'utilisation (en particulier this.save...)

La mise à jour d'un projet existant en 8.7 vers 8.8 ne sera probablement pas immédiate, mais l'idée est de limiter le plus possible les breaking changes inutiles et de se concentrer sur l'essentiel.

La mise à jour vers la 8.8 permettra également aux projets qui souhaitent la mener de bénéficier de toutes les nouveautés qui viendront par la suite, genre ça par exemple...

v9.7

28 Nov 19:44
Compare
Choose a tag to compare
v9.7 Pre-release
Pre-release

Nouveautés sur les formulaires

"transform" => "initializer" (#88)

La fonction de transformation de makeFormNode est maintenant appelée initializer, pour refléter le fait que son scope d'action n'est pas limité à du patchField ou du makeField, mais qu'on peut bien s'en servir pour initialiser le nœud de formulaire au sens large (valeurs initiales en particulier)

Création d'un FormNode vide (#85)

Il est maintenant possible de créer un FormNode vide, au lieu de toujours recopier le contenu du nœud source à la création.

La signature de makeFormNode à changé en conséquence et est maintenant makeFormNode(node, {isEdit?, isEmpty?}, initializer).

onClickCancel, onClickEdit (#87)

makeFormActions propose maintenant deux callbacks onClickCancel et onClickEdit à la place de onToggleEdit. L'usage à montré que dans l'écrasante majorité des cas les deux actions provoquaient des effets distincts.

Synchronisation du FormNode (#86)

La synchronisation du FormNode sur son nœud source est maintenant réalisée champ par champ et même si la mise à jour du champ source n'a pas d'effet sur lui-même. Pour référence, jusqu'à présent une simple autorun sur le reset() global sur FormNode était posée à sa création.

Les ajouts et suppressions d'éléments de listes (sur le nœud source, ce qui est quand même peu fréquent en dehors d'un replaceNodes) sont gérés séparément, puisque tous les éléments de la liste du formulaire ne se retrouvent pas forcément dans le nœud source. De façon générale, tout élément ajouté ou supprimé de la liste source sera ajouté ou supprimé (s'il ne l'a pas déjà été) dans la liste du formulaire. Les ajouts/suppressions de la liste du formulaire ne seront annulés que si on réinitialise totalement la liste source, via un replaceNodes ou un reset().

Concrètement, cela apporte deux bénéfices :

  • Une modification d'une partie du nœud source ne réinitialisera que la partie affectée par la modification (au champ près)
  • Une réinitialisation du nœud source via un replace du même contenu (par exemple si je recharge le même contenu dans un load) réinitialisera bien toujours le formulaire, même si le nœud source reste inchangé (au champ près)

De plus, les réactions de réinitialisation seront disposées au démontage du <Form> qui utilise les champs et les listes concernées. Cela correspond au comportement précédent qui disposait la réaction globale au démontage du <Form> qui utilisait le nœud global. Il n'y a pas vraiment d'autre solution, mais je doute fortement que cela pose un problème en pratique.

Breaking changes

  • StoreListNode#$transform => StoreListNode#$initializer
  • makeFormNode(node, isEdit) => makeFormNode(node, {isEdit, isEmpty}). Il faudrait donc retirer les entity.clear() qui traînent et qui sont maintenant inutiles.
  • FormActionsConfig.onToggleEdit => FormActionsConfig.onClickCancel + FormActionsConfig.onClickEdit
  • FormActionsConfig.clearBeforeInit supprimé au profit de isEmpty sur makeFormNode
  • En théorie l'intégralité de #86 est un breaking change, mais en pratique je pense qu'on évitait les cas qui posaient problème et qui sont maintenant améliorés. Il y aurait possiblement des entity.clear() à retirer, ajoutés pour forcer des mises à jour suite à un rechargement.

Exemple de migration

Voir klee-contrib/focus4-starter-kit@c99efb8

Autres

#93 : Migration vers le nouveau contexte React, en vue de la suppression prochaine de l'ancien. De toute façon, depuis qu'il y a static contextType, il n'y avait plus de raison de ne pas migrer.