From 9fc7d3a2e0cfbaf17c17960de9fb31752ba2fe29 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 11:16:59 +0200 Subject: [PATCH] feat: add entity list state management and UI components - Implemented asset and resource list query state helpers to manage loading, error, and availability states. - Created EntityListStatus component for displaying status messages and retry options based on entity list state. - Added EntityTableSkeleton component for rendering loading placeholders in tables. - Defined EntityListState type and entityListState function for consistent state management across components. - Developed comprehensive UI tests for list states using Playwright, covering various scenarios including loading, error handling, and responsiveness. --- DESIGN.md | 6 +- docs/auditoria-listados-l01.md | 25 ++ docs/auditoria-ui-ux-visual.md | 22 ++ docs/componentes-de-interfaz.md | 24 ++ docs/estados-listados-l02-3.md | 266 ++++++++++++++++++ docs/patron-de-listados-de-entidades.md | 14 + modules/assets/src/common/locales/en.json | 23 ++ modules/assets/src/common/locales/es.json | 23 ++ .../use-inventory-page-controller.ts | 8 +- .../list/helpers/asset-list-query-state.ts | 33 +++ .../list/ui/asset-workstation-action.tsx | 3 + .../src/web/assets/list/ui/inventory-page.tsx | 200 ++++++------- .../src/web/assets/shared/ui/asset-list.tsx | 16 ++ modules/resources/src/common/locales/en.json | 16 ++ modules/resources/src/common/locales/es.json | 16 ++ .../use-resources-page-controller.ts | 31 +- .../list/helpers/resource-list-query-state.ts | 32 +++ .../web/resources/list/ui/resources-page.tsx | 54 ++-- .../src/components/entity-list-controls.tsx | 11 +- .../src/components/entity-list-status.tsx | 66 +++++ .../rdx-ui/src/components/entity-list.tsx | 49 +++- .../components/entity-table-configuration.tsx | 9 +- .../src/components/entity-table-content.tsx | 30 +- .../src/components/entity-table-skeleton.tsx | 23 ++ packages/rdx-ui/src/entity-list.test.tsx | 130 +++++++++ packages/rdx-ui/src/lib/entity-list-state.ts | 61 ++++ packages/rdx-ui/src/locales/en.json | 3 +- packages/rdx-ui/src/locales/es.json | 3 +- scripts/list-states-ui.test.py | 181 ++++++++++++ scripts/picker-ui.test.py | 5 +- scripts/visual-audit-ui.test.py | 7 +- 31 files changed, 1226 insertions(+), 164 deletions(-) create mode 100644 docs/estados-listados-l02-3.md create mode 100644 modules/assets/src/web/assets/list/helpers/asset-list-query-state.ts create mode 100644 modules/resources/src/web/resources/list/helpers/resource-list-query-state.ts create mode 100644 packages/rdx-ui/src/components/entity-list-status.tsx create mode 100644 packages/rdx-ui/src/components/entity-table-skeleton.tsx create mode 100644 packages/rdx-ui/src/lib/entity-list-state.ts create mode 100644 scripts/list-states-ui.test.py diff --git a/DESIGN.md b/DESIGN.md index 652665a..4390f4e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -213,8 +213,8 @@ En diálogos anidados, mantener borrador padre y foco de retorno; cancelar selec | Situación | Convención vigente / limitación conocida | | --- | --- | -| Carga inicial | Mensaje de estado sin fingir vacío; reservar estructura cuando la integración lo permita. Activos aún desmonta controles, V12. | -| Actualización en segundo plano | TanStack Query gestiona estado remoto; conservar datos y controles disponibles. No duplicar la caché en estado local. | +| Carga inicial | Conservar herramientas, cabeceras conocidas y paginación pendiente; skeletons decorativos sin registros/totales ficticios. Activos y Recursos aplican el estado explícito de [L02.3](docs/estados-listados-l02-3.md); otros consumidores mantienen su contrato. | +| Actualización en segundo plano | TanStack Query gestiona estado remoto; conservar instancia, datos, controles, selección y foco. Indicación discreta en la banda del listado, sin anuncios/toasts repetidos. Si falla, identificar los datos anteriores y ofrecer reintento dirigido. No duplicar la caché en estado local. | | Sin registros / sin resultados | Distinguir primer registro de filtros sin coincidencias. Mantener CTA de alta o limpieza cuando proceda; EmptyState para incorporación de elementos en pasos. | | Éxito | Confirmar solo tras respuesta satisfactoria. No equiparar color primario con éxito: FormFeedback mantiene una inconsistencia señalada en la auditoría, sin estado de éxito visual validado. | | Error | Mensaje recuperable, contexto del campo/formulario/consulta y reintento pertinente; sin SQL, stacks ni secretos. | @@ -276,7 +276,7 @@ V02, V03, V06 y V09 están **resueltos en los escenarios verificados del sprint | V09 | Resuelto: etiquetas/i18n de seis listados | Cabeceras breves y motivos conservados; tests es/en y navegador | | V10 | Jerarquía de ficha | Diseñar resumen de vacíos y grupos sin ocultar relaciones/acciones; validar fixture poblada | | V11 | Renderizadores y semántica de ausencia | Aplicar pauta de «—» distinguiendo estado de negocio | -| V12 | Carga inicial de implementación | Reservar estructura y mensajes precisos; probar recuperación y actualización | +| V12 | Resuelto en los escenarios verificados de L02.3 | Estructura inicial estable en Activos/Recursos, refetch con caché, error diferenciado y recuperación accesible; véase [informe](docs/estados-listados-l02-3.md) | La auditoría usó una compañía y un portátil. Faltan relaciones pobladas, fichas de otras entidades, todos los pasos de puesto/perfil, guardados positivos, rendimiento con volumen y comprobación de accesibilidad ampliada. Los riesgos detectados solo por código conservan esa categoría; no implican fallo visual demostrado. diff --git a/docs/auditoria-listados-l01.md b/docs/auditoria-listados-l01.md index ca89414..4f8532c 100644 --- a/docs/auditoria-listados-l01.md +++ b/docs/auditoria-listados-l01.md @@ -744,3 +744,28 @@ compañías sintéticas. Las evidencias y los fallos globales previos figuran en **V08 queda resuelto en los escenarios verificados. V05, V07 y V12 permanecen pendientes.** No cambia el estado de los demás hallazgos ni se implementa L02.3. + +## Seguimiento L02.3 — Estabilidad de estados V12 + +El 22 de octubre de 2026, [L02.3](estados-listados-l02-3.md) corrige V12 en los escenarios +verificados. Se conservan íntegros el diagnóstico y las medidas de la sección 11 como +línea base. Antes del cambio se reproduce de nuevo el GET retenido a 1440×900 y 320×640: +cero tablas/búsquedas/configuración, solo cabecera y «Cargando catálogo…». + +Activos mantiene ahora la instancia de tabla, búsqueda, cabeceras conocidas y paginación +pendiente, con skeletons decorativos. Catálogo y red tienen disponibilidad y recuperación +independientes; los datos accesorios no bloquean filas. Recursos adopta el mismo estado +sin cambiar sus rutas ni selección. Un refetch conserva datos y contexto; su fallo indica +explícitamente que se muestran datos anteriores. Error inicial, vacío válido, cero +coincidencias y permiso denegado se distinguen. Reintentar devuelve el foco a la búsqueda; +el aviso queda fuera del ancho desplazable de la tabla. + +Playwright MCP con login real y datos sintéticos comprueba las siete resoluciones, +retención/liberación, 503/403 simulados, recuperación, plantilla/búsqueda, orden/página, +selección, scroll, foco, borrador, movimiento reducido y es/en. La carga mantiene diez +placeholders sin controles ficticios. En refetch se conservan scrolls de 120/150 px y la +misma tabla; los nuevos textos medidos obtienen contraste de 4,83:1 (carga) y 10,01:1 (error). +El informe documenta suites, capturas abiertas, geometría y fallos técnicos preexistentes. + +**V12 queda resuelto en ese alcance; V05 y V07 siguen pendientes y V08 conserva su cierre +L02.2.** No se generaliza la adopción a todos los listados ni se implementan L03/L04. diff --git a/docs/auditoria-ui-ux-visual.md b/docs/auditoria-ui-ux-visual.md index b75df52..857cbf7 100644 --- a/docs/auditoria-ui-ux-visual.md +++ b/docs/auditoria-ui-ux-visual.md @@ -518,3 +518,25 @@ técnicos fallidos sin presentarlos como suites globales correctas. **V05, V07 y V12 siguen pendientes.** La normalización añadida solo corresponde al buscador de columnas; no corrige las búsquedas de entidades. No se cambia el estado de otros hallazgos ni se acredita WCAG completo o pruebas con datos reales. + +## Seguimiento L02.3 — Estados de consulta V12 + +El 22 de octubre de 2026 se completa [L02.3](estados-listados-l02-3.md) en Activos y Recursos. +**V12 pasa a resuelto en los escenarios verificados**, conservando la auditoría original +como línea base. El GET inicial retenido ya no desmonta búsqueda, herramientas ni tabla; +los placeholders no contienen entidades o acciones ficticias. Catálogo y datos de red +no bloquean indiscriminadamente la consulta principal. + +La navegación autenticada con Playwright MCP comprueba carga, refetch retenido/correcto, +503 inicial/posterior simulados, reintento, vacío válido, filtros, 403 y dependencias. +Se conserva la instancia DOM, selección, orden/página, columnas, scroll y foco durante +el refetch. Los mensajes de error no dependen del scroll horizontal; Reintentar devuelve +el foco a la búsqueda persistente. También se comprueban borrador de edición, es/en, +movimiento reducido y contraste de los textos nuevos. La matriz inicial cubre siete +resoluciones entre 320×640 y 1920×1080. + +Se utiliza una instalación propia desechable con quince compañías y una principal con +50 personas, 51 activos y 66 recursos. El informe enlaza evidencias locales y resultados +técnicos reales, diferenciando errores de red provocados y fallos globales preexistentes. +**V05/V07 continúan pendientes; V08 mantiene su estado L02.2.** No se declara WCAG completo, +rendimiento de producción ni adopción universal del nuevo contrato. diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index b8875db..ce6e72b 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -1180,6 +1180,30 @@ almacenamiento contenga una preferencia contraria. Los datos inválidos o el alm bloqueado no impiden utilizar la tabla. Cambiar de clave carga las preferencias de la nueva vista sin escribir sobre ellas la selección de la anterior. +### Estados de consulta de listados — L02.3 + +`EntityListState` y `entityListState` (`@repo/rdx-ui/lib/entity-list-state`) separan +disponibilidad y actividad sin conocer DTO, TanStack Query, endpoints ni permisos. +La unión tiene estados `loading`, `ready`, `refreshing`, `unavailable` y `stale`; +los errores incluyen `retrying`. El módulo adapta su consulta y aporta mensajes es/en. + +`EntityListStatus` compone Alert/Button existentes. Recibe `state`, `retryLabel`, +`onRetry` asíncrono y `retryFocus` opcional para un destino persistente; no carga datos. +La carga tiene estado accesible, el refetch normal evita anuncios repetidos y el error +permite recuperación. `EntityTableSkeleton` reserva filas decorativas, sin controles +ni identidades ficticias, y respeta movimiento reducido. + +`EntityTable` admite `state`, `onRetry` y `columnsDisabled` opcionales. Los inventarios +de Activos/Recursos adoptan estos estados con layout `content`, conservando una instancia +de tabla, búsqueda, columnas, página y foco. La banda de estado queda fuera del scroll +horizontal. La búsqueda es el destino de foco de Reintentar. Sin el contrato nuevo, +se conservan `loading`/`unavailable` y comportamiento de selectores legacy. Paginación +puede estar deshabilitada mientras el total es desconocido, sin anunciar registros cero. + +Los adaptadores de módulo separan consulta principal, catálogo y referencias accesorias; +una denegación explícita no habilita consulta de caché. Véanse estados, límites de adopción, +matriz responsive y evidencias en [L02.3](estados-listados-l02-3.md). + ### Configuración avanzada de columnas — L02.2 `EntityColumn` admite `groupId?: string` y `keywords?: readonly string[]`. diff --git a/docs/estados-listados-l02-3.md b/docs/estados-listados-l02-3.md new file mode 100644 index 0000000..ad1e698 --- /dev/null +++ b/docs/estados-listados-l02-3.md @@ -0,0 +1,266 @@ +# L02.3 — Estabilidad de listados y estados de consulta (V12) + +Fecha: 22 de octubre de 2026. Implementación en Activos y adopción equivalente en Recursos. +Este documento complementa [L01](auditoria-listados-l01.md), [L02.1](selectores-accesibilidad-l02-1.md), +[L02.2](configuracion-columnas-l02-2.md) y el [patrón de listados](patron-de-listados-de-entidades.md). + +## 1. Resumen de la implementación + +El inventario mantiene su instancia de `AssetList`/`EntityTable` desde la primera petición. +Búsqueda, cabeceras conocidas, configuración y espacio de paginación permanecen reconocibles. +La consulta principal, el catálogo y los datos de red tienen estados y reintentos independientes. +Las actualizaciones conservan la caché de TanStack Query y comunican cuándo fallan. +No se modifican backend, rutas, autenticación, permisos, dependencias ni vendor. + +Archivos afectados, agrupados por responsabilidad: + +| Área | Archivos principales | +| --- | --- | +| rdx-ui, componentes | `entity-list.tsx`, `entity-list-controls.tsx`, `entity-table-configuration.tsx`, `entity-table-content.tsx`; nuevos `entity-list-status.tsx` y `entity-table-skeleton.tsx`, en `packages/rdx-ui/src/components/`. | +| rdx-ui, contrato/pruebas/textos | Nuevo `src/lib/entity-list-state.ts`, `src/entity-list.test.tsx` y `src/locales/{es,en}.json`. | +| Activos | `modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts`, nuevo `list/helpers/asset-list-query-state.ts`, `list/ui/inventory-page.tsx`, `list/ui/asset-workstation-action.tsx`, `shared/ui/asset-list.tsx` y `modules/assets/src/common/locales/{es,en}.json`. | +| Recursos | `modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts`, nuevo `list/helpers/resource-list-query-state.ts`, `list/ui/resources-page.tsx` y `modules/resources/src/common/locales/{es,en}.json`. | +| Pruebas MCP | Nuevo `scripts/list-states-ui.test.py`; actualización de `scripts/picker-ui.test.py` y `scripts/visual-audit-ui.test.py` para contratos vigentes y datos abundantes. | +| Documentación | Este informe, `DESIGN.md`, `docs/patron-de-listados-de-entidades.md`, `docs/componentes-de-interfaz.md` y los seguimientos de `docs/auditoria-listados-l01.md` / `docs/auditoria-ui-ux-visual.md`. | + +## 2. Diagnóstico de consultas + +| Consulta / clave real | Datos y necesidad | Disponibilidad, actualización y error | +| --- | --- | --- | +| `inventoryKeys.company(companyId)` → `["inventory", companyId]` | GET `/api/assets/:companyId`: activos, instantáneas, puestos y relaciones. Imprescindible para filas y selección. | Inicialmente `data` indefinido; refetch conserva datos. Error inicial impide operar sobre filas; error posterior permite consultar la copia anterior. | +| `catalogKeys.scope(companyId)` → `["asset-catalog", companyId]` | GET `/api/asset-catalog/:companyId`: plantillas y campos vigentes. Necesario para configurar el catálogo completo y editar/crear. | Independiente del inventario. Las instantáneas permiten consultar filas mientras llega; sin catálogo se explica la limitación y se deshabilitan configuración y alta. Si falla refetch se conservan las definiciones anteriores y se indica su antigüedad. | +| `infrastructureKeys.company(companyId)` → `["asset-infrastructure", companyId]` | GET `/api/asset-infrastructure/:companyId`: roles, interfaces, IP y MAC. Accesorio para el inventario. | Pendiente/error no bloquea filas; aviso localizado y reintento propio. Refetch fallido mantiene los últimos datos de red. | +| `inventoryKeys.company(companyId)` de Resources → `["resource-inventory", companyId]` | GET `/api/resources/:companyId`: recursos y referencias. | Misma separación entre carga, disponibilidad, actualización y error; conserva contratos de selección URL y diálogo explícito. | +| `catalogKeys.scope(companyId)` de Resources → `["resource-catalog", companyId]` | GET `/api/resource-catalog/:companyId`: plantillas de recursos. | No bloquea lectura de instantáneas. Configuración y alta requieren referencias disponibles; el error tiene recuperación propia. | + +Estas consultas no dependen unas de otras y se habilitan al montar la página con `companyId`. +Sedes/personas llegan mediante `lookups` desde la consulta de compañía del padre; no se añaden +consultas ni se cambia ese requisito de montaje. Los campos dinámicos se derivan del catálogo +y de las instantáneas reales, sin consulta adicional. + +`useListQuery` mantiene su intervalo de 60 s en primer plano, refetch al recuperar foco/conexión +y deduplicación por claves. El cliente real configura `retry: false`: los reintentos añadidos +son explícitos y dirigidos a la consulta correspondiente. No cambia la política global. + +Antes, `AssetInventoryPage` condicionaba `AssetList` a `query.data` y agrupaba el pending del +inventario, catálogo e infraestructura. El mensaje genérico «Cargando catálogo…» no identificaba +la consulta retenida. En Recursos, el pending/error combinado podía ocultar datos utilizables. + +## 3. Modelo de estados adoptado + +`EntityListState` es una unión discriminada presentacional: `loading`, `ready`, `refreshing`, +`unavailable` y `stale`. `entityListState` combina disponibilidad, actividad y fallo; no conoce +TanStack Query. Los adaptadores de módulo leen `data`, `isFetching`, `isError` y `error`. + +| Datos disponibles | Consulta | Presentación | +| --- | --- | --- | +| No | Pendiente | `loading`, mensaje específico y skeleton | +| Sí, incluidos cero registros | Correcta | `ready`; vacío solo si la respuesta válida está vacía | +| Sí | En curso | `refreshing`, filas anteriores y aviso discreto | +| No | Fallida | `unavailable`, error y reintento recuperable | +| Sí | Fallida | `stale`, copia anterior, aviso explícito y reintento | + +Los errores conservan `retrying` para deshabilitar el reintento duplicado e indicar actividad. +No se guardan DTO, marcas temporales ni estados de consulta duplicados en React. Se opta por +explicar «últimos datos disponibles» sin introducir una hora ni duplicar `dataUpdatedAt`. + +## 4. Cambios de composición + +Activos monta siempre `AssetList`, sin keys dependientes de la consulta. Su controlador deriva +el conjunto legible de la caché; Resources aplica la misma regla. `EntityTable` conserva el hook +de tabla y acepta `state`, `onRetry` y `columnsDisabled` opcionales. Los consumidores legacy, +incluidos PickerList y selectores remotos, mantienen sus contratos existentes. + +Las páginas deciden permisos, dependencias y operaciones; rdx-ui solo recibe textos y callbacks. +`useContextualDetail`, presupuestos de contenedor, navegación y `EntityNotFound` se conservan. +Un error transitorio no confirma que haya desaparecido una selección. Una respuesta correcta +que la retire sigue utilizando el contrato de desaparición existente. + +## 5. Carga inicial + +La búsqueda y ordenación de columnas conocidas funcionan durante la espera; sus valores se +mantienen cuando llegan los datos. Diez placeholders como máximo reservan una página plausible, +con las clases de las columnas reales. Son `aria-hidden`, sin nombres de entidades, selección, +links ni botones de acción. No se anuncian diez registros ni se muestra un total cero inventado. + +Paginación/tamaño de página permanecen visibles pero deshabilitados hasta disponer de datos. +El catálogo pendiente tiene un marcador decorativo y un mensaje contextual cuando hay filas. +Las pestañas y columnas solo utilizan definiciones/instantáneas realmente conocidas. La alta +espera a inventario y catálogo válidos; los datos de red pendientes no bloquean el inventario. + +## 6. Refetch y datos anteriores + +«Actualizando activos…»/«Updating assets…» sustituye el resumen en su banda habitual, sin spinner +modal, skeleton completo ni toast por cada intervalo. Las filas y herramientas no se desmontan. +Se conservan búsqueda, plantilla, orden, página válida, preferencias, selección, scroll y foco +durante la petición. El refetch normal del catálogo/red no añade bandas que muevan el listado. + +## 7. Error y recuperación + +El error inicial y el posterior aparecen en la banda de estado, fuera del ancho desplazable de +las columnas. Así, el mensaje y Reintentar no requieren scroll horizontal en móvil. La tabla +conserva cabeceras y espacio corporal, sin simular registros. + +Un 503 posterior comunica que se muestran datos anteriores; permite búsqueda y consulta. +Alta/edición y operaciones sensibles se restringen cuando su consulta necesaria no es válida; +el borrador ya abierto se conserva ante un error transitorio. Los reintentos de catálogo/red +no invalidan indiscriminadamente otras consultas. + +Una denegación `InventoryApiError` con `FORBIDDEN` oculta los datos en caché del inventario y +muestra falta de permiso sin prometer recuperación mediante Reintentar. Se conserva la +navegación a otros destinos. No se altera la autorización del servidor. + +## 8. Estados vacíos + +La respuesta válida sin activos conserva «Esta compañía todavía no tiene activos.» y su alta +de cabecera, cuando las referencias están listas. Cero coincidencias conserva la búsqueda y +«Limpiar filtros». Un error no utiliza ninguno de esos mensajes. `—` sigue reservado para +valores ausentes/controles sin valor conocido; no sustituye los estados de consulta. + +## 9. Componentes compartidos + +- `lib/entity-list-state.ts`: unión y derivación pura, incluida compatibilidad legacy. +- `EntityListStatus`: estado, mensaje y recuperación con Alert/Button existentes; destino de + foco opcional `retryFocus`, sin DTO, query keys ni permisos. +- `EntityTableSkeleton`: placeholders decorativos con geometría de columnas conocidas. +- `EntityTable`: instancia estable y estado explícito; `EntityTableConfiguration` permite + deshabilitar la configuración dependiente; `EntityListPagination` distingue total desconocido. + +Adopción nueva limitada a los inventarios de Activos/Recursos con layout `content`. No se +convierte ningún selector remoto en tabla local ni se generalizan estados de consultas ajenas. + +## 10. Accesibilidad + +La carga inicial tiene un único `role=status`; los skeletons no participan en el foco. +La tabla señala `aria-busy` durante carga/refetch normal. El refetch periódico no genera +anuncios live repetidos. Los errores utilizan el Alert existente y texto explícito, además +del color. Los controles pendientes tienen explicación contextual en es/en. + +Reintentar funciona con Enter y enfoca la búsqueda persistente antes de iniciar la recuperación; +así su desaparición no deja el foco en `body`. El destino se aporta desde el consumidor. +Las animaciones nuevas respetan `prefers-reduced-motion`. Estas comprobaciones no acreditan +cumplimiento WCAG completo ni sustituyen pruebas con lector de pantalla. + +## 11. Resultados responsive + +La matriz de carga inicial comprende 1920×1080, 1440×900, 1366×768, 1024×768, 768×1024, +390×844 y 320×640. Se comprueba estructura, búsqueda habilitada, paginación pendiente, +placeholders sin controles y ausencia de overflow horizontal global. La tabla conserva +su scroll horizontal local. Paneles siguen los mínimos V01.3 (1144 px) y V01.4 (984 px). + +La línea base retenida antes del cambio tenía **cero tablas y cero controles de búsqueda y +configuración**, tanto a 1440×900 como a 320×640. La corrección conserva una tabla y sus +herramientas. Las medidas completas, incluidas posiciones, se guardan junto a las capturas. + +En la prueba de refetch a 1920×1080 se usan página 2, orden descendente, selección contextual, +columnas opcionales y scroll real: horizontal 120 px y vertical 150 px. Durante la espera se +conservan ambos scrolls, foco e identidad DOM; la cabecera de tabla permanece en y=230 px. +El aviso de fallo posterior ocupa espacio adicional (26 px en ese escenario), sin desmontar +filas ni reiniciar scroll/selección. No se promete layout idéntico ante mensajes de error. + +## 12. Pruebas + +| Comprobación ejecutada | Resultado real | +| --- | --- | +| `pnpm --filter @repo/rdx-ui typecheck`, `@metis/assets typecheck`, `@metis/resources typecheck` | Correctas las tres comprobaciones dirigidas. | +| `pnpm --filter @repo/rdx-ui test` | 11/11 ficheros; el fichero de listado ejecutado directamente obtiene 27/27 casos. | +| `pnpm --filter @metis/assets test` / `@metis/resources test` | 27/27 y 11/11 ficheros. | +| `pnpm exec biome check ` / `pnpm exec eslint ` | Correctos, sin diagnósticos nuevos. | +| `pnpm typecheck` | Falla en `packages/shadcn-ui/src/components/calendar.tsx:165`, TS2375 de `locale` con `exactOptionalPropertyTypes`; mismo fallo vendor previo. La interrupción global no sustituye los typechecks dirigidos. | +| `pnpm lint` | Falla: 15 errores, un warning y cuatro infos preexistentes. | +| `pnpm check` | Falla: 168 errores, un warning y cuatro infos preexistentes. | +| `pnpm test` | Falla: 7/8 tareas correctas; dos expectativas SSR antiguas de Compañías (`Personas (1)` y el mensaje de acceso de persona). Ejecutada contra otra MariaDB temporal `metis_desk_test`, sin utilizar la BD del navegador. | +| `pnpm build` | Correcto, 2/2 tareas; persiste el aviso de chunk mayor de 500 kB. | +| MCP, L02.3 | 7/7 métodos principales + 1/1 adicional de plantilla/contraste/reintento móvil, **ocho escenarios automatizados distintos correctos**. | +| MCP, regresiones seleccionadas | 9/9: navegación móvil/teclado/rutas, capacidad del shell y Activos, contraste, asistentes, foco, capacidad de Recursos y edición anidada. | +| MCP, L02.2 | 8/8 métodos correctos. | +| MCP, L02.1 | 8/8 métodos correctos tras actualizar las expectativas del configurador L02.2. | + +Se ejecutan **33 métodos MCP distintos**; las repeticiones de diagnóstico no incrementan ese +total. Las pruebas unitarias nuevas cubren ocho combinaciones de consulta, +compatibilidad legacy, estructura inicial, ausencia de registros ficticios, datos anteriores +y distinción entre error, vacío válido y filtros en es/en. + +`scripts/list-states-ui.test.py` intercepta exclusivamente GET del entorno sintético: retención +y liberación inicial/refetch, 503 inicial/posterior, reintento real, catálogo/red independientes, +403, respuesta vacía válida, filtros, edición conservada, movimiento reducido y traducción. +No cambia opciones internas de QueryClient para simular los estados; la reconexión controlada +dispara el mecanismo normal del consumidor. + +Ejecutar con la sesión privada activa del preparador, sin escribir su contenido ni guardarla +en Git. El procedimiento de [Playwright MCP](playwright-mcp.md) explica cómo obtenerla: + +```bash +METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \ + python3 scripts/list-states-ui.test.py +``` + +La suite contiene ya los ocho métodos. La ejecución documentada separó el octavo, incorporado +al final, de los siete primeros. Para las regresiones se seleccionaron los nueve métodos +indicados, no los 28 del fichero entero. La consola final de estas regresiones y la de columnas +no registra errores/warnings. En las simulaciones L02.3 aparecen los 503 deliberados del GET +interceptado; no se clasifican como fallos espontáneos. + +## 13. Evidencias + +Directorio local ignorado: `.artifacts/playwright/l02-3/`. Contiene baseline `before/`, capturas, +snapshots, geometría, foco, consola y escrituras bloqueadas en `after/`, y logs de validación. +Los errores HTTP 503/403 se provocan deliberadamente y no representan incidencias espontáneas. +El índice distingue ejecuciones intermedias y finales. Las imágenes no se versionan ni se +incluyen sesiones privadas; su entrega fuera de esta instalación requiere compartir esos +PNG/mediciones de forma explícita. + +Índice local: `.artifacts/playwright/l02-3/indice-evidencias.md`. Capturas representativas +abiertas: baseline retenido 1440, carga nueva 1440, error inicial 320, error posterior 1920, +catálogo fallido 1440 y reintento visible 320. El contraste real del texto obtiene 4,83:1 en +carga y 10,01:1 en error sobre sus fondos computados; son dos muestras concretas, no una +auditoría integral de contraste. + +Se reutiliza `scripts/visual-audit.py`, login real y usuario ordinario: una única instalación +propia desechable, exclusivamente `metis_desk_test`, con **15 compañías**, y una principal con +**50 personas, 51 activos y 66 recursos** mediante el preparador de volumen existente. +La sesión anterior encontrada devolvía 401 y no se reutilizó ni se eliminaron servicios ajenos. +Al terminar se cierra el preparador propio: salida correcta, servicios temporales retirados y +comprobación explícita de que su `session.json` ya no existe. Las capturas se conservan sin sesión. + +## 14. Regresiones + +Las suites de columnas L02.2, selectores L02.1 y los casos seleccionados de +`visual-audit-ui.test.py` comprueban navegación móvil/portátil, presupuestos de inventario, +detalle de Recursos, contraste de selección, asistentes y retorno de foco. Se preservan +visibilidad persistente, IDs de campos y acciones ancladas. Los tests anteriores deben esperar +datos disponibles explícitamente: la existencia de la tabla ya no implica que acabó el GET. + +Las primeras ejecuciones detectaron supuestos obsoletos en pruebas: un único botón Editar, +etiquetas DOM sin la marca Obligatoria, `selection` configurable y todos los destinos visibles +sin scroll con quince compañías. Se actualizan las expectativas al contrato L02.2 y al volumen +real, usando nombre accesible para la identidad protegida y comprobando que el foco hace visible +Inicio en el menú desplazable. No se cambia el shell para hacer pasar esas expectativas. + +Los instrumentos nuevos también se corrigieron antes de la ejecución final: esperar promesas +de Locator, limitar Reintentar a su listado (hay otros observadores de la misma consulta) y +esperar las compuertas en el contexto Node del MCP. Se conservan sus logs fallidos intermedios. + +## 15. Limitaciones + +No se acredita rendimiento de producción, otros motores, lectores de pantalla ni acceso a +datos reales. La sesión y el navegador son efímeros. Las interceptaciones no prueban una caída +real del backend. Compañía/lookups conserva su carga exterior; otros listados legacy no adoptan +automáticamente el nuevo estado. Siguen vigentes los fallos globales preexistentes de calendario +vendor, lint/check y dos expectativas SSR de Compañías; no se corrigen fuera de alcance. + +Skills leídas: `ux-tables` principal; `ux-empty-states`, `ux-error-recovery`, `ux-microcopy`, +`shadcn`, `vercel-composition-patterns` y `vercel-react-best-practices` complementan estados, +mensajes, primitivas reales, composición y derivación sin efectos. Prevalecen DESIGN y contratos +de Metis. Los auditores `*-audit` y `docs/orchestration-policy.md` referidos por algunas skills +no están instalados/disponibles; no se instalan ni se afirma haberlos ejecutado. + +## 16. Pendientes de L03 y L04 + +V05 (restauración entre rutas) y V07 (normalización general de búsqueda) permanecen pendientes. +V08 sigue resuelto en los escenarios L02.2, sin reordenación ni densidades nuevas. +L03 conserva su alcance futuro; L04 podrá valorar adopciones de estado en otros consumidores, +sin refactor masivo de consultas ni paginación/virtualización nuevas en esta entrega. + +**V12 queda resuelto en los escenarios descritos y ejecutados.** No se cambia el estado de +otros hallazgos ni se declara que todos los listados del monorepo adopten este contrato. diff --git a/docs/patron-de-listados-de-entidades.md b/docs/patron-de-listados-de-entidades.md index 00cc313..8d4213b 100644 --- a/docs/patron-de-listados-de-entidades.md +++ b/docs/patron-de-listados-de-entidades.md @@ -99,6 +99,20 @@ Usar tabla semántica con nombre accesible, th con scope, controles etiquetados, Carga inicial y errores se presentan dentro del contenido sin afirmar que no hay registros. El vacío distingue ausencia de datos y búsqueda sin resultados. Mantener el mensaje y la recuperación de errores de cada consulta; no inventar resultados ni endpoints. Las recargas conservan datos y controles cuando la integración existente lo permite. +[L02.3](estados-listados-l02-3.md) define el estado explícito de Activos y Recursos: +`loading`, `ready`, `refreshing`, `unavailable` y `stale`. Mantener la instancia de tabla, +búsqueda, cabeceras conocidas y espacio de paginación desde el GET inicial. Skeletons +decorativos sin acciones, nombres ni totales inventados; controles dependientes de datos +pendientes deshabilitados con explicación. Separar errores del inventario, catálogo y +referencias accesorias. Una dependencia de red pendiente no bloquea todas las filas. + +En refetch conservar la caché y el contexto sin copiar DTO a estado React. El fallo posterior +debe indicar «últimos datos disponibles» y ofrecer reintento dirigido. Situar error y +Reintentar fuera del ancho desplazable de las columnas; no esconderlos en una celda ancha. +Al reintentar, enfocar la búsqueda persistente para evitar que la desaparición del botón +deje el foco en el documento. Las denegaciones explícitas siguen el contrato de permisos +del módulo y no se representan como vacío ni como una copia consultable sin autorización. + ## Inventario de listados y ubicaciones actuales La aplicación navega por espacios y secciones mediante rutas URL independientes, según [Rutas del cliente](rutas-del-cliente.md). Los componentes siguientes se componen desde CompaniesWorkspace, CompanyDetail o IdentityWorkspace; altas, ediciones e historiales conservan sus modales. diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index f79c86e..f2323e8 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -665,5 +665,28 @@ "SMB_RESOURCE_INACTIVE": "The share is inactive.", "SMB_RESOURCE_NOT_FOUND": "This share is no longer available." } + }, + "listState": { + "forbidden": "You do not have permission to view this company information.", + "retryCatalog": "Retry catalog", + "retryInfrastructure": "Retry infrastructure", + "inventory": { + "loading": "Loading assets…", + "unavailable": "Assets could not be loaded. You can try again.", + "refreshing": "Updating assets…", + "stale": "Assets could not be updated. Showing the last available data." + }, + "catalog": { + "loading": "Loading assets templates. Column configuration and creation will be available when loading finishes.", + "unavailable": "The catalog could not be loaded. Snapshot data is shown; column configuration and creation are unavailable.", + "refreshing": "Updating catalog…", + "stale": "The catalog could not be updated. Showing the last available definitions." + }, + "infrastructure": { + "loading": "Loading network data; the asset inventory is already available.", + "unavailable": "Network data could not be loaded. The asset inventory remains available.", + "refreshing": "Updating network data…", + "stale": "Network data could not be updated. Showing the last available network data." + } } } diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index d81f2a3..d2a946d 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -665,5 +665,28 @@ "SMB_RESOURCE_INACTIVE": "La carpeta está inactiva.", "SMB_RESOURCE_NOT_FOUND": "La carpeta ya no está disponible." } + }, + "listState": { + "forbidden": "No tienes permiso para consultar esta información de la compañía.", + "retryCatalog": "Reintentar catálogo", + "retryInfrastructure": "Reintentar infraestructura", + "inventory": { + "loading": "Cargando activos…", + "unavailable": "No se pudieron cargar los activos. Puedes volver a intentarlo.", + "refreshing": "Actualizando activos…", + "stale": "No se pudieron actualizar los activos. Se muestran los últimos datos disponibles." + }, + "catalog": { + "loading": "Cargando las plantillas de activos. La configuración de columnas y el alta estarán disponibles al terminar.", + "unavailable": "No se pudo cargar el catálogo. Se muestran los datos de las instantáneas; la configuración de columnas y el alta no están disponibles.", + "refreshing": "Actualizando catálogo…", + "stale": "No se pudo actualizar el catálogo. Se conservan sus últimas definiciones disponibles." + }, + "infrastructure": { + "loading": "Cargando los datos de red; el inventario de activos ya está disponible.", + "unavailable": "No se pudieron cargar los datos de red. El inventario de activos sigue disponible.", + "refreshing": "Actualizando datos de red…", + "stale": "No se pudieron actualizar los datos de red. Se muestran los últimos datos de red disponibles." + } } } diff --git a/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts b/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts index ce6cbc7..dd6b7fa 100644 --- a/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts +++ b/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts @@ -11,6 +11,7 @@ import { import { inventoryKeys } from "../../../shared/queries"; import { removeAsset } from "../../shared/api"; import { getAssetInventory } from "../api"; +import { assetListQueryDenied } from "../helpers/asset-list-query-state"; import { INVENTORY_DETAIL_BUDGET } from "../helpers/inventory-detail-layout"; import type { AssetInventoryPageProps } from "../types"; @@ -99,14 +100,15 @@ export function useAssetInventoryPageController({ setSelectedId(id); }; - const assets = query.data?.assets ?? []; + const denied = assetListQueryDenied(query.error); + const assets = denied ? [] : (query.data?.assets ?? []); const selected = selectedId ? assets.find((asset) => asset.id === selectedId) : undefined; const edited = assets.find((asset) => asset.id === editor); const deleted = assets.find((asset) => asset.id === deleteId); return { - loading: query.isPending || catalog.isPending || infrastructure.isPending, - hasLoadError: query.isError || catalog.isError || infrastructure.isError, + assets, + denied, query, infrastructure, entityHref, diff --git a/modules/assets/src/web/assets/list/helpers/asset-list-query-state.ts b/modules/assets/src/web/assets/list/helpers/asset-list-query-state.ts new file mode 100644 index 0000000..d3fd16e --- /dev/null +++ b/modules/assets/src/web/assets/list/helpers/asset-list-query-state.ts @@ -0,0 +1,33 @@ +import type { EntityListState } from "@repo/rdx-ui/lib/entity-list-state"; +import { entityListState } from "@repo/rdx-ui/lib/entity-list-state"; +import type { UseQueryResult } from "@tanstack/react-query"; +import type { TFunction } from "i18next"; + +import { InventoryApiError } from "../../../shared/api/inventory-api-error"; + +/** Una denegación explícita no permite seguir mostrando la copia en caché. */ +export function assetListQueryDenied(error: Error | null): boolean { + return error instanceof InventoryApiError && error.code === "FORBIDDEN"; +} + +export function assetListQueryState( + query: Pick, "data" | "isFetching" | "isError" | "error">, + kind: "inventory" | "catalog" | "infrastructure", + t: TFunction<"assets"> +): EntityListState { + const denied = assetListQueryDenied(query.error); + + return entityListState( + { + available: query.data !== undefined && !denied, + fetching: query.isFetching, + failed: query.isError, + }, + { + loading: t(`listState.${kind}.loading`), + unavailable: denied ? t("listState.forbidden") : t(`listState.${kind}.unavailable`), + refreshing: t(`listState.${kind}.refreshing`), + stale: t(`listState.${kind}.stale`), + } + ); +} diff --git a/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx b/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx index d307b75..6a895b6 100644 --- a/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx +++ b/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx @@ -12,11 +12,13 @@ export function AssetWorkstationAction({ workstations, template, onCreate, + disabled = false, }: { asset: AssetDTO; workstations: readonly WorkstationDTO[]; template: CatalogItemDTO | undefined; onCreate: (assetId: string) => void; + disabled?: boolean; }) { const { t } = useTranslation("assets"); const action = assetWorkstationAction(asset, workstations, template); @@ -41,6 +43,7 @@ export function AssetWorkstationAction({ return action.kind === "create" ? ( )} - )} - {query.data && ( - ( - item.id === asset.values.templateId - )} - workstations={query.data?.workstations ?? []} - /> - ), - }, - { - id: "actions", - label: t("actions"), - hideable: false, - render: (asset) => ( - + ), + }, + ]} + infrastructure={infrastructure.data?.items ?? []} + onClearFilters={clearFilters} + onRetry={denied ? undefined : () => query.refetch()} + onRowClick={(asset) => selectAsset(asset.id)} + onSearchChange={setSearch} + onTemplateChange={setTemplateId} + renderName={(asset) => ( + + {asset.values.name} + + )} + rowClassName={(asset) => selectedRowClass(asset.id, selected?.id, detailOpen)} + search={search} + sites={lookups.sites} + state={inventoryState} + templateId={templateId} + /> detail.setVisible(false)} @@ -262,7 +268,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { open={detailOpen && detail.mode === "inline"} template={catalog.data?.items.find((item) => item.id === selected?.values.templateId)} /> - {editor && catalog.data && ( + {editor && !denied && catalog.data && ( Promise) | undefined; }) { const { t, i18n } = useTranslation("assets"); const templates = new Map( @@ -98,6 +106,9 @@ export function AssetList({ locale={i18n.language} value={templateId ?? "all"} /> + {catalogState?.status === "loading" && ( +