Champs de signature
Développement
9 août 2024
Jusque récemment, Documenso proposait un ensemble de 5 champs pour la signature de documents : signature, email, nom, date, et un champ de texte pour des informations supplémentaires. Bien que ces champs couvraient les exigences de base pour la signature de documents, nous avons reconnu le besoin d'une plus grande flexibilité et variété.
En conséquence, nous avons décidé d'introduire plusieurs champs supplémentaires, tels que :
(une version améliorée) Champ de texte
Champ numérique
Champ radio
Champ de case à cocher
Champ déroulant/Sélection
Ces nouveaux champs apportent plus de flexibilité et de variété à Documenso. En tant que propriétaire du document, ils vous permettent de rassembler des informations plus spécifiques ou supplémentaires de la part des signataires.
Introduction de Nouveaux Champs
Examinons de plus près chaque type de nouveau champ.
Champ de Texte
Bien que le champ de texte ait déjà été disponible, il ne pouvait pas être configuré. C'était une simple boîte de saisie où les signataires pouvaient entrer une seule ligne de texte.
L'image illustre l'ancien champ de texte dans l'éditeur de documents.
Le champ de texte rénové offre maintenant une gamme d'options de configuration, vous permettant de :
Ajouter une étiquette, un texte d'espace réservé, du texte par défaut, et une limite de caractères
Définir le champ comme requis ou en lecture seule
Du côté de la signature, le champ est resté visuellement presque le même. La seule chose qui a changé est la fonctionnalité, qui doit prendre en compte les règles de validation. Par exemple, si le champ est requis, le signataire doit entrer une valeur pour le signer. Ou, si le champ a une limite de caractères, la valeur entrée par le signataire ne doit pas dépasser cette limite.
L'image ci-dessous illustre quatre champs de texte différents avec diverses configurations.
Le premier champ de texte n'a pas de valeur par défaut ("Ajouter du texte") ou de configuration. Vous pouvez signer le champ en entrant n'importe quel texte.
Le deuxième champ de texte, "label-1"/"text-1", a les configurations suivantes :
Étiquette
Texte d'espace réservé
Texte par défaut
Limite de caractères
Étant donné qu'il y a une valeur par défaut, le champ se signe automatiquement avec cette valeur. Cependant, vous pouvez resigné le champ avec une nouvelle valeur qui ne dépasse pas la limite de caractères.
Le troisième champ, "label-2"/"text-2", a les mêmes configurations que le deuxième, avec une addition - l'option requise
est cochée. Lorsque le champ est marqué comme requise
, vous devez le signer avant de compléter le document.
En dehors de cela, il fonctionne comme le deuxième champ.
Le quatrième champ, "label-3"/"text-3", a les mêmes configurations que le deuxième, avec une addition — lecture seule
est cochée. Cela signifie que le champ se signe automatiquement avec la valeur par défaut, et vous ne pouvez pas le modifier.
Champs Non Signés
Vous pouvez annuler la signature d'un champ pour changer la valeur et le signer à nouveau. L'état non signé du champ varie en fonction de sa configuration :
Si le champ a une étiquette, celle-ci s'affiche au lieu de "Ajouter du texte" lorsqu'il est non signé.
Si le champ a une valeur par défaut, la valeur par défaut sera affichée lorsqu'il est non signé.
Si le champ a à la fois une étiquette et une valeur par défaut, l'étiquette prendra le pas et sera affichée lorsqu'il est non signé.
L'image ci-dessous montre l'état non signé des champs de texte.
L'unique exception est le quatrième champ, en lecture seule, qui ne peut pas être non signé ou modifié.
Champ Numérique
Nous avons également introduit un nouveau "Champ Numérique" pour insérer et signer des documents avec des valeurs numériques. Ce champ aide à collecter des quantités, des mesures, et d'autres données mieux représentées sous forme de nombres.
Le "Champ Numérique" offre une gamme d'options de configuration, ce qui vous permet de :
Définir une étiquette, un texte d'espace réservé et une valeur par défaut
Spécifier le format du nombre
Marquer le champ comme requis ou lecture seule
Spécifier les valeurs minimales et maximales
Le champ Numérique ressemble et fonctionne de manière similaire au Champ de Texte. La différence est qu'il n'accepte que des valeurs numériques et a 2 configurations supplémentaires : le format du nombre et les valeurs minimales et maximales.
Champ Radio
Les boutons radio permettent aux signataires de sélectionner une seule option d'une liste prédéfinie que le propriétaire du document définit.
Avant d'envoyer le document pour signature, vous devez ajouter au moins une option radio, qui peut contenir une chaîne ou une valeur vide et peut être cochée ou décochée. Cependant, il est important de noter qu'une seule option peut être cochée à la fois.
En ce qui concerne la configuration du champ, vous pouvez marquer le champ comme requis ou lecture seule.
L'image ci-dessous montre ce que voit le signataire après que le document a été envoyé pour signature.
Remarque : L'image est modifiée pour afficher à la fois les états non signés et signés du champ.
Étant donné que le champ a une option présélectionnée (option radio-val-2-checked
), il signera automatiquement avec cette valeur et apparaîtra comme le champ marqué avec le numéro 1.
Si le champ n'est pas en lecture seule, le signataire peut :
Annuler la signature du champ et choisir une autre option en cliquant dessus.
Resigner avec la valeur par défaut en rafraîchissant la page lorsque le champ est non signé.
Cependant, si le champ est marqué comme étant en lecture seule, le signataire ne peut pas modifier la valeur présélectionnée.
Champ Déroulant/Sélection
Nous avons également introduit un nouveau champ "Déroulant/Sélection" qui permet aux signataires de choisir une option parmi une liste prédéfinie de choix. Ce type de champ est idéal dans des scénarios avec des options valides limitées, comme sélectionner un pays, un état ou une catégorie.
Lors de la configuration d'un champ "Déroulant/Sélection", vous pouvez :
Ajouter plusieurs options
Marquer le champ comme requis ou lecture seule
Choisir une option par défaut parmi la liste de choix
Sur la page de signature, le champ "Déroulant/Sélection" apparaît comme indiqué ci-dessous :
Voici comment le champ "Déroulant/Sélection" fonctionne :
Si aucune valeur par défaut n'est définie, le champ ne se signe pas automatiquement. Le signataire doit cliquer sur le champ et choisir une option dans la liste déroulante pour le signer.
Après signature, le champ affiche la valeur sélectionnée, similaire à un champ de texte signé.
Si le champ est marqué comme requis, les signataires doivent sélectionner une valeur avant de compléter le processus de signature.
Si le champ est marqué comme lecture seule, les signataires peuvent voir la valeur sélectionnée mais ne peuvent pas la modifier.
Champ de Case à Cocher
Le dernier champ introduit est le champ "Case à Cocher", qui permet aux signataires de sélectionner plusieurs options dans une liste prédéfinie. Ce champ est utile dans des scénarios où les signataires doivent choisir plusieurs éléments ou accepter plusieurs conditions, par exemple.
Avant d'envoyer le document pour signature, vous devez ajouter au moins une option de case à cocher. Cette option peut contenir une chaîne ou une valeur vide et peut être cochée ou décochée. Contrairement au champ "Radio", le champ "Case à Cocher" peut avoir plusieurs options cochées.
Comme les autres champs, vous pouvez marquer le champ "Case à Cocher" comme requis ou lecture seule. De plus, il a également un champ de validation, et vous pouvez spécifier combien de cases à cocher le signataire doit signer :
Sélectionner au moins X (un nombre de 1 à 10)
Sélectionner au maximum X (un nombre de 1 à 10)
Sélectionner exactement X (un nombre de 1 à 10)
Lorsque le signataire reçoit le document, il verra le champ "Case à Cocher" comme montré ci-dessous :
L'image illustre les deux états du champ - signé et non signé. Dans cet exemple, le champ 'Case à Cocher' a deux options cochées par défaut, donc il se signe automatiquement.
Le champ marqué '1' apparaît lorsque le signataire visite la page pour la première fois ou lorsque l'utilisateur rafraîchit la page et qu'aucune option n'est sélectionnée. Le champ marqué '2' affiche l'état vidé, où toutes les sélections ont été désélectionnées. Cela montre à quoi ressemble le champ lorsque l'utilisateur efface toutes les sélections.
Dans cet exemple, aucune règle de validation n'a été définie, permettant au signataire de sélectionner n'importe quelle option. Cependant, lorsqu'une règle de validation est appliquée, les signataires doivent respecter les critères spécifiés pour compléter le processus de signature.
Défis de Développement
L'introduction de ces nouveaux champs n'a pas été sans ses défis. Les principaux défis étaient :
Déterminer comment stocker les nouvelles informations pour les champs dans la base de données
Differenciation des destinataires utilisant des couleurs
Stocker les paramètres avancés pour les champs locaux sur le frontend
Implémentation des champs de Case à Cocher et Radio
1er Défi : Stocker les Informations des Nouveaux Champs
Le premier défi a été de décider comment stocker les informations supplémentaires pour chaque nouveau champ dans la base de données. Chaque champ a des propriétés uniques, seules requises
et lecture seule
étant partagées par tous les champs avancés.
Le modèle existant Field
dans la base de données ressemble à ceci :
Au départ, nous avions envisagé de créer une nouvelle table FieldMeta
avec des colonnes pour chaque propriété de champ. Cependant, cette approche présente 2 problèmes.
Tout d'abord, les champs avancés ne partagent que deux propriétés communes : requises
et lecture seule
. Étant donné que toutes les autres propriétés sont uniques à chaque type de champ, cela entraînerait de nombreuses colonnes nullables dans le modèle FieldMeta
.
Deuxièmement, la création d'une nouvelle table de base de données avec des colonnes pour chaque propriété de champ et les relations associées augmenterait la complexité de la base de données.
En conséquence, nous avons décidé de chercher une autre solution qui fonctionnerait mieux avec notre cas d'utilisation.
Solution : Champ JSONB
Étant donné que les données des paramètres avancés sont uniques à chaque champ, nous avons décidé de les stocker sous forme de JSON en utilisant le type de données JSONB
de PostgreSQL. Nous avons ajouté une nouvelle propriété optionnelle fieldMeta
de type JSONB
au modèle Field :
Cette approche nous permet de stocker les paramètres de chaque champ sous forme d'objet JSON. Nous utilisons des schémas Zod pour analyser et valider les métadonnées du champ lors de la lecture ou de l'écriture dans la base de données pour garantir l'intégrité des données.
Cette approche a plusieurs avantages :
Consistance : L'application utilise le même schéma Zod pour récupérer et insérer des données dans la base de données. Cela signifie que les données sont cohérentes à travers l'application.
Sécurité des types : En analysant les données avec Zod, nous pouvons garantir que les données correspondent aux types et à la structure attendus. Nous pouvons également utiliser l'utilitaire
infer
de Zod pour permettre une forte typage et une autocomplétion.Meilleure gestion des erreurs : Zod fournit des messages d'erreur détaillés indiquant quelle partie des données est invalide. Cela rend le débogage et la résolution de problèmes plus faciles et plus rapides.
Maintenabilité : La réutilisation du même schéma Zod pour récupérer et insérer des données dans la base de données rend la structure des données plus facile à maintenir.
Cependant, l'utilisation de JSONB
a également des inconvénients, comme la consultation des données. Étant donné que les données sont stockées sous forme de JSON (plus précisément, au format binaire), les requêtes complexes peuvent être moins efficaces comparées à la consultation de données relationnelles normalisées. De plus, la consultation des données nécessite des opérateurs et des fonctions spécifiques, tels que ->
, ->>
, @>
, et ?
. Cela rend les requêtes plus verbeuses et moins intuitives, et par conséquent, elles nécessitent plus de précision.
Un autre inconvénient est la surcharge de stockage. Les données JSONB
sont stockées dans un format binaire, ce qui peut entraîner une certaine surcharge de stockage par rapport à des données relationnelles normalisées. Dans les cas où les données JSON sont volumineuses ou contiennent beaucoup d'informations redondantes, la surcharge de stockage peut être significative.
Malgré ces inconvénients, le type JSONB
convient à notre cas d'utilisation, car les informations meta de champ sont relativement petites et ne nécessitent pas de requêtes complexes. La flexibilité de JSONB
correspond à la nature dynamique du champ Meta.
Postgres fournit 2 champs pour stocker des données JSON -
json
etjsonb
. Pour plus d'informations, vous pouvez consulter la documentation.
2ème Défi : Stockage des Paramètres Avancés des Champs sur le Frontend
Le défi suivant était de trouver le meilleur moyen de stocker les paramètres avancés des champs saisis par les utilisateurs.
Actuellement, l'application ne sauvegarde que les champs et les paramètres associés dans la base de données lorsque l'utilisateur passe à l'étape suivante.
Les champs sont stockés localement jusqu'à ce que l'utilisateur passe à l'étape suivante. Cela signifie que tous les champs et leurs paramètres sont perdus lorsque l'utilisateur :
Ferme l'onglet des paramètres avancés
Rafraîchit la page
Ferme l'onglet
Navigue vers l'étape précédente
À l'avenir, nous prévoyons d'améliorer ce flux et de sauvegarder les champs au flou, préservant ainsi les données de l'utilisateur même s'il navigue ailleurs. Cependant, jusqu'à ce moment-là, nous avions besoin d'une solution pour sauvegarder les paramètres avancés lorsque l'utilisateur ferme l'onglet de paramètres.
Solution : Stockage Local
Notre solution temporaire consiste à stocker les paramètres avancés dans le stockage local, car les champs ne sont disponibles que localement. Si les champs avaient été sauvegardés dans la base de données, nous aurions pu stocker les paramètres avancés à côté d'eux.
Comme les champs ne sont pas sauvegardés dans la base de données, nous devons persister les données jusqu'à ce que l'utilisateur passe à l'étape suivante, à quel point les données sont sauvegardées dans la base de données. Stocker les données dans le stockage local permet aux utilisateurs d'ouvrir, fermer et configurer divers champs dans l'onglet de paramètres avancés sans perdre d'informations.
Lorsque l'utilisateur passe à l'étape suivante, les champs et leurs paramètres avancés sont sauvegardés dans la base de données, et le stockage local est effacé.
Nous avons également reconnu les dangers de sauvegarder des données dans le stockage local, car les utilisateurs pourraient les modifier et casser l'application. En conséquence, nous avons mis en œuvre de vastes vérifications tant sur le backend que sur le frontend, en plus d'analyser et de valider les données avec Zod.
Cependant, cette solution a des limites. Les données sont toujours perdues lorsque l'utilisateur :
Rafraîchit la page
Navigue vers l'étape précédente
Ferme le navigateur
Dans ces cas, les champs sont supprimés du document. Une amélioration future pour sauvegarder les champs dans la base de données au flou résoudra ce problème.
3ème Défi : Champs Radio et Case à Cocher
L'implémentation des champs Radio et Case à Cocher a été difficile tant d'un point de vue logique que design. Les deux champs peuvent contenir des valeurs vides et non vides, et le champ Case à Cocher permet aux utilisateurs de sélectionner plusieurs valeurs vides/non vides.
L'image ci-dessus montre les champs Radio et Case à Cocher dans l'éditeur de documents. Le champ Radio à gauche a 4 options, dont 1 est cochée. Le champ Case à Cocher à droite a 4 options, dont 2 sont cochées.
Le champ Radio était plus facile à implémenter car les utilisateurs ne peuvent sélectionner qu'une seule option, ce qui entraîne une logique plus simple. Le signataire clique sur une option pour la choisir, et le champ se signe automatiquement avec cette valeur. Pour changer la sélection, l'utilisateur clique sur une autre option, annulant la signature du champ et le resignant avec la nouvelle valeur.
Le champ Case à Cocher était plus difficile car :
Les signataires peuvent sélectionner plusieurs options simultanément, ce qui entraîne que le champ contient plusieurs valeurs.
Il peut avoir des règles de validation (par exemple, sélectionner au moins, au maximum, ou exactement X options).
Les utilisateurs peuvent cocher/décocher les options en cliquant dessus ou vider le champ avec un bouton.
Ces facteurs rendent le champ Case à Cocher plus complexe et plus difficile à implémenter correctement.
Solution
Au lieu de se concentrer sur une solution spécifique, nous discuterons de l'implémentation générale et de ses aspects les plus difficiles. Je fournirai un lien vers l'implémentation complète pour chaque champ afin que vous puissiez la consulter.
Champ Radio
Le fonctionnement de la signature pour le champ Radio consiste à extraire les données de la base de données et à afficher les options disponibles. Si le champ a une valeur par défaut définie par l'expéditeur du document, il se signe automatiquement avec cette valeur.
Vous pouvez voir l'implémentation complète du champ radio dans le fichier radio-field.tsx.
Si le champ n'est pas en lecture seule et que l'utilisateur clique sur une autre option, le champ annule la signature et se resignera avec la nouvelle valeur. Les champs en lecture seule ne peuvent pas être modifiés.
La valeur est sauvegardée dans la base de données chaque fois que le champ est signé, que ce soit par auto-signature ou par l'utilisateur. De même, la valeur est supprimée de la base de données lorsque le champ est non signé.
Étant donné que le champ Radio peut contenir des valeurs vides, nous parcourons les valeurs et remplaçons les vides par une chaîne unique empty-value-${item.id}
. Cela est dû au fait que la chaîne vide n'est pas une valeur valide pour le champ, et nous devons différencier entre les valeurs vides et non vides.
Champ Case à Cocher
L'implémentation du champ Case à Cocher est similaire à celle du champ Radio, avec les principales différences suivantes :
Les champs Case à Cocher peuvent contenir plusieurs valeurs.
Les champs Case à Cocher ont des règles de validation qui doivent être appliquées.
Ensuite, nous récupérons la règle de validation et la longueur de la base de données et trouvons le signe de validation correspondant (par exemple, ">=", "=", "\<=") en fonction de l'étiquette de la règle. Le tableau checkboxValidationSigns
associe les étiquettes de règles à leurs signes correspondants.
Ensuite, nous vérifions si la condition de longueur est remplie en fonction de la règle de validation, du signe et de la longueur. Si elle est remplie, l'utilisateur peut procéder à la signature du champ. Sinon, il doit sélectionner le nombre correct d'options.
En résumé, le champ Case à Cocher permet aux signataires de sélectionner plusieurs options, le champ signant automatiquement en fonction de ces sélections. Les signataires peuvent annuler la signature du champ en désélectionnant les options ou en effaçant toutes les sélections. Le système applique les règles de validation tout au long de ce processus, garantissant que les signataires sélectionnent le nombre requis d'options pour signer avec succès le champ.
Vous pouvez voir l'implémentation complète du champ Case à Cocher dans le fichier checkbox-field.tsx.
4ème Défi : Couleurs des Destinataires
Un autre défi que nous avons rencontré était d'utiliser des couleurs pour différencier les destinataires. Nous avions besoin de générer et de réutiliser dynamiquement les mêmes classes Tailwind à travers plusieurs composants. Cependant, TailwindCSS n'inclut que les classes CSS utilisées dans le projet, supprimant celles non utilisées de la construction finale. Cela a entraîné des couleurs qui n'étaient pas appliquées aux composants, car les classes n'étaient pas utilisées dans le code.
Les images ci-dessous illustrent les couleurs des destinataires dans 2 états différents.
Dans la première image, le champ "Signature" à droite est dans l'état actif (bleu), déclenché lorsque l'utilisateur clique sur le champ pour le faire glisser sur le document. Le champ de signature à gauche, placé sur le document, est dans l'état normal.
La première image illustre le champ "Signature" dans l'état actif, déclenché lorsque l'utilisateur clique dessus.
La deuxième image montre le champ "Signature" dans l'état normal.
L'éditeur de documents est composé de divers composants (champs, destinataires, etc.), ce qui signifie que les mêmes couleurs et le même code sont réutilisés à travers plusieurs composants.
Le code ci-dessus montre une solution naïve utilisant un objet combinedStyles
contenant des classes TailwindCSS pour divers styles de composants (anneau, bordure, survol, etc.).
Les composants utiliseraient des hooks personnalisés pour appliquer les styles appropriés en fonction du destinataire sélectionné. Par exemple, le destinataire 1 utiliserait les styles green-500
, turnant tous les éléments associés en vert.
Le problème avec cette approche est que nous ne pouvons pas importer l'objet combinedStyles
dans d'autres composants car TailwindCSS supprimera les classes inutilisées. Cela signifie que nous devions copier et coller le même objet dans plusieurs fichiers. En conséquence, cela pollue le code avec du code dupliqué, ce qui rend plus difficile la maintenance et l'évolutivité du code. À mesure que l'application grandit, l'objet combinedStyles
deviendra plus grand et plus complexe. De plus, ce n'est pas très flexible, car cela ne permet pas de personnaliser facilement les couleurs.
Bien que cette approche fonctionne, il existe une solution plus efficace et évolutive.
Solution : Modulariser la Logique et Utiliser des Variables CSS
Pour résoudre le défi de la réutilisation des couleurs à travers les composants, nous avons déplacé les couleurs et les hooks associés dans un fichier séparé, définissant les styles uniquement dans ce fichier et y accédant à partir des composants via des hooks personnalisés.
Le fichier a été tronqué pour des raisons de lisibilité. Vous pouvez voir le code complet dans le fichier signer-colors.ts du dépôt Documenso.
L'objet SIGNER_COLOR_STYLES
contient les styles pour chaque couleur, tels que les couleurs de fond, de bordure et de survol. En fonction de l'index du signataire, le hook useSignerColors
récupère les styles pour une couleur spécifique. La fonction getSignerColorStyles
est une fonction d'assistance qui renvoie les styles pour un signataire particulier.
Maintenant, les composants peuvent accéder aux couleurs et aux styles en utilisant des hooks personnalisés. Par exemple, pour obtenir les styles d'un signataire spécifique, le composant peut appeler le hook useSignerColors
avec l'index du signataire.
Le hook renverra les styles pour ce signataire, qui peuvent ensuite être appliqués au composant. Par exemple, vous pouvez accéder à la couleur de fond du signataire en utilisant signerStyles.default.background
.
Cette approche facilite la gestion des couleurs et des styles, car ils sont définis dans un seul fichier. Changer ou ajouter des couleurs peut être fait en un seul endroit, rendant le code plus modulaire et réutilisable.
Nous avons également opté pour des variables CSS pour définir les couleurs, permettant plus de flexibilité et de cohérence dans le style. Une seule variable CSS pour chaque couleur peut couvrir une large gamme d'états sans dépendre de multiples classes TailwindCSS. Par exemple, vous pouvez facilement définir l'opacité et la luminosité d'une couleur sans utiliser de multiples classes. Les variables CSS aident à aligner les couleurs avec nos directives de marque tout en simplifiant l'ensemble du processus de stylisation.
La Fin
Nous sommes heureux de voir les nouveaux champs avancés publiés car ils offrent à nos utilisateurs plus de flexibilité, de variété et d'options de personnalisation. La mise en œuvre des nouveaux champs a eu ses défis, mais nous les avons surmontés et avons appris d'eux. Nous sommes impatients de continuer à améliorer Documenso et à fournir à nos utilisateurs la meilleure expérience de signature de documents.