Firmar campos
Desarrollo
9 ago 2024
Hasta recientemente, Documenso proporcionaba un conjunto de 5 campos para la firma de documentos: firma, correo electrónico, nombre, fecha y un campo de texto para información adicional. Si bien estos campos cubrían los requisitos básicos para la firma de documentos, reconocimos la necesidad de más flexibilidad y variedad.
Como resultado, hemos decidido introducir varios campos adicionales, tales como:
(mejorado) Campo de texto
Campo numérico
Campo de radio
Campo de casilla de verificación
Campo de lista desplegable/selección
Estos nuevos campos aportan más flexibilidad y variedad a Documenso. Como propietario del documento, le permiten reunir información más específica o adicional de los signatarios.
Introducción de Nuevos Campos
Examinemos más de cerca cada tipo de campo nuevo.
Campo de Texto
Si bien el campo de texto estaba disponible anteriormente, no podía configurarse. Era un simple cuadro de entrada donde los signatarios podían ingresar una sola línea de texto.
La imagen ilustra el antiguo campo de texto en el editor de documentos.
El campo de texto renovado ahora ofrece una variedad de opciones de configuración, permitiéndole:
Agregar una etiqueta, un marcador de posición, texto predeterminado y un límite de caracteres
Marcar el campo como obligatorio o de solo lectura
En el lado de la firma, el campo siguió siendo mayormente el mismo visualmente. Lo único que cambió es la funcionalidad, que necesita considerar las reglas de validación. Por ejemplo, si el campo es obligatorio, el firmante debe ingresar un valor para firmarlo. O, si el campo tiene un límite de caracteres, el valor ingresado por el firmante no debe exceder el límite.
La imagen a continuación ilustra cuatro campos de texto diferentes con varias configuraciones.
El primer campo de texto no tiene un valor predeterminado ("Agregar texto") ni configuración. Puede firmar el campo ingresando cualquier texto.
El segundo campo de texto, "label-1"/"text-1", tiene las siguientes configuraciones:
Etiqueta
Marcador de posición
Texto predeterminado
Límite de caracteres
Como hay un valor predeterminado, el campo se firma automáticamente con ese valor. Sin embargo, puede volver a firmar el campo con un nuevo valor que no exceda el límite de caracteres.
El tercer campo, "label-2"/"text-2", tiene las mismas configuraciones que el segundo, con una adición: la opción `required` está seleccionada. Cuando el campo está marcado como `required`, debe firmarlo antes de completar el documento.
Aparte de eso, funciona como el segundo campo.
El cuarto campo, "label-3"/"text-3", tiene las mismas configuraciones que el segundo, con una adición: read-only
está seleccionado. Eso significa que el campo se firma automáticamente con el valor predeterminado y no puede modificarlo.
Campos No Firmados
Puedes desfirmar un campo para cambiar el valor y firmarlo nuevamente. El estado no firmado del campo varía según su configuración:
Si el campo tiene una etiqueta, la muestra en lugar de "Agregar texto" cuando no está firmado.
Si el campo tiene un valor predeterminado, el valor predeterminado se mostrará cuando no esté firmado.
Si el campo tiene tanto una etiqueta como un valor predeterminado, la etiqueta tendrá prioridad y se mostrará cuando no esté firmado.
La imagen a continuación muestra el estado no firmado de los campos de texto.
La única excepción es el cuarto campo, de solo lectura, que no se puede desfirmar ni modificar.
Campo Numérico
También introdujimos un nuevo campo "Número" para insertar y firmar documentos con valores numéricos. Este campo ayuda a recopilar cantidades, medidas y otros datos que se representan mejor como números.
El campo "Número" ofrece una variedad de opciones de configuración, lo que le permite:
Establecer una etiqueta, un marcador de posición y un valor predeterminado
Especificar el formato del número
Marcar el campo como obligatorio o de solo lectura
Especificar valores mínimos y máximos
El campo Número se ve y funciona de manera similar al campo de Texto. La diferencia es que acepta solo valores numéricos y tiene 2 configuraciones adicionales: el formato del número y los valores mínimos y máximos.
Campo de Radio
Los botones de radio permiten a los firmantes seleccionar una única opción de una lista predefinida establecida por el propietario del documento.
Antes de enviar el documento para su firma, debe agregar al menos una opción de radio, que puede contener una cadena o un valor vacío y puede estar seleccionada o deseleccionada. Sin embargo, es importante tener en cuenta que solo se puede seleccionar una opción a la vez.
En lo que respecta a la configuración del campo, puede marcar el campo como obligatorio o de solo lectura.
La imagen a continuación muestra lo que ve el firmante después de que el documento se envía para su firma.
Nota: La imagen se ha modificado para mostrar tanto los estados no firmados como firmados del campo.
Dado que el campo tiene una opción preseleccionada (opción radio-val-2-checked
), se firmará automáticamente con ese valor y aparecerá como el campo marcado con el número 1.
Si el campo no es de solo lectura, el firmante puede:
Desfirmar el campo y elegir otra opción haciendo clic en ella.
Re-firmar con el valor predeterminado al actualizar la página cuando el campo no está firmado.
Sin embargo, si el campo está marcado como de solo lectura, el firmante no puede modificar el valor preseleccionado.
Campo de Lista Desplegable/Selección
También hemos introducido un nuevo campo "Lista Desplegable/Selección" que permite a los firmantes elegir una opción de una lista predefinida de opciones. Este tipo de campo es ideal para escenarios con opciones válidas limitadas, como seleccionar un país, estado o categoría.
Al configurar un campo "Lista Desplegable/Selección", puede:
Agregar múltiples opciones
Marcar el campo como obligatorio o de solo lectura
Elegir una opción predeterminada de la lista de opciones
En la página de firma, el campo "Lista Desplegable/Selección" aparece como se muestra a continuación:
Así es como funciona el campo "Lista Desplegable/Selección":
Si no se establece un valor predeterminado, el campo no se firmará automáticamente. El firmante debe hacer clic en el campo y seleccionar una opción de la lista desplegable para firmarlo.
Después de firmar, el campo muestra el valor seleccionado, similar a un campo de texto firmado.
Si el campo está marcado como obligatorio, los firmantes deben seleccionar un valor antes de completar el proceso de firma.
Si el campo está marcado como de solo lectura, los firmantes pueden ver el valor seleccionado, pero no pueden modificarlo.
Campo de Casilla de Verificación
El último campo introducido es el campo "Casilla de Verificación", que permite a los firmantes seleccionar múltiples opciones de una lista predefinida. Este campo es útil para escenarios donde los firmantes necesitan elegir múltiples elementos o aceptar varios términos y condiciones, por ejemplo.
Antes de enviar el documento para su firma, debe agregar al menos una opción de casilla de verificación. Esta opción puede contener una cadena o un valor vacío y puede estar seleccionada o deseleccionada. A diferencia del campo "Radio", el campo "Casilla de Verificación" puede tener múltiples opciones seleccionadas.
Al igual que otros campos, puede marcar el "Checkbox" como obligatorio o de solo lectura. Además de eso, también tiene un campo de validación, y puede especificar cuántas casillas debe firmar el firmante:
Seleccionar al menos X (un número de 1 a 10)
Seleccionar como máximo X (un número de 1 a 10)
Seleccionar exactamente X (un número de 1 a 10)
Cuando un firmante recibe el documento, verá el campo "Casilla de Verificación" como se muestra a continuación:
La imagen ilustra ambos estados del campo: firmado y no firmado. En este ejemplo, el campo 'Casilla de Verificación' tiene dos opciones seleccionadas por defecto, por lo que se firma automáticamente.
El campo marcado '1' aparece cuando el firmante visita la página por primera vez o cuando el usuario actualiza la página y no se selecciona ninguna opción. El campo marcado '2' muestra el estado despejado, donde todas las opciones han sido deseleccionadas. Esto muestra cómo se ve el campo cuando un usuario borra todas las selecciones.
En este ejemplo, no se ha establecido ninguna regla de validación, lo que permite al firmante seleccionar cualquier opción. Sin embargo, cuando se aplica una regla de validación, los firmantes deben cumplir con los criterios especificados para completar el proceso de firma.
Desafíos de Desarrollo
La introducción de estos nuevos campos no estuvo exenta de desafíos. Los principales desafíos fueron:
Decidir cómo almacenar la nueva información para los campos en la base de datos
Diferenciar a los destinatarios usando colores
Almacenar la configuración avanzada para los campos locales en el frontend
Implementar los campos de Checkbox y Radio
1er Desafío: Almacenar Nueva Información de Campo
El primer desafío fue decidir cómo almacenar la información adicional para cada nuevo campo en la base de datos. Cada campo tiene propiedades únicas, teniendo solo `required` y `read-only` en común entre todos los campos avanzados.
El modelo `Field` existente en la base de datos se ve así:
Inicialmente, consideramos crear una nueva tabla FieldMeta
con columnas para cada propiedad de campo. Sin embargo, este enfoque tiene 2 problemas.
Primero, los campos avanzados solo comparten dos propiedades comunes: required
y read-only
. Dado que todas las demás propiedades son únicas para cada tipo de campo, esto resultaría en muchas columnas anulables en el modelo FieldMeta
.
En segundo lugar, crear una nueva tabla de base de datos con columnas para cada propiedad de campo y las relaciones asociadas aumentaría la complejidad de la base de datos.
Como resultado, decidimos buscar otra solución que funcionara mejor con nuestro caso de uso.
Solución: Campo JSONB
Dado que los datos de configuración avanzados son únicos para cada campo, decidimos almacenarlos como JSON utilizando el tipo de dato `JSONB` de PostgreSQL. Agregamos una nueva propiedad opcional `fieldMeta` de tipo `JSONB` al modelo Field:
Este enfoque nos permite almacenar la configuración de cada campo como un objeto JSON. Utilizamos esquemas Zod para analizar y validar los metadatos del campo al leer o escribir en la base de datos para garantizar la integridad de los datos.
Este enfoque tiene varios beneficios:
Consistencia: La aplicación utiliza el mismo esquema Zod para recuperar e insertar datos en la base de datos. Eso significa que los datos son consistentes en toda la aplicación.
Seguridad de tipo: Al analizar los datos con Zod, podemos garantizar que los datos coincidan con los tipos y estructuras esperados. También podemos utilizar la utilidad `infer` de Zod para habilitar un fuerte tipado y autocompletado.
Mejor manejo de errores: Zod proporciona mensajes de error detallados que indican qué parte de los datos es inválida. Esto facilita y acelera la depuración y la solución de problemas.
Mantenibilidad: Reutilizar el mismo esquema Zod para recuperar e insertar datos en la base de datos facilita el mantenimiento de la estructura de datos.
Sin embargo, el uso de `JSONB` también tiene inconvenientes como la consulta de datos. Dado que los datos se almacenan como JSON (más específicamente, en formato binario), las consultas complejas pueden ser menos eficientes en comparación con la consulta de datos relacionales normalizados. Además, consultar datos requiere operadores y funciones específicas, como ->
, ->>
, @>
y ?
. Esto hace que la consulta sea más verbosa y menos intuitiva, por lo tanto, requiere más destreza.
Otro inconveniente es el gasto de almacenamiento. Los datos `JSONB` se almacenan en un formato binario, lo que puede resultar en un gasto de almacenamiento adicional en comparación con los datos relacionales normalizados. En casos donde los datos JSON son grandes o contienen mucha información redundante, el gasto de almacenamiento puede ser significativo.
A pesar de estos inconvenientes, el tipo `JSONB` se adapta a nuestro caso de uso, ya que la información de metadatos de campo es relativamente pequeña y no requiere consultas complejas. La flexibilidad de `JSONB` coincide con la naturaleza dinámica del campoMeta.
Postgres proporciona 2 campos para almacenar datos JSON —
json
yjsonb
. Para más información, puede consultar la documentación.
2do Desafío: Almacenar Configuraciones Avanzadas de Campos en el Frontend
El siguiente desafío fue encontrar la mejor manera de almacenar las configuraciones avanzadas de los campos ingresadas por los usuarios.
Actualmente, la aplicación solo guarda los campos y las configuraciones asociadas en la base de datos cuando el usuario pasa al siguiente paso.
Los campos se almacenan localmente hasta que el usuario avanza al siguiente paso. Esto significa que todos los campos y sus configuraciones se pierden cuando el usuario:
Cierra la pestaña de configuraciones avanzadas
Actualiza la página
Cierra la pestaña
Navega hacia el paso anterior
En el futuro, planeamos mejorar este flujo y guardar los campos en失, preservando los datos del usuario incluso si navegan lejos. Sin embargo, hasta entonces, necesitábamos una solución para guardar las configuraciones avanzadas cuando el usuario cierra la pestaña de configuraciones.
Solución: Almacenamiento Local
Nuestra solución temporal es almacenar las configuraciones avanzadas en almacenamiento local, ya que los campos solo están disponibles localmente. Si los campos se guardaran en la base de datos, podríamos almacenar las configuraciones avanzadas junto a ellos.
Dado que los campos no se guardan en la base de datos, debemos persistir los datos hasta que el usuario pase al siguiente paso, momento en el cual los datos se guardan en la base de datos. Almacenar los datos en el almacenamiento local permite a los usuarios abrir, cerrar y configurar varios campos en la pestaña de configuraciones avanzadas sin perder información.
Cuando el usuario pasa al siguiente paso, los campos y sus configuraciones avanzadas se guardan en la base de datos, y el almacenamiento local se borra.
También reconocimos los peligros de guardar datos en el almacenamiento local, ya que los usuarios podrían modificarlos y romper la aplicación. Como resultado, hemos implementado extensas verificaciones tanto en el backend como en el frontend, además de analizar y validar los datos con Zod.
Sin embargo, esta solución tiene limitaciones. Los datos todavía se pierden cuando el usuario:
Actualiza la página
Navega hacia el paso anterior
Cierra el navegador
En estos casos, los campos se borran del documento. Una mejora futura para guardar campos en la base de datos al perder el foco resolverá este problema.
3er Desafío: Campos de Radio y Casillas de Verificación
Implementar los campos Radio y Checkbox fue un desafío tanto desde lo lógico como desde el diseño. Ambos campos pueden contener valores vacíos y no vacíos, y el campo de casilla de verificación permite a los usuarios seleccionar múltiples valores vacíos/no vacíos.
La imagen anterior muestra los campos Radio y Checkbox en el editor de documentos. El campo de Radio en el lado izquierdo tiene 4 opciones, una de las cuales está seleccionada. El campo de Checkbox en el lado derecho tiene 4 opciones, 2 de las cuales están seleccionadas.
El campo de Radio fue más fácil de implementar porque los usuarios solo pueden seleccionar una opción, lo que resulta en una lógica más simple. El firmante hace clic en una opción para elegirla, y el campo se firma automáticamente con ese valor. Para cambiar la selección, el usuario hace clic en otra opción, desfirmando el campo y firmándolo de nuevo con el nuevo valor.
El campo de Checkbox fue más desafiante porque:
Los firmantes pueden seleccionar múltiples opciones simultáneamente, lo que resulta en que el campo contenga múltiples valores.
Puede tener reglas de validación (por ejemplo, seleccionar al menos, como máximo o exactamente X opciones).
Los usuarios pueden marcar/desmarcar opciones haciendo clic en ellas o borrar el campo con un botón.
Estos factores hacen que el campo de Checkbox sea más complejo y desafiante de implementar correctamente.
Solución
En lugar de enfocarnos en una solución específica, discutiremos la implementación general y sus aspectos más desafiantes. Incluiré un enlace a la implementación completa de cada campo para que pueda revisarlo.
Campo de Radio
La forma en que funciona la firma para el campo de Radio es extraer los datos de la base de datos y mostrar las opciones disponibles. Si el campo tiene un valor predeterminado establecido por el remitente del documento, se firma automáticamente con ese valor.
Puede ver la implementación completa del campo de radio en el radio-field.tsx archivo.
Si el campo no es de solo lectura y el usuario hace clic en otra opción, el campo se desfirma y se firma de nuevo con el nuevo valor. Los campos de solo lectura no se pueden modificar.
El valor se guarda en la base de datos cada vez que se firma el campo, ya sea mediante la firma automática o del usuario. De manera similar, el valor se elimina de la base de datos cuando el campo se desfirma.
Dado que el campo de Radio puede contener valores vacíos, mapeamos sobre los valores y reemplazamos los vacíos con una cadena única empty-value-${item.id}
. Esto se debe a que la cadena vacía no es un valor válido para el campo, y necesitamos diferenciar entre valores vacíos y no vacíos.
Campo de Casilla de Verificación
La implementación del campo de Casilla de Verificación es similar a la del campo de Radio, con las principales diferencias siendo:
Los campos de casilla de verificación pueden contener múltiples valores.
Los campos de casilla de verificación tienen reglas de validación que deben hacerse cumplir.
Luego, recuperamos la regla de validación y la longitud de la base de datos y encontramos la señal de validación correspondiente (por ejemplo, ">=", "=", "\<=") según la etiqueta de la regla. La matriz `checkboxValidationSigns` mapea las etiquetas de las reglas a sus señales correspondientes.
Luego, verificamos si se cumple la condición de longitud según la regla de validación, la señal y la longitud. Si se cumple, el usuario puede proceder a firmar el campo. De lo contrario, necesita seleccionar el número correcto de opciones.
En resumen, el campo de Casilla de Verificación permite a los firmantes seleccionar múltiples opciones, siendo el campo firmado automáticamente según estas selecciones. Los firmantes pueden desfirmar el campo al deseleccionar opciones o borrar todas las selecciones. El sistema aplica las reglas de validación a lo largo de este proceso, asegurando que los firmantes seleccionen el número requerido de opciones para poder firmar el campo con éxito.
Puede ver la implementación completa del campo de casilla de verificación en el checkbox-field.tsx archivo.
4to Desafío: Colores de los Destinatarios
Otro desafío que enfrentamos fue usar colores para diferenciar a los destinatarios. Necesitábamos generar dinámicamente y reutilizar las mismas clases de Tailwind en varios componentes. Sin embargo, TailwindCSS solo incluye las clases CSS utilizadas en el proyecto, descartando las que no se usan de la compilación final. Esto resultó en que los colores no se aplicaran a los componentes, ya que las clases no se usaron en el código.
Las imágenes a continuación ilustran los colores de los destinatarios en 2 estados diferentes.
En la primera imagen, el campo "Firma" en la derecha está en estado activo (azul), activado cuando el usuario hace clic en el campo para arrastrarlo al documento. El campo de firma a la izquierda, colocado en el documento, está en el estado normal.
La primera imagen ilustra el campo "Firma" en el estado activo, activado cuando el usuario hace clic en él.
La segunda imagen muestra el campo "Firma" en el estado normal.
El editor de documentos consta de varios componentes (campos, destinatarios, etc.), lo que significa que los mismos colores y código se reutilizan en múltiples componentes.
El código anterior muestra una solución ingenua usando un objeto combinedStyles
que contiene clases de TailwindCSS para varios estilos de componentes (anillo, borde, paso del cursor, etc.).
Los componentes usarían ganchos personalizados para aplicar estilos apropiados según el destinatario seleccionado. Por ejemplo, el destinatario 1 utilizaría estilos green-500
, volviendo todos los elementos relacionados verdes.
El problema con este enfoque es que no podemos importar el objeto combinedStyles
en otros componentes porque TailwindCSS eliminará las clases no utilizadas. Eso significa que tuvimos que copiar y pegar el mismo objeto en varios archivos. Como resultado, contamina la base de código con código duplicado, lo que hace que sea más difícil mantener y escalar el código. A medida que la aplicación crece, el objeto combinedStyles
se volverá más grande y más complejo. Además, no es muy flexible, ya que no permite una fácil personalización de los colores.
Si bien este enfoque funciona, hay una solución más eficiente y escalable.
Solución: Modularizar la Lógica y Usar Variables CSS
Para abordar el desafío de reutilizar colores en varios componentes, movimos los colores y los ganchos asociados a un archivo separado, definiendo estilos solo en este archivo y accediendo a ellos desde componentes a través de ganchos personalizados.
El archivo fue truncado para mejorar la legibilidad. Puede ver el código completo en el signer-colors.ts archivo del repositorio de Documenso.
El objeto SIGNER_COLOR_STYLES
contiene los estilos para cada color, como los colores de fondo, borde y paso del cursor. Según el índice del firmante, el gancho useSignerColors
obtiene los estilos para un color específico. La función getSignerColorStyles
es una función auxiliar que devuelve los estilos para un firmante particular.
Ahora, los componentes pueden acceder a los colores y estilos utilizando ganchos personalizados. Por ejemplo, para obtener los estilos de un firmante específico, el componente puede llamar al gancho useSignerColors
con el índice del firmante.
El gancho devolverá los estilos para ese firmante, que luego se pueden aplicar al componente. Por ejemplo, puede acceder al color de fondo del firmante usando signerStyles.default.background
.
Este enfoque facilita la gestión de los colores y estilos, ya que se definen en un solo archivo. Cambiar o agregar colores se puede hacer en un solo lugar, haciendo el código más modular y reutilizable.
También optamos por utilizar variables CSS para definir colores, lo que permite una mayor flexibilidad y consistencia en el estilo. Una sola variable CSS para cada color puede cubrir una amplia gama de estados sin depender de múltiples clases de TailwindCSS. Por ejemplo, puede establecer fácilmente la opacidad y ligereza de un color sin usar múltiples clases. Las variables CSS ayudan a alinear los colores con nuestras pautas de marca mientras simplifican el proceso de estilización en general.
El Fin
Estamos felices de ver que se han lanzado los nuevos campos avanzados porque ofrecen a nuestros usuarios más flexibilidad, variedad y opciones de personalización. La implementación de los nuevos campos vino con sus desafíos, pero los superamos y aprendimos de ellos. Estamos emocionados de continuar mejorando Documenso y brindar a nuestros usuarios la mejor experiencia de firma de documentos.