Refactor form dialogs and UI components for improved consistency and usability

- Updated DeleteBrandDialog and DeleteCatalogDefinitionDialog to use FormBody and FormActions for better structure.
- Replaced DialogFooter with FormActions in various dialogs to standardize button layouts.
- Introduced FormSectionTitle in multiple components for consistent section headings.
- Adjusted grid layouts to use responsive design with @min-[560px] for better adaptability.
- Created SoftwareResourceCreateDialog and SoftwareInstallationEditorDialog for better separation of concerns in software installation management.
- Enhanced FormActions to support cancelDisabled prop for better control over button states.
- Refined FormSectionTitle to allow for additional HTML elements like "legend" and "span".
- Added EntityTableColumnsDialog for managing table column visibility in a more user-friendly manner.
This commit is contained in:
David Arranz 2026-10-15 16:14:26 +02:00
parent ccb52fb534
commit a0f02ced6e
38 changed files with 559 additions and 337 deletions

View File

@ -88,9 +88,7 @@ React, TanStack Query, Tailwind y shadcn/ui (Base UI, estilo Vega). Los componen
Estado remoto en TanStack Query; claves centralizadas e invalidación explícita después de mutaciones.
No duplicar estado remoto ni usar efectos para derivar valores calculables durante render.
Mantener accesibilidad y estados de carga, error, vacío y acciones deshabilitadas.
Los títulos de grupos en todos los diálogos siguen el estilo de «Datos de la plantilla»: `text-xs font-medium uppercase tracking-wide text-muted-foreground`. Reutilizar `FormSectionTitle` de @repo/rdx-ui, también dentro de `FormSection`, conservando el nivel semántico del encabezado. La variante del contenedor no cambia este estilo. Los títulos principales del diálogo y los nombres editables de secciones conservan su tratamiento propio.
Todos los diálogos de selección de tipo `PickerList` utilizan `FormDialog layout="picker"`: altura estable adaptada a la ventana, independiente del número de resultados. La tabla ocupa el espacio disponible con `EntityTable layout="fill"`; búsqueda, filtros, paginación y acciones quedan estables, y los resultados tienen desplazamiento interno con espacio reservado para la barra. En ventanas pequeñas se permite desplazamiento adicional del cuerpo para mantener accesibles todos los controles.
En los diálogos de selección de tipo `PickerList`, la columna del checkbox o radio se sitúa siempre al principio, antes de las columnas de datos, con identificador `selection`, cabecera solo visible para lectores de pantalla y anchura ajustada al control, próxima a la columna del nombre, sin ordenación ni posibilidad de ocultarla. No se trata como columna `actions` ni se ancla a la derecha. Pulsar cualquier punto no interactivo de una fila disponible debe activar/desactivar su checkbox, o seleccionar su radio en modo único. Reutilizar `onRowClick` de la tabla común, respetar las filas deshabilitadas y conservar la navegación por teclado del control. Pulsar directamente el checkbox, radio u otro control debe ejecutar solo su interacción propia, sin duplicar el cambio por propagación.
Todos los diálogos nuevos o modificados siguen el [patrón de diseño de cuadros de diálogo](docs/patron-de-dialogos.md), fuente normativa para títulos uniformes, separación entre grupos, distribución responsive, alturas iguales por fila, secciones plegables, filas compactas, ordenación accesible y selección múltiple. Reutilizar los componentes de @repo/rdx-ui indicados en el patrón. En todos los `PickerList`, mantener altura estable, columna `selection` compacta al principio y selección al pulsar zonas no interactivas de la fila. Aplicar cada pauta según el tipo de contenido; conservar los campos y restricciones propios de cada entidad.
Separar componentes por responsabilidades reales, sin fragmentar JSX trivial.
Antes de crear o modificar pantallas, reconocer activamente patrones visuales y de interacción comunes entre ellas, así como componentes repetidos dentro de una misma pantalla. Aplicar soluciones coherentes a todos los casos equivalentes, conservando las necesidades propias de cada entidad.
Reutilizar y extender la biblioteca visual existente antes de duplicar estructura, estilos o comportamiento. Extraer componentes compartidos cuando haya repetición real: cabeceras, listados, formularios modales, grupos de campos, selectores con alta, estados y ayuda contextual.
@ -224,6 +222,8 @@ Estas reglas complementan las convenciones y documentos de arquitectura existent
#### Diálogos, modales y ventanas emergentes
El diseño visual y de interacción sigue [el patrón de diálogos](docs/patron-de-dialogos.md); las siguientes reglas definen la organización de sus componentes.
- Páginas y listados no deben implementar directamente el contenido de ventanas modales funcionales. Los diálogos de alta, edición, confirmación, selección o detalle son componentes independientes.
- Toda ventana modal, cuadro de diálogo o formulario modal con contenido y responsabilidad propios debe extraerse a un componente React independiente y vivir en su propio fichero `.tsx`.
- Esta regla se aplica especialmente a componentes construidos con primitivas de shadcn/ui como `Dialog`, `AlertDialog`, `Sheet`, `Drawer` o componentes propios como `FormDialog`, pero no depende del nombre concreto utilizado.

View File

@ -9,6 +9,10 @@ consumen mediante `@repo/rdx-ui`. Los componentes que conocen contratos, textos,
reglas de Metis viven en el módulo o aplicación que los utiliza; nunca en ninguno de los dos
paquetes genéricos.
El [patrón de diseño de cuadros de diálogo](patron-de-dialogos.md) es la fuente normativa
para componer grupos, columnas responsive, filas compactas y selectores. Esta guía describe
los componentes que lo implementan y sus contratos.
## Componentes actuales
- Button: volver a comprobar la conexión.
@ -495,7 +499,7 @@ URLs de vista previa se liberan al cambiar de archivo o desmontar el componente.
El alta y la edición de plantillas de activos y recursos agrupan los datos generales con `FormSection` e incorporan campos desde las cabeceras de sus grupos. `TemplateSectionsEditor` permite secciones apiladas o una cuadrícula responsive mediante `sectionLayout="stack" | "responsive"`, con una cabecera numerada sobre fondo diferenciado que contiene el input del nombre, el asa de arrastre y las acciones de orden y eliminación. El cuerpo conserva la descripción y un área de campos separada por un borde. `TemplateSectionEditor` encapsula cada sección con `Collapsible`: todas empiezan abiertas y un botón de cabecera permite plegarlas con ratón o teclado. El nombre, el asa y las acciones siguen visibles al plegar. El contenido permanece montado para conservar los controles; si la validación nativa detecta un control inválido oculto, la sección se abre antes de que el navegador lo enfoque. La apertura se conserva por identidad al reordenar y no se guarda como parte de la plantilla. Los editores de activos y recursos usan la cuadrícula responsive: una columna hasta 1.000 px de ancho disponible y dos a partir de ese umbral, con separación de 16 px y tarjetas de igual altura en cada fila, determinada por la sección más alta. El orden recorre cada fila de izquierda a derecha y se conserva en el DOM, la numeración, el teclado y el arrastre; la altura de cada fila se recalcula al plegar o desplegar sus secciones, manteniendo ambas tarjetas con la misma altura. El bloque «Sin sección» permanece debajo, a ancho completo.
`TemplateSectionFields` muestra un título traducido y el número de campos. Cada campo tiene fondo propio y separa su nombre y acciones de la configuración de obligatoriedad y asignación, cuyo selector conserva una etiqueta visible. Los campos se distribuyen en dos columnas a partir de 700 px de su propio grupo y en una para anchos menores; cada grupo establece su contenedor para evitar columnas demasiado estrechas cuando las secciones se muestran en paralelo. El bloque «Sin sección» se diferencia con borde discontinuo; las secciones vacías conservan un mensaje explícito. Se reutilizan los colores semánticos del tema para mantener el contraste en modo claro y oscuro.
`TemplateSectionFields` muestra un título traducido y el número de campos. Cada campo tiene fondo propio. En la presentación `cards`, separa su nombre y acciones de la configuración de obligatoriedad y asignación, cuyo selector conserva una etiqueta visible; los editores de plantillas de activos y recursos utilizan la presentación `compact` descrita más abajo. Los campos se distribuyen en dos columnas a partir de 700 px de su propio grupo y en una para anchos menores; cada grupo establece su contenedor para evitar columnas demasiado estrechas cuando las secciones se muestran en paralelo. El bloque «Sin sección» se diferencia con borde discontinuo; las secciones vacías conservan un mensaje explícito. Se reutilizan los colores semánticos del tema para mantener el contraste en modo claro y oscuro.
### Orden mediante arrastre y teclado
@ -537,10 +541,28 @@ El alta y la edición de plantillas de activos y recursos reutilizan `TemplateMe
## Títulos de grupos en diálogos
Todos los diálogos con grupos utilizan el estilo de «Datos de la plantilla»: texto pequeño, peso medio, mayúsculas, espaciado entre letras y color atenuado (`text-xs font-medium uppercase tracking-wide text-muted-foreground`). `FormSectionTitle` centraliza esta presentación y recibe `children`, `id` opcional y `as="h3" | "h4" | "h5"` para conservar la jerarquía semántica. El consumidor aporta el texto traducido; las mayúsculas se aplican mediante CSS.
Todos los diálogos con grupos utilizan el estilo de «Datos de la plantilla»: texto pequeño, seminegrita, mayúsculas, espaciado entre letras y color atenuado (`text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-4`). `FormSectionTitle` centraliza esta presentación y recibe `children`, `id` opcional y `as="h3" | "h4" | "h5" | "legend" | "span"` para conservar la semántica del encabezado, leyenda o título dentro de un `summary`. El consumidor aporta el texto traducido; las mayúsculas se aplican mediante CSS. La variante `span` conserva la tipografía y deja el espaciado al `summary` que la contiene, sin margen inferior propio.
`FormSection`, `TemplateSectionsEditor` y `TemplateSectionFields` comparten este título, con independencia de sus bordes, acciones o contenido. La regla también se aplica a grupos de consulta, como los puestos del historial de perfiles. Los títulos principales del diálogo y los nombres editables de secciones mantienen su presentación propia.
### Campos compactos en plantillas de activos y recursos
`TemplateSectionsEditor fieldLayout="compact"` presenta cada campo en una fila: asa de arrastre, switch de obligatoriedad, nombre flexible, flechas arriba/abajo y papelera. El nombre puede ocupar varias líneas en pantallas estrechas. El switch conserva una etiqueta accesible con «Obligatorio» y el nombre del campo; su texto visual y el selector de sección se omiten. Las notas de campos inactivos siguen disponibles para lectores de pantalla. Se mantienen el arrastre, las flechas por teclado, los límites de orden y los estados deshabilitados. Ambos editores utilizan esta presentación compacta.
## Ajustes comunes tras la revisión de diálogos
`FormBody` establece su propio contenedor de consultas de tamaño: las cuadrículas de formularios
y subcontroles utilizan `@min-[560px]` para decidir sus dos columnas según el ancho real del
modal. `FieldSections` aplica la misma regla a los campos y estira los grupos de cada fila
a la altura de su vecino más alto. Los títulos de consulta, grupos de permisos y secciones
desplegables reutilizan `FormSectionTitle`, conservando el estilo vigente del componente.
`FormActions` admite `cancelDisabled` para conservar el bloqueo de cancelación durante una
confirmación pendiente, además de `leadingActions` para acciones opcionales a la izquierda.
Las confirmaciones de marcas y definiciones de catálogo usan `FormBody` y `FormActions` para
mantener su pie fuera del área desplazable. La selección de compañía del dashboard también
sitúa Continuar en ese pie y lo deshabilita mientras falta una selección o una consulta correcta.
`SoftwareInstallationEditorDialog`, `SoftwareResourceCreateDialog` y `EntityTableColumnsDialog`
encapsulan sus interacciones en ficheros independientes. Sus consumidores conservan apertura,
selección, datos y callbacks; los formularios y controles mantienen sus contratos anteriores.

View File

@ -1,177 +1,171 @@
# Inventario de interacciones modales
Revisión de 2026-10-08. Se inspeccionaron las primitivas y wrappers importados, sus consumidores,
los bloques condicionales y las implementaciones manuales posibles: Dialog, AlertDialog, Sheet,
Drawer, FormDialog, portales, elementos dialog, aria-modal y roles dialog/alertdialog.
La búsqueda no dependió del nombre de los componentes.
Revisión de código de 2026-10-15 según el [patrón de diseño de diálogos](patron-de-dialogos.md).
Se buscaron constructores `FormDialog`, `ConfirmDeleteDialog` y `FieldPickerDialog`, además de
primitivas `Dialog`, `AlertDialog`, `Sheet`, `Drawer`, portales y diálogos manuales. Se revisaron
los formularios, controles compartidos y contenido de consulta que reciben estos constructores.
El código propio contiene **81 componentes que construyen una interacción modal**. Todos
usan FormDialog de @repo/rdx-ui y tienen un componente principal por fichero. Un componente
puede representar variantes de alta, edición o copia, y catálogos con distintos tipos; el número
no es un recuento de todos los estados posibles de las ventanas. No se encontraron otras
implementaciones propias con AlertDialog, Sheet, Drawer ni diálogos HTML/manuales.
El código propio contiene **81 componentes modales en módulos** y **3 constructores
compartidos en @repo/rdx-ui**. El recuento identifica componentes, no cada variante de alta,
edición, copia o estado. Las dos variantes de CatalogFieldPickerDialog son adaptadores del
mismo selector genérico. El código vendor de `packages/shadcn-ui` queda fuera de esta revisión
y no se modifica.
Actualización de 2026-10-09: se añade DeleteBrandDialog para confirmar la eliminación de
marcas personalizadas; el inventario pasa a 82 componentes. Se ha comprobado esta operación
en navegador con API simulada: Cancelar/Escape, errores, bloqueo durante el envío, éxito,
español/inglés y anchura móvil. Esta comprobación no amplía la revisión al resto de modales.
## Resultado de la revisión
La implementación vendor de packages/shadcn-ui queda fuera de la refactorización según AGENTS.md.
Sus primitivas y el diálogo de configuración de tablas se consultan y consumen, no se duplican
ni reorganizan. Los popovers de selectores y menús de acciones no se contabilizan como formularios
o ventanas modales funcionales.
- Los diálogos utilizan la estructura común de cabecera, cuerpo desplazable y pie. Las
confirmaciones de marcas y catálogo se han adaptado al mismo pie fijo, conservando el
bloqueo durante el envío y la presentación de errores.
- Los formularios y sus subcontroles calculan las columnas según su contenedor. Los títulos
de grupos de consulta, permisos y secciones comparten el estilo de Datos de la plantilla.
- Las secciones de plantillas conservan dos columnas cuando hay espacio, igual altura por
fila, plegado, campos compactos, ordenación accesible e incorporación de campos por grupo.
- AssetPickerDialog y los dos CatalogFieldPickerDialog mantienen altura estable, selección
compacta al principio, selección desde la fila y confirmación provisional. Los diálogos
de configuración de columnas son ajustes, no selectores de entidades PickerList.
- Se extrajeron los diálogos de edición de instalaciones de software, alta de su recurso y
configuración de columnas. Los padres mantienen apertura y datos, sin definir el modal.
- Las variantes de selección de compañía del dashboard colocan Continuar en el pie común.
Los formularios sencillos, historiales y ayudas mantienen su tamaño según contenido; las
fechas de revisión y nombres de registros conservan su tipografía de datos, no de grupos.
- Se conservan los pasos de alta de recursos ya existentes: no se ha añadido ningún asistente
para resolver únicamente espacio. Tampoco se añaden cuadrículas a formularios sencillos.
## Responsabilidades y consumidores
## Componentes revisados
Las páginas y listados conservan apertura, selección y callbacks. Los componentes enumerados
encapsulan título, disposición, anchura, contenido y cierre. Los formularios existentes mantienen
sus schemas, borradores, mutaciones, ayuda y errores. EndRelationshipDialog posee además la
mutación de confirmación y sus estados pendientes y de error.
Las rutas siguen la [estructura por entidades y operaciones](estructura-web-por-entidades-y-operaciones.md).
La columna Composición indica el constructor utilizado; las variantes heredan sus reglas comunes.
CompanyEditorDialog, PersonEditorDialog, DepartmentEditorDialog y SiteEditorDialog se reutilizan
el patrón entre catálogo y selector. Los callbacks de selección permanecen en el consumidor que
posee el borrador. Las altas anidadas conservan sus restricciones existentes.
### assets (23)
Se integró el contenido exclusivo de los antiguos SupportHistory, ApplicationHistory,
CertificateHistory y PrinterHistory en sus respectivos componentes de diálogo, tras comprobar
sus referencias. No se eliminaron historiales ni acciones. Los archivos anteriores se pueden
recuperar desde Git.
| Componente | Composición |
| --- | --- |
| [EndRelationshipDialog](../modules/assets/src/web/asset-relationships/end/ui/end-relationship-dialog.tsx) | `FormDialog` |
| [RelationshipEditorDialog](../modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx) | `FormDialog` |
| [WorkstationAssetCreateDialog](../modules/assets/src/web/assets/create/ui/workstation-asset-create-dialog.tsx) | `FormDialog` |
| [AssetDetailDialog](../modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx) | `FormDialog` |
| [AssetPickerDialog](../modules/assets/src/web/assets/select/ui/asset-picker-dialog.tsx) | `FormDialog` · altura estable |
| [AssetEditorDialog](../modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx) | `FormDialog` |
| [DeleteAssetDialog](../modules/assets/src/web/assets/shared/ui/delete-asset-dialog.tsx) | `ConfirmDeleteDialog` |
| [DeleteBrandDialog](../modules/assets/src/web/brands/delete/ui/delete-brand-dialog.tsx) | `FormDialog` |
| [BrandEditorDialog](../modules/assets/src/web/brands/shared/ui/brand-editor-dialog.tsx) | `FormDialog` |
| [DeleteCatalogDefinitionDialog](../modules/assets/src/web/catalog/delete/ui/delete-catalog-definition-dialog.tsx) | `FormDialog` |
| [CatalogDefinitionDialog](../modules/assets/src/web/catalog/detail/ui/catalog-definition-dialog.tsx) | `FormDialog` |
| [CatalogEditorDialog](../modules/assets/src/web/catalog/shared/ui/catalog-editor-dialog.tsx) | `FormDialog` |
| [CatalogFieldCreateDialog](../modules/assets/src/web/catalog-fields/create/ui/catalog-field-create-dialog.tsx) | `FormDialog` |
| [CatalogFieldPickerDialog](../modules/assets/src/web/catalog-fields/select/ui/catalog-field-picker-dialog.tsx) | `FieldPickerDialog` · altura estable |
| [CatalogRelationCreateDialog](../modules/assets/src/web/catalog-relations/create/ui/catalog-relation-create-dialog.tsx) | `FormDialog` |
| [CatalogTemplateCreateDialog](../modules/assets/src/web/catalog-templates/create/ui/catalog-template-create-dialog.tsx) | `FormDialog` |
| [DeleteRecordDialog](../modules/assets/src/web/shared/ui/delete-record-dialog.tsx) | `ConfirmDeleteDialog` |
| [SupportHistoryDialog](../modules/assets/src/web/support-connections/history/ui/support-history-dialog.tsx) | `FormDialog` |
| [SupportConnectionEditorDialog](../modules/assets/src/web/support-connections/shared/ui/support-connection-editor-dialog.tsx) | `FormDialog` |
| [SupportResourceCreateDialog](../modules/assets/src/web/support-connections/shared/ui/support-resource-create-dialog.tsx) | `FormDialog` |
| [WorkstationHistoryDialog](../modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx) | `FormDialog` |
| [WorkstationEditorDialog](../modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx) | `FormDialog` |
| [WorkstationLifecycleDialog](../modules/assets/src/web/workstations/shared/ui/workstation-lifecycle-dialog.tsx) | `ConfirmDeleteDialog` |
## Componentes por área
### companies (49)
Las rutas reflejan la [estructura por entidades y operaciones](estructura-web-por-entidades-y-operaciones.md).
| Componente | Composición |
| --- | --- |
| [AccessQuickEditor](../modules/companies/src/web/access/create/ui/access-quick-editor.tsx) | `FormDialog` |
| [QuickCreateDialog](../modules/companies/src/web/access/create/ui/quick-create-dialog.tsx) | `FormDialog` |
| [AccessHelpDialog](../modules/companies/src/web/access/help/ui/access-help-dialog.tsx) | `FormDialog` |
| [AccessHistory](../modules/companies/src/web/access/history/ui/access-history.tsx) | `FormDialog` |
| [AccessEditorDialog](../modules/companies/src/web/access/shared/ui/access-editor-dialog.tsx) | `FormDialog` |
| [ApplicationQuickEditor](../modules/companies/src/web/application-configurations/create/ui/application-quick-editor.tsx) | `FormDialog` |
| [ApplicationHistoryDialog](../modules/companies/src/web/application-configurations/history/ui/application-history-dialog.tsx) | `FormDialog` |
| [ApplicationConfigurationEditorDialog](../modules/companies/src/web/application-configurations/shared/ui/application-configuration-editor-dialog.tsx) | `FormDialog` |
| [AssignmentHistoryDialog](../modules/companies/src/web/assignments/history/ui/assignment-history-dialog.tsx) | `FormDialog` |
| [AssignmentEditorDialog](../modules/companies/src/web/assignments/shared/ui/assignment-editor-dialog.tsx) | `FormDialog` |
| [CertificateQuickEditor](../modules/companies/src/web/certificate-configurations/create/ui/certificate-quick-editor.tsx) | `FormDialog` |
| [CertificateHistoryDialog](../modules/companies/src/web/certificate-configurations/history/ui/certificate-history-dialog.tsx) | `FormDialog` |
| [CertificateConfigurationEditorDialog](../modules/companies/src/web/certificate-configurations/shared/ui/certificate-configuration-editor-dialog.tsx) | `FormDialog` |
| [InventoryQuickCreateDialog](../modules/companies/src/web/companies/detail/ui/inventory-quick-create-dialog.tsx) | `FormDialog` |
| [CompanyEditorDialog](../modules/companies/src/web/companies/shared/ui/company-editor-dialog.tsx) | `FormDialog` |
| [DashboardQuickCreate](../modules/companies/src/web/dashboard/create/ui/dashboard-quick-create.tsx) | `FormDialog` |
| [DashboardDetailsDialog](../modules/companies/src/web/dashboard/detail/ui/dashboard-details-dialog.tsx) | `FormDialog` |
| [DepartmentEditorDialog](../modules/companies/src/web/departments/shared/ui/department-editor-dialog.tsx) | `FormDialog` |
| [FolderQuickEditor](../modules/companies/src/web/folder-mappings/create/ui/folder-quick-editor.tsx) | `FormDialog` |
| [FolderMappingHistoryDialog](../modules/companies/src/web/folder-mappings/history/ui/folder-mapping-history-dialog.tsx) | `FormDialog` |
| [FolderMappingEditorDialog](../modules/companies/src/web/folder-mappings/shared/ui/folder-mapping-editor-dialog.tsx) | `FormDialog` |
| [MailQuickEditor](../modules/companies/src/web/mail-configurations/create/ui/mail-quick-editor.tsx) | `FormDialog` |
| [MailConfigurationHistoryDialog](../modules/companies/src/web/mail-configurations/history/ui/mail-configuration-history-dialog.tsx) | `FormDialog` |
| [MailConfigurationEditorDialog](../modules/companies/src/web/mail-configurations/shared/ui/mail-configuration-editor-dialog.tsx) | `FormDialog` |
| [MailboxUseEditorDialog](../modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-editor-dialog.tsx) | `FormDialog` |
| [MailboxCreateDialog](../modules/companies/src/web/mailboxes/create/ui/mailbox-create-dialog.tsx) | `FormDialog` |
| [PersonEditorDialog](../modules/companies/src/web/people/shared/ui/person-editor-dialog.tsx) | `FormDialog` |
| [PrinterProfileCreateDialog](../modules/companies/src/web/printer-configurations/create/ui/printer-profile-create-dialog.tsx) | `FormDialog` |
| [PrinterHistoryDialog](../modules/companies/src/web/printer-configurations/history/ui/printer-history-dialog.tsx) | `FormDialog` |
| [PrinterConfigurationEditorDialog](../modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-editor-dialog.tsx) | `FormDialog` |
| [ProcessTemplateEditorDialog](../modules/companies/src/web/process-templates/shared/ui/process-template-editor-dialog.tsx) | `FormDialog` |
| [ProcessQuickCreateDialog](../modules/companies/src/web/processes/create/ui/process-quick-create-dialog.tsx) | `FormDialog` |
| [ProcessStartDialog](../modules/companies/src/web/processes/start/ui/process-start-dialog.tsx) | `FormDialog` |
| [ProcessEditDialog](../modules/companies/src/web/processes/update/ui/process-edit-dialog.tsx) | `FormDialog` |
| [RemoteQuickEditor](../modules/companies/src/web/remote-connections/create/ui/remote-quick-editor.tsx) | `FormDialog` |
| [RemoteConnectionHistoryDialog](../modules/companies/src/web/remote-connections/history/ui/remote-connection-history-dialog.tsx) | `FormDialog` |
| [RemoteConnectionEditorDialog](../modules/companies/src/web/remote-connections/shared/ui/remote-connection-editor-dialog.tsx) | `FormDialog` |
| [ConfigurationQuickEditor](../modules/companies/src/web/shared/ui/configuration-quick-editor.tsx) | `FormDialog` |
| [DeleteRecordDialog](../modules/companies/src/web/shared/ui/delete-record-dialog.tsx) | `ConfirmDeleteDialog` |
| [SiteEditorDialog](../modules/companies/src/web/sites/shared/ui/site-editor-dialog.tsx) | `FormDialog` |
| [SoftwareResourceCreateDialog](../modules/companies/src/web/software-installations/create/ui/software-resource-create-dialog.tsx) | `FormDialog` |
| [SoftwareHistoryDialog](../modules/companies/src/web/software-installations/history/ui/software-history-dialog.tsx) | `FormDialog` |
| [SoftwareInstallationEditorDialog](../modules/companies/src/web/software-installations/shared/ui/software-installation-editor-dialog.tsx) | `FormDialog` |
| [TeleworkAuthorizationHistoryDialog](../modules/companies/src/web/telework-authorizations/history/ui/telework-authorization-history-dialog.tsx) | `FormDialog` |
| [TeleworkAuthorizationEditorDialog](../modules/companies/src/web/telework-authorizations/shared/ui/telework-authorization-editor-dialog.tsx) | `FormDialog` |
| [WorkProfileHistoryDialog](../modules/companies/src/web/work-profiles/history/ui/work-profile-history-dialog.tsx) | `FormDialog` |
| [WorkProfileEditorDialog](../modules/companies/src/web/work-profiles/shared/ui/work-profile-editor-dialog.tsx) | `FormDialog` |
| [WorkProfileLifecycleDialog](../modules/companies/src/web/work-profiles/shared/ui/work-profile-lifecycle-dialog.tsx) | `ConfirmDeleteDialog` |
| [WorkspaceSettingsDialog](../modules/companies/src/web/workspace/settings/ui/workspace-settings-dialog.tsx) | `FormDialog` |
### modules/assets/src/web
### identity (2)
- [AssetDetailDialog](../modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx)
- [AssetEditorDialog](../modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx)
- [BrandEditorDialog](../modules/assets/src/web/brands/shared/ui/brand-editor-dialog.tsx)
- [DeleteBrandDialog](../modules/assets/src/web/brands/delete/ui/delete-brand-dialog.tsx)
- [CatalogDefinitionDialog](../modules/assets/src/web/catalog/detail/ui/catalog-definition-dialog.tsx)
- [CatalogEditorDialog](../modules/assets/src/web/catalog/shared/ui/catalog-editor-dialog.tsx)
- [CatalogFieldCreateDialog](../modules/assets/src/web/catalog-fields/create/ui/catalog-field-create-dialog.tsx)
- [CatalogFieldPickerDialog](../modules/assets/src/web/catalog-fields/select/ui/catalog-field-picker-dialog.tsx): selección múltiple provisional para añadir campos a una plantilla; reutiliza la tabla común y confirma el lote antes de cambiar el borrador.
- [CatalogRelationCreateDialog](../modules/assets/src/web/catalog-relations/create/ui/catalog-relation-create-dialog.tsx)
- [CatalogTemplateCreateDialog](../modules/assets/src/web/catalog-templates/create/ui/catalog-template-create-dialog.tsx)
- [EndRelationshipDialog](../modules/assets/src/web/asset-relationships/end/ui/end-relationship-dialog.tsx)
- [RelationshipEditorDialog](../modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx)
- [SupportConnectionEditorDialog](../modules/assets/src/web/support-connections/shared/ui/support-connection-editor-dialog.tsx)
- [SupportHistoryDialog](../modules/assets/src/web/support-connections/history/ui/support-history-dialog.tsx)
- [WorkstationAssetCreateDialog](../modules/assets/src/web/assets/create/ui/workstation-asset-create-dialog.tsx)
- [WorkstationEditorDialog](../modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx)
- [WorkstationHistoryDialog](../modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx)
| Componente | Composición |
| --- | --- |
| [AccountPasswordDialog](../modules/identity/src/web/passwords/change/ui/account-password-dialog.tsx) | `FormDialog` |
| [UserEditorDialog](../modules/identity/src/web/users/shared/ui/user-editor-dialog.tsx) | `FormDialog` |
### modules/companies/src/web/access
### resources (7)
- [AccessEditorDialog](../modules/companies/src/web/access/shared/ui/access-editor-dialog.tsx)
- [AccessHelpDialog](../modules/companies/src/web/access/help/ui/access-help-dialog.tsx)
- [AccessHistory](../modules/companies/src/web/access/history/ui/access-history.tsx)
- [AccessQuickEditor](../modules/companies/src/web/access/create/ui/access-quick-editor.tsx)
- [QuickCreateDialog](../modules/companies/src/web/access/create/ui/quick-create-dialog.tsx)
| Componente | Composición |
| --- | --- |
| [CatalogDefinitionDialog](../modules/resources/src/web/catalog/detail/ui/catalog-definition-dialog.tsx) | `FormDialog` |
| [CatalogEditorDialog](../modules/resources/src/web/catalog/shared/ui/catalog-editor-dialog.tsx) | `FormDialog` |
| [CatalogFieldCreateDialog](../modules/resources/src/web/catalog-fields/create/ui/catalog-field-create-dialog.tsx) | `FormDialog` |
| [CatalogFieldPickerDialog](../modules/resources/src/web/catalog-fields/select/ui/catalog-field-picker-dialog.tsx) | `FieldPickerDialog` · altura estable |
| [CatalogTemplateCreateDialog](../modules/resources/src/web/catalog-templates/create/ui/catalog-template-create-dialog.tsx) | `FormDialog` |
| [ResourceDetailDialog](../modules/resources/src/web/resources/detail/ui/resource-detail-dialog.tsx) | `FormDialog` |
| [ResourceEditorDialog](../modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx) | `FormDialog` |
### modules/companies/src/web/application-configurations
### rdx-ui (3)
- [ApplicationConfigurationEditorDialog](../modules/companies/src/web/application-configurations/shared/ui/application-configuration-editor-dialog.tsx)
- [ApplicationHistoryDialog](../modules/companies/src/web/application-configurations/history/ui/application-history-dialog.tsx)
- [ApplicationQuickEditor](../modules/companies/src/web/application-configurations/create/ui/application-quick-editor.tsx)
| Componente | Composición |
| --- | --- |
| [ConfirmDeleteDialog](../packages/rdx-ui/src/components/confirm-delete-dialog.tsx) | `FormDialog` |
| [EntityTableColumnsDialog](../packages/rdx-ui/src/components/entity-table-columns-dialog.tsx) | `FormDialog` |
| [FieldPickerDialog](../packages/rdx-ui/src/components/field-picker-dialog.tsx) | `FormDialog` · altura estable |
### modules/companies/src/web/certificate-configurations
## Validación y límites
- [CertificateConfigurationEditorDialog](../modules/companies/src/web/certificate-configurations/shared/ui/certificate-configuration-editor-dialog.tsx)
- [CertificateHistoryDialog](../modules/companies/src/web/certificate-configurations/history/ui/certificate-history-dialog.tsx)
- [CertificateQuickEditor](../modules/companies/src/web/certificate-configurations/create/ui/certificate-quick-editor.tsx)
La revisión cubre código y render estático mediante las pruebas existentes. Las comprobaciones
interactivas de apertura, Escape, restauración de foco, arrastre, envío, errores y altura visual
en español/inglés y anchos estrecho/amplio quedan pendientes de una sesión de navegador.
No se dispone de una herramienta de navegador en esta sesión; los análisis de código, tipos,
formato y pruebas no sustituyen esa revisión visual.
### Compañías, organización, dashboard y workspace de companies
Resultados de los comandos ejecutados:
- [AssignmentEditorDialog](../modules/companies/src/web/assignments/shared/ui/assignment-editor-dialog.tsx)
- [AssignmentHistoryDialog](../modules/companies/src/web/assignments/history/ui/assignment-history-dialog.tsx)
- [CompanyEditorDialog](../modules/companies/src/web/companies/shared/ui/company-editor-dialog.tsx)
- [ConfigurationQuickEditor](../modules/companies/src/web/shared/ui/configuration-quick-editor.tsx)
- [DashboardDetailsDialog](../modules/companies/src/web/dashboard/detail/ui/dashboard-details-dialog.tsx)
- [DashboardQuickCreate](../modules/companies/src/web/dashboard/create/ui/dashboard-quick-create.tsx)
- [DepartmentEditorDialog](../modules/companies/src/web/departments/shared/ui/department-editor-dialog.tsx)
- [InventoryQuickCreateDialog](../modules/companies/src/web/companies/detail/ui/inventory-quick-create-dialog.tsx)
- [PersonEditorDialog](../modules/companies/src/web/people/shared/ui/person-editor-dialog.tsx)
- [SiteEditorDialog](../modules/companies/src/web/sites/shared/ui/site-editor-dialog.tsx)
- [WorkspaceSettingsDialog](../modules/companies/src/web/workspace/settings/ui/workspace-settings-dialog.tsx)
| Comprobación | Resultado |
| --- | --- |
| Typecheck dirigido a rdx-ui, assets, resources, companies e identity | Correcto |
| Biome y ESLint dirigidos a los archivos de código modificados | Correctos |
| `pnpm build` | Correcto; conserva el aviso de chunks grandes |
| `pnpm test` | UI, activos, recursos, identidad, core y DDD correctos; empresas mantiene el fallo previo de `companies-page.test.tsx` |
| `pnpm typecheck` | Mantiene TS2375 previo en `packages/shadcn-ui/src/components/calendar.tsx:165` |
| `pnpm lint` / `pnpm check` | Mantienen respectivamente 14 y 155 errores previos ajenos al cambio |
| `git diff --check` y enlaces locales de la documentación | Correctos |
### modules/companies/src/web/folder-mappings
- [FolderMappingEditorDialog](../modules/companies/src/web/folder-mappings/shared/ui/folder-mapping-editor-dialog.tsx)
- [FolderMappingHistoryDialog](../modules/companies/src/web/folder-mappings/history/ui/folder-mapping-history-dialog.tsx)
- [FolderQuickEditor](../modules/companies/src/web/folder-mappings/create/ui/folder-quick-editor.tsx)
### Buzones, usos y configuraciones de correo de companies
- [MailboxCreateDialog](../modules/companies/src/web/mailboxes/create/ui/mailbox-create-dialog.tsx)
- [MailboxUseEditorDialog](../modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-editor-dialog.tsx)
- [MailConfigurationEditorDialog](../modules/companies/src/web/mail-configurations/shared/ui/mail-configuration-editor-dialog.tsx)
- [MailConfigurationHistoryDialog](../modules/companies/src/web/mail-configurations/history/ui/mail-configuration-history-dialog.tsx)
- [MailQuickEditor](../modules/companies/src/web/mail-configurations/create/ui/mail-quick-editor.tsx)
### modules/companies/src/web/printer-configurations
- [PrinterConfigurationEditorDialog](../modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-editor-dialog.tsx)
- [PrinterHistoryDialog](../modules/companies/src/web/printer-configurations/history/ui/printer-history-dialog.tsx)
- [PrinterProfileCreateDialog](../modules/companies/src/web/printer-configurations/create/ui/printer-profile-create-dialog.tsx)
### modules/companies/src/web/processes
- [ProcessEditDialog](../modules/companies/src/web/processes/update/ui/process-edit-dialog.tsx)
- [ProcessQuickCreateDialog](../modules/companies/src/web/processes/create/ui/process-quick-create-dialog.tsx)
- [ProcessStartDialog](../modules/companies/src/web/processes/start/ui/process-start-dialog.tsx)
### modules/companies/src/web/process-templates
- [ProcessTemplateEditorDialog](../modules/companies/src/web/process-templates/shared/ui/process-template-editor-dialog.tsx)
### modules/companies/src/web/work-profiles
- [WorkProfileEditorDialog](../modules/companies/src/web/work-profiles/shared/ui/work-profile-editor-dialog.tsx)
- [WorkProfileHistoryDialog](../modules/companies/src/web/work-profiles/history/ui/work-profile-history-dialog.tsx)
### modules/companies/src/web/remote-connections
- [RemoteConnectionEditorDialog](../modules/companies/src/web/remote-connections/shared/ui/remote-connection-editor-dialog.tsx)
- [RemoteConnectionHistoryDialog](../modules/companies/src/web/remote-connections/history/ui/remote-connection-history-dialog.tsx)
- [RemoteQuickEditor](../modules/companies/src/web/remote-connections/create/ui/remote-quick-editor.tsx)
### modules/companies/src/web/telework-authorizations
- [TeleworkAuthorizationEditorDialog](../modules/companies/src/web/telework-authorizations/shared/ui/telework-authorization-editor-dialog.tsx)
- [TeleworkAuthorizationHistoryDialog](../modules/companies/src/web/telework-authorizations/history/ui/telework-authorization-history-dialog.tsx)
### modules/identity/src/web
- [AccountPasswordDialog](../modules/identity/src/web/passwords/change/ui/account-password-dialog.tsx)
- [UserEditorDialog](../modules/identity/src/web/users/shared/ui/user-editor-dialog.tsx)
- [UserManagementDialog](../modules/identity/src/web/users/list/ui/user-management-dialog.tsx)
### modules/resources/src/web
- [CatalogDefinitionDialog](../modules/resources/src/web/catalog/detail/ui/catalog-definition-dialog.tsx)
- [CatalogEditorDialog](../modules/resources/src/web/catalog/shared/ui/catalog-editor-dialog.tsx)
- [CatalogFieldCreateDialog](../modules/resources/src/web/catalog-fields/create/ui/catalog-field-create-dialog.tsx)
- [CatalogFieldPickerDialog](../modules/resources/src/web/catalog-fields/select/ui/catalog-field-picker-dialog.tsx): adaptador del selector compartido para incorporar campos al destino y ofrecer alta rápida configurable.
- [CatalogTemplateCreateDialog](../modules/resources/src/web/catalog-templates/create/ui/catalog-template-create-dialog.tsx)
- [ResourceDetailDialog](../modules/resources/src/web/resources/detail/ui/resource-detail-dialog.tsx)
- [ResourceEditorDialog](../modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx)
## Comprobaciones y límites
Typecheck, lint, pruebas de los módulos y validaciones globales correctos. Las pruebas existentes
comprueban traducciones es/en, listados sin formularios visibles, ayuda, restricciones de altas
anidadas, estados y contratos. Se amplió la prueba del dashboard con datos no vacíos de los
paneles extraídos.
No se ejecutó una sesión interactiva de navegador. Queda por comprobar manualmente apertura,
Escape, restauración del foco, cancelación, envío y error en modales simples y anidados, en español,
inglés y anchura móvil. El render estático y el lint no sustituyen esa comprobación.
Véase [revisión de buenas prácticas](revision-de-buenas-practicas.md) para el alcance y los resultados
globales, y [componentes de interfaz](componentes-de-interfaz.md) para los patrones compartidos.
Los modales de tickets, intervenciones y sus plantillas están [archivados](funcionalidades-archivadas.md).
El fallo de empresas se reprodujo ejecutando directamente el fichero: una prueba de persona
espera el texto «Esta persona no tiene acceso a Metis Desk», ausente en el render actual.
No se ha modificado esa prueba ni el contenido de la ayuda para ocultar el fallo.
Los módulos de tickets, intervenciones y sus plantillas permanecen [archivados](funcionalidades-archivadas.md).

118
docs/patron-de-dialogos.md Normal file
View File

@ -0,0 +1,118 @@
# Patrón de diseño de cuadros de diálogo
Fuente normativa para crear o modificar diálogos de Metis Desk. Recoge las decisiones
aplicadas a los editores de plantillas de activos y recursos y a los selectores `PickerList`.
La [guía de componentes](componentes-de-interfaz.md) describe sus APIs y consumidores;
el [inventario de modales](inventario-de-modales.md) recoge las implementaciones existentes.
## Composición y reutilización
- Cada interacción modal con responsabilidad propia vive en un componente y fichero `.tsx`
independientes. El padre decide su apertura y aporta datos y callbacks.
- Reutilizar `FormDialog`, `FormBody` y `FormActions` de `@repo/rdx-ui`, apoyados en las
primitivas de shadcn/ui y Base UI. Consultar los patrones existentes antes de crear otro.
- Mantener los bloques visuales genéricos independientes de entidades, DTO, consultas y
endpoints. El módulo aporta textos traducidos, controles, datos y acciones.
- Aplicar el mismo patrón a alta, edición y copia y a entidades equivalentes. Conservar los
campos y restricciones propios de cada entidad, como el uso de las plantillas de recursos.
## Grupos, títulos y separación
- Usar `FormSectionTitle` para los títulos de grupo: texto pequeño, seminegrita, mayúsculas,
espaciado entre letras y color atenuado (`text-xs font-semibold uppercase tracking-wide
text-muted-foreground mb-4`). Conservar el nivel semántico del encabezado; las mayúsculas
pertenecen a la presentación CSS y los textos pasan por i18n.
- El título principal del diálogo y los nombres editables de secciones tienen su propia
función. El nombre editable de una sección va en su cabecera, junto a sus acciones.
- Para los datos generales, preferir `FormSection variant="plain"`, sin borde ni relleno de
tarjeta. Los grupos repetidos que necesitan identidad visual usan borde y fondo semánticos
discretos. Evitar acumular marcos y rellenos en grupos anidados.
- Mantener más separación entre grupos principales que entre controles relacionados. En el
patrón de plantillas, `TemplateMetadataSection` deja 36 px antes del grupo de secciones
(20 px del cuerpo y 16 px adicionales); las columnas y tarjetas se separan 16 px.
- Situar las acciones locales en la cabecera del grupo al que afectan. Por ejemplo,
«Añadir campo» se ofrece en «Campos» y lleva implícita la sección de destino.
## Distribución responsive y alturas
- Decidir las columnas por el ancho disponible del contenedor, mediante container queries.
En espacios estrechos, apilar el contenido y permitir que nombres y acciones se adapten.
- En formularios con nombre, icono y descripción, reutilizar `TemplateMetadataSection`:
nombre e icono debajo a la izquierda, descripción a la derecha, en dos columnas iguales
desde 560 px. Ambas columnas tienen igual altura y el área de descripción ocupa el alto
restante. En una columna, el recorrido es nombre, icono y descripción.
- Para secciones repetidas equivalentes, usar una columna con poco espacio y dos cuando sus
controles quepan con comodidad. `TemplateSectionsEditor sectionLayout="responsive"`
cambia a dos columnas desde 1.000 px. Las secciones de cada fila tienen la misma altura,
determinada por la más alta, y se reajustan al plegar, desplegar o cambiar su contenido.
- Mantener el orden de izquierda a derecha y de arriba abajo en el DOM, la numeración,
el teclado y el arrastre. No introducir un orden visual diferente mediante CSS.
- Cada grupo de campos responde a su propio ancho, también cuando su sección ocupa media
fila. El bloque «Sin sección» permanece debajo de las secciones, a ancho completo.
- No convertir formularios sencillos en cuadrículas de secciones ni imponerles altura fija.
Los umbrales anteriores corresponden a los componentes citados; otros contenidos deben
elegir su disposición según el espacio que necesitan sus controles.
## Secciones plegables y filas compactas
- Las secciones extensas o repetidas utilizan la primitiva `Collapsible` mediante
`TemplateSectionEditor`. Empiezan abiertas y conservan el estado de apertura por identidad
al reordenarse. El nombre, el agarre y las acciones permanecen en la cabecera.
- Conservar los controles al plegar. Si la validación detecta un control inválido oculto,
abrir la sección antes de enfocarlo. Plegar no modifica los datos.
- En listas de configuración de campos, utilizar la presentación compacta
`TemplateSectionsEditor fieldLayout="compact"`: agarre, switch de obligatoriedad, nombre,
flechas de desplazamiento y papelera. Evitar una segunda fila de controles cuando esta
configuración sea suficiente. Esta pauta corresponde a referencias de campos dentro de
una plantilla; los formularios de edición de sus valores conservan sus controles propios.
- Dar nombre accesible a los switches y botones de icono, incluyendo el campo o la sección.
La obligatoriedad conserva su etiqueta accesible y ayuda al pasar el ratón.
- Reutilizar `IconPicker` como desplegable compacto con icono y texto, incluida la opción
sin icono. Evitar una fila permanente con todas las opciones de iconos.
## Ordenación y selección de campos
- Reutilizar `useDragReorder`, `ReorderHandle` y el indicador de destino. Mantener flechas
explícitas y ordenación por teclado; el arrastre no es la única forma de cambiar el orden.
- Las secciones se ordenan entre sí y los campos dentro de su sección. Conservar el foco
y bloquear los movimientos imposibles o las acciones durante el guardado. Cancelar un
arrastre no cambia los datos y guardar confirma el borrador del formulario.
- Cuando se incorporen varias referencias de un catálogo, utilizar un selector múltiple
en diálogo en lugar de un desplegable que obligue a añadirlas una a una. En plantillas,
reutilizar `CatalogFieldPickerDialog` desde la cabecera de cada grupo de campos, sin un
bloque independiente de incorporación tras los datos generales.
- Confirmar la selección como lote, conservarla al buscar o paginar y descartar la selección
provisional al cancelar. Respetar límites, disponibilidad y ausencia de duplicados.
- El alta rápida opcional se ofrece a la izquierda del pie mediante `leadingActions`.
Debe poder ocultarse o deshabilitarse desde el consumidor. Mantener el selector y su estado
durante el alta anidada; crear el elemento no confirma por sí solo el lote seleccionado.
## Diálogos de selección PickerList
- Todos utilizan `FormDialog layout="picker"` y `EntityTable layout="fill"`: altura estable
adaptada a la ventana, independiente de búsquedas, carga y número de resultados. La tabla
ocupa el espacio restante y mantiene desplazamiento interno con espacio reservado para
la barra; búsqueda, filtros, paginación y acciones permanecen estables.
- Mantener el área de resultados incluso vacía y las cabeceras visibles durante el scroll.
En ventanas pequeñas, permitir el desplazamiento adicional del cuerpo para acceder a
todos los controles.
- La columna `selection` es la primera, antes del nombre: anchura ajustada al checkbox o
radio, poco espacio hasta el nombre y cabecera visualmente oculta con nombre accesible.
No se ordena, no se oculta y no se ancla a la derecha como las acciones.
- Pulsar una zona no interactiva de una fila disponible alterna su checkbox o selecciona
su radio. Reutilizar `onRowClick` de la tabla; los controles internos ejecutan solo su
propia acción, sin duplicar cambios. Respetar filas deshabilitadas y navegación por teclado.
## Cuerpo, pie y accesibilidad
- Elegir la anchura del diálogo según su contenido y respetar los márgenes de la ventana.
Mantener cabecera y pie visibles y el cuerpo desplazable cuando el contenido lo requiera.
- Los formularios conservan altura según contenido; la altura estable por resultados es
la regla de los `PickerList`. No introducir pestañas ni pasos para resolver solo espacio.
- Situar ayuda contextual general al final del cuerpo, inmediatamente antes de `FormActions`.
Mantener errores junto al contexto que los origina y las acciones dentro del formulario.
- Conservar foco, cancelación, cierre por teclado, estados de carga, error, vacío y envío.
No cerrar ni limpiar el formulario antes de confirmar una operación correcta.
- Validar los cambios de interacción con pruebas dirigidas y revisar visualmente los anchos
estrecho y amplio, nombres largos, secciones de distinta altura, plegado y resultados vacíos
cuando correspondan. Informar de las comprobaciones que no se hayan podido ejecutar.

View File

@ -1,4 +1,5 @@
import { BrowserLink } from "@metis/core/client";
import { FormSectionTitle } from "@repo/rdx-ui/components/form-section-title";
import { Button } from "@repo/shadcn-ui/components/button";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -65,7 +66,7 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
return (
<section aria-label={t("relations")} className="space-y-3">
<header className="flex flex-wrap items-center justify-between gap-2">
{!compact && <h3 className="font-medium">{t("relations")}</h3>}
{!compact && <FormSectionTitle>{t("relations")}</FormSectionTitle>}
<Button onClick={() => setAdding(true)} size="sm" variant="outline">
{t("linkAsset")}
</Button>

View File

@ -2,6 +2,7 @@ import { BrowserLink } from "@metis/core/client";
import { DetailField } from "@repo/rdx-ui/components/detail-field";
import { DetailSection } from "@repo/rdx-ui/components/detail-section";
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
import { FormSectionTitle } from "@repo/rdx-ui/components/form-section-title";
import { useTranslation } from "react-i18next";
import { useAssetDetailController } from "../controllers";
@ -28,7 +29,7 @@ export function AssetDetail(props: AssetDetailProps) {
: String(value);
return (
<div className="space-y-2">
<div className="@container space-y-5">
{!compact && <p className="text-sm text-muted-foreground">{t("snapshotNotice")}</p>}
{!compact && (
<DetailSection title={t("detailSections.identification")}>
@ -72,9 +73,7 @@ export function AssetDetail(props: AssetDetailProps) {
</DetailSection>
<div>
{!asset.specification.sections?.length && (
<h3 className="py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{t("characteristics")}
</h3>
<FormSectionTitle>{t("characteristics")}</FormSectionTitle>
)}
<FieldSections
items={asset.specification.fields}
@ -105,9 +104,9 @@ export function AssetDetail(props: AssetDetailProps) {
{children}
{!compact && (
<>
<h3 className="font-medium">
<FormSectionTitle>
{t("history")} ({asset.history.length})
</h3>
</FormSectionTitle>
<ol className="space-y-4">
{[...asset.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3 text-sm" key={revision.id}>
@ -125,7 +124,9 @@ export function AssetDetail(props: AssetDetailProps) {
{t("site")}: {nameOf(revision.values.siteId, lookups.sites)}
</p>
<details>
<summary className="cursor-pointer">{t("characteristics")}</summary>
<summary className="cursor-pointer focus-visible:outline-2 focus-visible:outline-ring">
<FormSectionTitle as="span">{t("characteristics")}</FormSectionTitle>
</summary>
<FieldSections
items={asset.specification.fields}
layout="rows"

View File

@ -1,6 +1,5 @@
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { Button } from "@repo/shadcn-ui/components/button";
import { DialogFooter } from "@repo/shadcn-ui/components/dialog";
import { useTranslation } from "react-i18next";
import { BrandApiError } from "../../shared/api";
@ -26,17 +25,16 @@ export function DeleteBrandDialog(props: DeleteBrandDialogProps) {
open
title={t("brands.delete")}
>
<p>{t("brands.deleteConfirm", { name: brand.name })}</p>
<p className="text-sm text-muted-foreground">{t("brands.deleteGuidance")}</p>
{mutation.isError && (
<p role="alert">
{t(`brands.errors.${errorCode}`, { defaultValue: t("brands.deleteError") })}
</p>
)}
<DialogFooter className="border-t pt-4">
<Button disabled={mutation.isPending} onClick={onClose} type="button" variant="outline">
{t("cancel")}
</Button>
<FormBody>
<p>{t("brands.deleteConfirm", { name: brand.name })}</p>
<p className="text-sm text-muted-foreground">{t("brands.deleteGuidance")}</p>
{mutation.isError && (
<p role="alert">
{t(`brands.errors.${errorCode}`, { defaultValue: t("brands.deleteError") })}
</p>
)}
</FormBody>
<FormActions cancelDisabled={mutation.isPending}>
<Button
disabled={mutation.isPending || brand.builtin}
onClick={() => {
@ -49,7 +47,7 @@ export function DeleteBrandDialog(props: DeleteBrandDialogProps) {
>
{t(mutation.isPending ? "brands.deleting" : "brands.delete")}
</Button>
</DialogFooter>
</FormActions>
</FormDialog>
);
}

View File

@ -1,6 +1,5 @@
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { Button } from "@repo/shadcn-ui/components/button";
import { DialogFooter } from "@repo/shadcn-ui/components/dialog";
import { useTranslation } from "react-i18next";
import type { CatalogItemDTO } from "../../../../common";
@ -34,17 +33,16 @@ export function DeleteCatalogDefinitionDialog({
open
title={t("delete")}
>
<p>{t("deleteDefinitionConfirm", { name: item.values.name })}</p>
<p className="text-sm text-muted-foreground">{t("deleteDefinitionGuidance")}</p>
{error && <p role="alert">{t(`errors.${code}`, { defaultValue: t("deleteError") })}</p>}
<DialogFooter className="border-t pt-4">
<Button disabled={pending} onClick={onClose} type="button" variant="outline">
{t("cancel")}
</Button>
<FormBody>
<p>{t("deleteDefinitionConfirm", { name: item.values.name })}</p>
<p className="text-sm text-muted-foreground">{t("deleteDefinitionGuidance")}</p>
{error && <p role="alert">{t(`errors.${code}`, { defaultValue: t("deleteError") })}</p>}
</FormBody>
<FormActions cancelDisabled={pending}>
<Button disabled={pending} onClick={onDelete} type="button" variant="destructive">
{t(pending ? "deleting" : "delete")}
</Button>
</DialogFooter>
</FormActions>
</FormDialog>
);
}

View File

@ -1,3 +1,4 @@
import { FormSectionTitle } from "@repo/rdx-ui/components/form-section-title";
import { Button } from "@repo/shadcn-ui/components/button";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -30,7 +31,7 @@ export function AssetSupport(props: AssetSupportProps) {
return (
<section className={compact ? "space-y-3" : "space-y-3 rounded-md border p-4"}>
<header className="flex flex-wrap items-center justify-between gap-3">
{!compact && <h3 className="font-semibold">{t("support.title")}</h3>}
{!compact && <FormSectionTitle>{t("support.title")}</FormSectionTitle>}
<div className="flex flex-wrap gap-2">
<Button
disabled={!query.data || retired}

View File

@ -27,7 +27,7 @@ export function WorkstationAssociationFields({
const { t } = useTranslation("assets");
return (
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
<SelectWithAction

View File

@ -40,10 +40,12 @@ export function AccessFormFields(
<>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
<FormBody>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
{editableFields.map((field) => (
<div
className={["note", "permissions"].includes(field.type) ? "sm:col-span-2" : ""}
className={
["note", "permissions"].includes(field.type) ? "@min-[560px]:col-span-2" : ""
}
key={field.key}
>
<AccessFieldControl

View File

@ -1,3 +1,4 @@
import { FormSectionTitle } from "@repo/rdx-ui/components/form-section-title";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Label } from "@repo/shadcn-ui/components/label";
import type { UseFormReturn } from "react-hook-form";
@ -24,7 +25,7 @@ export function PermissionsControl({
return (
<fieldset className="space-y-2">
<legend className="text-sm font-medium">{t(fieldKey)}</legend>
<FormSectionTitle as="legend">{t(fieldKey)}</FormSectionTitle>
{options.map((option) => (
<div className="flex items-center gap-2" key={option.value}>
<Checkbox

View File

@ -49,7 +49,7 @@ export function ApplicationConfigurationForm(props: ApplicationConfigurationForm
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
<FormBody>
<fieldset className="space-y-4" disabled={mutation.isPending}>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<SelectWithAction

View File

@ -57,7 +57,7 @@ export function AssignmentForm(props: AssignmentFormProps) {
<Label htmlFor={`${id}-position`}>{t("departmentPosition")}</Label>
<Input id={`${id}-position`} maxLength={100} {...form.register("position")} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${id}-start`}>{t("startDate")}</Label>
<Input id={`${id}-start`} required type="date" {...form.register("startDate")} />

View File

@ -48,7 +48,7 @@ export function CertificateConfigurationForm(props: CertificateConfigurationForm
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
<FormBody>
<fieldset className="space-y-4" disabled={mutation.isPending}>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<SelectWithAction

View File

@ -1,4 +1,4 @@
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { Button } from "@repo/shadcn-ui/components/button";
import { useTranslation } from "react-i18next";
@ -19,7 +19,7 @@ export function DashboardQuickCreate(props: DashboardQuickCreateProps) {
<>
<FormDialog
cancelLabel={t("cancel")}
formActions={kind !== "asset" && kind !== "resource"}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
@ -30,35 +30,41 @@ export function DashboardQuickCreate(props: DashboardQuickCreateProps) {
>
{kind === "company" && <CompanyForm onSaved={saved} showTitle={false} />}
{(kind === "asset" || kind === "resource") && (
<BlockState empty={false} emptyText="" query={companies}>
<SelectWithAction
action={{ label: t("newCompany"), onClick: () => setAddingCompany(true) }}
id="dashboard-create-company"
label={t("companyLabel")}
onValueChange={setSelectedCompany}
options={
companies.data?.companies
.filter((item) => item.active)
.map((item) => ({ value: item.id, label: item.name })) ?? []
}
placeholder={t("dashboard.chooseCompany")}
value={selectedCompany}
/>
<p className="text-sm text-muted-foreground">{t("dashboard.chooseCompany")}</p>
<Button
disabled={!selectedCompany}
onClick={() =>
onNavigate({
area: "company",
id: selectedCompany,
section: kind === "asset" ? "inventory" : "resources",
create: true,
})
}
>
{t("dashboard.continue")}
</Button>
</BlockState>
<>
<FormBody>
<BlockState empty={false} emptyText="" query={companies}>
<SelectWithAction
action={{ label: t("newCompany"), onClick: () => setAddingCompany(true) }}
id="dashboard-create-company"
label={t("companyLabel")}
onValueChange={setSelectedCompany}
options={
companies.data?.companies
.filter((item) => item.active)
.map((item) => ({ value: item.id, label: item.name })) ?? []
}
placeholder={t("dashboard.chooseCompany")}
value={selectedCompany}
/>
<p className="text-sm text-muted-foreground">{t("dashboard.chooseCompany")}</p>
</BlockState>
</FormBody>
<FormActions>
<Button
disabled={!(selectedCompany && companies.isSuccess)}
onClick={() =>
onNavigate({
area: "company",
id: selectedCompany,
section: kind === "asset" ? "inventory" : "resources",
create: true,
})
}
>
{t("dashboard.continue")}
</Button>
</FormActions>
</>
)}
</FormDialog>
{addingCompany && (

View File

@ -76,7 +76,7 @@ export function FolderMappingForm(props: FolderMappingFormProps) {
</Button>
</div>
)}
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-letter`}>{t("folders.letter")}</Label>
<Input

View File

@ -75,7 +75,7 @@ export function MailboxUseForm(props: MailboxUseFormProps) {
value={values.personId}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-start`}>{t("mailbox.start")}</Label>
<Input

View File

@ -1,3 +1,4 @@
import { FormSectionTitle } from "@repo/rdx-ui/components/form-section-title";
import { Button } from "@repo/shadcn-ui/components/button";
import { useTranslation } from "react-i18next";
@ -14,7 +15,7 @@ export function MailboxRelations(props: MailboxRelationsProps) {
return (
<section className="space-y-3 border-t pt-4">
<div className="flex items-center justify-between gap-3">
<h3 className="font-medium">{t("people")}</h3>
<FormSectionTitle>{t("people")}</FormSectionTitle>
<Button onClick={() => setEditor("new")} size="sm" variant="outline">
{t("mailbox.linkPerson")}
</Button>

View File

@ -53,7 +53,7 @@ export function PersonForm(props: PersonFormProps) {
onChange={setPhotoFile}
person={person}
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
@ -65,7 +65,7 @@ export function PersonForm(props: PersonFormProps) {
</div>
</FormSection>
<FormSection title={t("personForm.contact")} variant="plain">
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${id}-email`}>{t("emailOptional")}</Label>
<Input id={`${id}-email`} maxLength={254} type="email" {...form.register("email")} />

View File

@ -52,7 +52,7 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) {
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
<FormBody>
<fieldset className="space-y-4" disabled={mutation.isPending}>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<SelectWithAction

View File

@ -62,7 +62,7 @@ export function RemoteConnectionForm(props: RemoteConnectionFormProps) {
value={v.personId}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<RemoteEndpointFields
assetOptions={assetOptions}
company={company}

View File

@ -0,0 +1,39 @@
import { ResourceEditor } from "@metis/resources/client";
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
export function SoftwareResourceCreateDialog({
companyId,
kind,
onClose,
onSaved,
}: {
companyId: string;
kind: "os" | "application";
onClose: () => void;
onSaved: (id: string) => void;
}) {
const { t } = useTranslation("companies");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t(kind === "os" ? "software.newOs" : "applications.newResource")}
wide
>
<ResourceEditor
allowQuickCreate={false}
companyId={companyId}
initialKind={kind}
onSaved={onSaved}
/>
</FormDialog>
);
}

View File

@ -12,7 +12,6 @@ import {
EntityStatus,
EntityTable,
} from "@repo/rdx-ui/components/entity-list";
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { useId, useState } from "react";
@ -21,7 +20,7 @@ import { useTranslation } from "react-i18next";
import { DeleteRecordDialog } from "../../../shared/ui/delete-record-dialog";
import { SoftwareHistoryDialog } from "../../history/ui/software-history-dialog";
import { softwareApi, softwareKeys } from "../../shared/api/software-api";
import { SoftwareInstallationForm } from "../../shared/ui/software-installation-form";
import { SoftwareInstallationEditorDialog } from "../../shared/ui/software-installation-editor-dialog";
export function SoftwareInstallationsPanel({
companyId,
@ -255,26 +254,15 @@ export function SoftwareInstallationsPanel({
/>
)}
{editor && assetId && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setEditor(undefined);
}
}}
open
title={t(edited ? "software.edit" : "software.new")}
>
<SoftwareInstallationForm
assetId={assetId}
companyId={companyId}
installation={edited}
licenseOptions={licenseOptions}
onSaved={() => setEditor(undefined)}
options={options}
/>
</FormDialog>
<SoftwareInstallationEditorDialog
assetId={assetId}
companyId={companyId}
installation={edited}
licenseOptions={licenseOptions}
onClose={() => setEditor(undefined)}
onSaved={() => setEditor(undefined)}
options={options}
/>
)}
{viewed && (
<SoftwareHistoryDialog

View File

@ -0,0 +1,28 @@
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { SoftwareInstallationForm } from "./software-installation-form";
export function SoftwareInstallationEditorDialog({
onClose,
...formProps
}: ComponentProps<typeof SoftwareInstallationForm> & { onClose: () => void }) {
const { t } = useTranslation("companies");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t(formProps.installation ? "software.edit" : "software.new")}
>
<SoftwareInstallationForm {...formProps} />
</FormDialog>
);
}

View File

@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ResourceEditor } from "@metis/resources/client";
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input";
@ -14,6 +13,7 @@ import { useTranslation } from "react-i18next";
import type { SoftwareInstallationDTO, SoftwareInstallationInput } from "../../../../common";
import { SOFTWARE_STATE_TRANSITIONS, SoftwareInstallationInputSchema } from "../../../../common";
import { SoftwareResourceCreateDialog } from "../../create/ui/software-resource-create-dialog";
import { SoftwareApiError, softwareApi, softwareKeys } from "../api/software-api";
export function SoftwareInstallationForm({
@ -90,7 +90,7 @@ export function SoftwareInstallationForm({
{t("applications.newResource")}
</Button>
)}
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-version`}>{t("applications.version")}</Label>
<Input id={`${prefix}-version`} maxLength={100} {...form.register("version")} />
@ -160,28 +160,15 @@ export function SoftwareInstallationForm({
</FormActions>
</form>
{quick && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setQuick(undefined);
}
<SoftwareResourceCreateDialog
companyId={companyId}
kind={quick}
onClose={() => setQuick(undefined)}
onSaved={(id) => {
form.setValue("resourceId", id);
setQuick(undefined);
}}
open
title={t(quick === "os" ? "software.newOs" : "applications.newResource")}
wide
>
<ResourceEditor
allowQuickCreate={false}
companyId={companyId}
initialKind={quick}
onSaved={(id) => {
form.setValue("resourceId", id);
setQuick(undefined);
}}
/>
</FormDialog>
/>
)}
</>
);

View File

@ -54,7 +54,7 @@ export function TeleworkAuthorizationForm(props: TeleworkAuthorizationFormProps)
label={t("telework.active")}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label>
<Input

View File

@ -73,7 +73,7 @@ export function WorkProfileForm(props: WorkProfileFormProps) {
)}
</div>
)}
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
{selectors.map((selector) => {
const fixed = !!profile && selector.field !== "accountId";

View File

@ -30,7 +30,7 @@ export function IntegrationSummary({
}));
return (
<dl className="grid gap-2 sm:grid-cols-2">
<dl className="grid gap-2 @min-[560px]:grid-cols-2">
{entries.map((entry) => (
<div key={entry.label}>
<dt className="text-sm text-muted-foreground">{entry.label}</dt>

View File

@ -1,5 +1,6 @@
import { DetailField } from "@repo/rdx-ui/components/detail-field";
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
import { FormSectionTitle } from "@repo/rdx-ui/components/form-section-title";
import { useTranslation } from "react-i18next";
import type { ResourceDTO, ResourceReferenceDTO } from "../../../../common/inventory";
@ -25,10 +26,10 @@ export function ResourceDetail({
: String(value);
return (
<div className="space-y-5">
<div className="@container space-y-5">
{!compact && <p className="text-sm text-muted-foreground">{t("snapshotNotice")}</p>}
{!compact && (
<dl className="grid gap-4 sm:grid-cols-2">
<dl className="grid gap-4 @min-[560px]:grid-cols-2">
<div>
<dt className="text-sm text-muted-foreground">{t("template")}</dt>
<dd>{resource.specification.templateName}</dd>
@ -65,9 +66,9 @@ export function ResourceDetail({
)}
{!compact && (
<>
<h3 className="font-medium">
<FormSectionTitle>
{t("history")} ({resource.history.length})
</h3>
</FormSectionTitle>
<ol className="space-y-4">
{[...resource.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3 text-sm" key={revision.id}>

View File

@ -32,7 +32,7 @@ export function ResourceIntegrationFields({
if (mailbox) {
return (
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-kind`}>{t("mailboxKind")}</Label>
<SelectWithAction
@ -104,7 +104,7 @@ export function ResourceIntegrationFields({
};
return (
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 @min-[560px]:grid-cols-2">
{(["resourceTypeId", "systemId", "environmentId"] as const).map((key) => {
const kind =
key === "resourceTypeId"

View File

@ -48,7 +48,7 @@ export function ConfirmDeleteDialog({
</p>
)}
</FormBody>
<FormActions>
<FormActions cancelDisabled={pending}>
<Button
disabled={pending}
onClick={async () => {

View File

@ -2,16 +2,18 @@ import type { ReactNode } from "react";
import { ChevronRightIcon } from "../lib/icons";
import { FormSectionTitle } from "./form-section-title";
/** Grupo de consulta compartido por fichas y plantillas; abierto inicialmente. */
export function DetailSection({ title, children }: { title: string; children: ReactNode }) {
return (
<details className="@container group/detail-section" open>
<summary className="flex cursor-pointer list-none items-center gap-2 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden">
<summary className="flex cursor-pointer list-none items-center gap-2 py-2 focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden">
<ChevronRightIcon
aria-hidden="true"
className="size-3 shrink-0 transition-transform group-open/detail-section:rotate-90 motion-reduce:transition-none"
className="size-3 shrink-0 text-muted-foreground transition-transform group-open/detail-section:rotate-90 motion-reduce:transition-none"
/>
{title}
<FormSectionTitle as="span">{title}</FormSectionTitle>
</summary>
<div className="space-y-3 border-t border-border/60 px-5 pb-2 [&>dl>div:first-child]:pt-2.5 [&>dl>div:last-child]:pb-2.5">
{children}

View File

@ -0,0 +1,49 @@
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import type { Table as TableModel } from "@tanstack/react-table";
import { useId } from "react";
import { entityListLabels } from "../lib/entity-list";
import { FormDialog } from "./form-dialog";
export function EntityTableColumnsDialog<T>({
table,
locale,
open,
onOpenChange,
}: {
table: TableModel<T>;
locale: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const id = useId();
const labels = entityListLabels(locale);
return (
<FormDialog
cancelLabel={locale.startsWith("en") ? "Close" : "Cerrar"}
onOpenChange={onOpenChange}
open={open}
title={labels.columns}
>
<div className="space-y-3">
{table.getAllLeafColumns().map((column) => (
<label
className="flex items-center gap-2"
htmlFor={`${id}-column-${column.id}`}
key={column.id}
>
<Checkbox
checked={column.getIsVisible()}
disabled={!column.getCanHide()}
id={`${id}-column-${column.id}`}
onCheckedChange={(checked) => column.toggleVisibility(checked === true)}
/>
{String(column.columnDef.header)}
</label>
))}
</div>
</FormDialog>
);
}

View File

@ -1,11 +1,10 @@
import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import type { Table as TableModel } from "@tanstack/react-table";
import { useId, useState } from "react";
import { entityListLabels } from "../lib/entity-list";
import { FormDialog } from "./form-dialog";
import { EntityTableColumnsDialog } from "./entity-table-columns-dialog";
import { SelectWithAction } from "./select-with-action";
export function EntityTableConfiguration<T>({
@ -60,30 +59,12 @@ export function EntityTableConfiguration<T>({
value={sorting[0] ? `${sorting[0].id}:${sorting[0].desc ? "desc" : "asc"}` : "initial"}
/>
</div>{" "}
<FormDialog
cancelLabel={locale.startsWith("en") ? "Close" : "Cerrar"}
<EntityTableColumnsDialog
locale={locale}
onOpenChange={setColumnsOpen}
open={columnsOpen}
title={labels.columns}
>
<div className="space-y-3">
{table.getAllLeafColumns().map((column) => (
<label
className="flex items-center gap-2"
htmlFor={`${id}-column-${column.id}`}
key={column.id}
>
<Checkbox
checked={column.getIsVisible()}
disabled={!column.getCanHide()}
id={`${id}-column-${column.id}`}
onCheckedChange={(checked) => column.toggleVisibility(checked === true)}
/>
{String(column.columnDef.header)}
</label>
))}
</div>
</FormDialog>
table={table}
/>
</>
);
}

View File

@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { ChevronRightIcon } from "../lib/icons";
import { DetailSection } from "./detail-section";
import { FormSectionTitle } from "./form-section-title";
export type FieldSection = { id: string; name: string; description: string };
@ -99,10 +100,10 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
{styles.compact && (
<ChevronRightIcon
aria-hidden="true"
className="size-3 shrink-0 transition-transform group-open/field-section:rotate-90 motion-reduce:transition-none"
className="size-3 shrink-0 text-muted-foreground transition-transform group-open/field-section:rotate-90 motion-reduce:transition-none"
/>
)}
{section.name}
<FormSectionTitle as="span">{section.name}</FormSectionTitle>
</summary>
<div className={styles.body}>
{section.description && <p className={styles.description}>{section.description}</p>}
@ -113,7 +114,9 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
})}
{ungrouped.length > 0 && (
<section className="col-span-full space-y-3">
<h4 className={styles.ungrouped}>{ungroupedLabel}</h4>
<div className={styles.compact ? "pl-5" : undefined}>
<FormSectionTitle as="h4">{ungroupedLabel}</FormSectionTitle>
</div>
{content(ungrouped)}
</section>
)}
@ -129,8 +132,8 @@ function fieldSectionStyles(layout: FieldSectionLayout, values: boolean) {
? "@container"
: "@container rounded-md border";
const summary = compact
? "flex cursor-pointer list-none items-center gap-2 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden"
: `cursor-pointer text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring ${values ? "py-2" : "rounded-md px-4 py-3"}`;
? "flex cursor-pointer list-none items-center gap-2 py-2 focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden"
: `cursor-pointer focus-visible:outline-2 focus-visible:outline-ring ${values ? "py-2" : "rounded-md px-4 py-3"}`;
const body =
layout === "form"
? "space-y-3 border-t border-border/60 pt-3"
@ -150,20 +153,17 @@ function fieldSectionStyles(layout: FieldSectionLayout, values: boolean) {
body,
description,
...FIELD_SECTION_LAYOUTS[layout],
ungrouped: compact
? "pl-5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
: "text-sm font-medium",
};
}
const FIELD_SECTION_LAYOUTS = {
fields: {
content: "grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3",
wrapper: "grid items-start gap-4 @min-[900px]:grid-cols-2",
content: "grid gap-4 @min-[560px]:grid-cols-2 @min-[900px]:grid-cols-3",
wrapper: "grid items-stretch gap-4 @min-[900px]:grid-cols-2",
},
rows: {
content: "divide-y divide-border/50",
wrapper: "grid items-start gap-x-8 gap-y-3 @min-[900px]:grid-cols-2",
wrapper: "grid items-stretch gap-x-8 gap-y-3 @min-[900px]:grid-cols-2",
},
form: {
content:

View File

@ -75,9 +75,11 @@ export function FormDialog({
export function FormActions({
children,
leadingActions,
cancelDisabled = false,
}: {
children: ReactNode;
leadingActions?: ReactNode;
cancelDisabled?: boolean;
}) {
const cancelLabel = useContext(FormCancelContext);
@ -85,7 +87,9 @@ export function FormActions({
<DialogFooter className="shrink-0 border-t bg-popover py-4">
{leadingActions && <div className="flex sm:mr-auto">{leadingActions}</div>}
{cancelLabel && (
<DialogClose render={<Button type="button" variant="outline" />}>{cancelLabel}</DialogClose>
<DialogClose render={<Button disabled={cancelDisabled} type="button" variant="outline" />}>
{cancelLabel}
</DialogClose>
)}
{children}
</DialogFooter>
@ -100,8 +104,8 @@ export function FormBody({ children }: { children: ReactNode }) {
<div
className={
layout === "picker"
? "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-1 pt-1 pb-5 [scrollbar-gutter:stable] [&>*]:shrink-0"
: "min-h-0 flex-1 space-y-5 overflow-y-auto px-1 pt-1 pb-5"
? "@container flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-1 pt-1 pb-5 [scrollbar-gutter:stable] [&>*]:shrink-0"
: "@container min-h-0 flex-1 space-y-5 overflow-y-auto px-1 pt-1 pb-5"
}
data-slot="form-body"
>

View File

@ -8,11 +8,11 @@ export function FormSectionTitle({
}: {
children: ReactNode;
id?: string;
as?: "h3" | "h4" | "h5";
as?: "h3" | "h4" | "h5" | "legend" | "span";
}) {
return (
<Heading
className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-4"
className={`text-xs font-semibold uppercase tracking-wide text-muted-foreground ${Heading === "span" ? "" : "mb-4"}`}
id={id}
>
{children}