diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index c44ed9cb..8376dea2 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -1444,6 +1444,13 @@ listado de entidades y la configuración accesible de columnas. El consumidor ap ordenación, búsqueda, filtros, estado, traducciones y callbacks de página; el componente no conoce HTTP, DTO ni entidades de Metis. +La prop opcional `columnVisibilityStorageKey` recupera y guarda la visibilidad en +localStorage con el mismo contrato que EntityTable. El consumidor define la clave y su +ámbito; Nombre (si declara `hideable: false`) y Acciones permanecen visibles, las nuevas +columnas respetan `defaultVisible` y restaurar elimina las preferencias de la tabla actual. +El listado sigue funcionando con almacenamiento bloqueado o datos locales inválidos. +Recursos de compañía proporciona una clave independiente por compañía y pestaña. + La ordenación y la paginación son manuales en TanStack Table: no filtra ni reordena únicamente las filas de una página remota. `index` y `size` describen la página visible, `hasNext` procede del cursor devuelto por el servidor. La pantalla conserva los cursores anteriores y los diff --git a/docs/patron-de-listados-de-entidades.md b/docs/patron-de-listados-de-entidades.md index 969fa1c1..5081923e 100644 --- a/docs/patron-de-listados-de-entidades.md +++ b/docs/patron-de-listados-de-entidades.md @@ -308,12 +308,17 @@ actuales de los recursos, identificados por UUID, además de notas, fechas y aut alta/última modificación y los campos de integración de buzones, accesos y dominios. Las columnas adicionales comienzan ocultas y se agrupan en Datos generales, Integración, Registro y cambios y Características. La búsqueda admite también nombres de plantillas, -secciones, descripciones y unidades. Filtrar por plantilla o por texto no reduce las -columnas disponibles ni reinicia su selección. Las celdas sin valor muestran «—»; +secciones, descripciones y unidades. Filtrar por texto no reduce las columnas disponibles. Cada pestaña, incluida Todo, +guarda sus propias columnas y ordenación en localStorage, separadas por compañía y por +UUID de plantilla. Cambiar de pestaña recupera su configuración y reinicia la página; +recargar o volver a entrar conserva las preferencias. Una ordenación explícita en la URL +prevalece sobre la guardada. Un almacenamiento bloqueado o inválido permite seguir usando +el listado sin persistencia. Las celdas sin valor muestran «—»; cero, booleanos, fechas y unidades conservan su representación semántica. La pestaña Aplicaciones mantiene su listado remoto especializado de resúmenes globales y -locales; su contrato no incluye características ni revisiones completas. El historial +locales; comparte la persistencia por compañía y pestaña de columnas y ordenación, +respetando la ordenación remota. Su contrato no incluye características ni revisiones completas. El historial completo y las relaciones de cada recurso siguen en su ficha. ### Adopción proporcional en catálogos y configuraciones — L04.2 diff --git a/modules/resources/package.json b/modules/resources/package.json index d1dd4cf2..ffcc0c8f 100644 --- a/modules/resources/package.json +++ b/modules/resources/package.json @@ -11,7 +11,7 @@ }, "scripts": { "typecheck": "tsc -p tsconfig.json", - "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/application/application-coincidence.test.ts src/api/domain/resource-catalog.test.ts src/api/domain/resource.test.ts src/api/domain/resource-template-propagation.test.ts src/api/application/resource-lifecycle.test.ts src/web/catalog/shared/catalog.test.tsx src/web/resources/shared/resources.test.tsx src/web/resources/list/helpers/resource-columns.test.tsx src/web/resources/shared/resource-values.test.ts src/api/domain/template-sections.test.ts src/web/catalog-fields/select/catalog-field-selection.test.tsx src/api/domain/directory-domain.test.ts src/api/domain/network-share.test.ts src/api/domain/resource-scope.test.ts src/api/infrastructure/persistence/resource-scope-reader.test.ts src/api/application/resource-scope.test.ts src/api/application/resource-reference-contracts.test.ts src/api/domain/global-resource.test.ts src/web/global-applications/shared/global-applications.test.tsx src/api/domain/operating-system-identity.test.ts src/api/application/global-operating-system-contracts.test.ts src/api/application/global-operating-system-policy.test.ts src/web/resources/shared/ui/resource-characteristic-control.test.tsx" + "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/application/application-coincidence.test.ts src/api/domain/resource-catalog.test.ts src/api/domain/resource.test.ts src/api/domain/resource-template-propagation.test.ts src/api/application/resource-lifecycle.test.ts src/web/catalog/shared/catalog.test.tsx src/web/resources/shared/resources.test.tsx src/web/resources/list/helpers/resource-columns.test.tsx src/web/resources/list/helpers/resource-list-preferences.test.tsx src/web/resources/shared/resource-values.test.ts src/api/domain/template-sections.test.ts src/web/catalog-fields/select/catalog-field-selection.test.tsx src/api/domain/directory-domain.test.ts src/api/domain/network-share.test.ts src/api/domain/resource-scope.test.ts src/api/infrastructure/persistence/resource-scope-reader.test.ts src/api/application/resource-scope.test.ts src/api/application/resource-reference-contracts.test.ts src/api/domain/global-resource.test.ts src/web/global-applications/shared/global-applications.test.tsx src/api/domain/operating-system-identity.test.ts src/api/application/global-operating-system-contracts.test.ts src/api/application/global-operating-system-policy.test.ts src/web/resources/shared/ui/resource-characteristic-control.test.tsx" }, "dependencies": { "@metis/core": "workspace:*", diff --git a/modules/resources/src/web/resources/list/controllers/use-company-applications-controller.ts b/modules/resources/src/web/resources/list/controllers/use-company-applications-controller.ts index ff497735..9056ade1 100644 --- a/modules/resources/src/web/resources/list/controllers/use-company-applications-controller.ts +++ b/modules/resources/src/web/resources/list/controllers/use-company-applications-controller.ts @@ -7,9 +7,22 @@ import { applicationReferencesApi, } from "../../../application-references/shared/api/application-references-api"; import { applicationReferenceKeys } from "../../../application-references/shared/queries/application-reference-keys"; +import { + readResourceListSorting, + resourceListStorageKey, + writeResourceListSorting, +} from "../helpers/resource-list-preferences"; export function useCompanyApplicationsController(companyId: string) { const { searchParams } = useBrowserSearchParams(); + const storageKey = resourceListStorageKey(companyId, searchParams.get("template")); + const storedSorting = ListGlobalApplicationsRequestSchema.pick({ + sort: true, + direction: true, + }).safeParse(readResourceListSorting(storageKey)); + const preferredSorting = storedSorting.success ? storedSorting.data : undefined; + const hasUrlSorting = searchParams.has("sort") || searchParams.has("direction"); + const sorting = hasUrlSorting ? undefined : preferredSorting; const search = searchParams.get("q") ?? ""; const state = searchParams.get("active") ?? "all"; const pageIndex = Math.max(0, Number(searchParams.get("page") ?? "1") - 1); @@ -17,8 +30,8 @@ export function useCompanyApplicationsController(companyId: string) { const parsed = ListGlobalApplicationsRequestSchema.safeParse({ search, ...(state === "all" ? {} : { active: state }), - sort: searchParams.get("sort") ?? "name", - direction: searchParams.get("direction") ?? "asc", + sort: searchParams.get("sort") ?? sorting?.sort, + direction: searchParams.get("direction") ?? sorting?.direction, pageSize: searchParams.get("pageSize") ?? "25", ...(searchParams.get("cursor") ? { cursor: searchParams.get("cursor") } : {}), }); @@ -60,6 +73,7 @@ export function useCompanyApplicationsController(companyId: string) { }; return { + columnVisibilityStorageKey: `${storageKey}:columns`, query, denied: query.error instanceof ApplicationReferenceApiError && @@ -71,6 +85,10 @@ export function useCompanyApplicationsController(companyId: string) { state, pageIndex, update, + changeSorting: (sorting: { sort: string; direction: "asc" | "desc" }) => { + writeResourceListSorting(storageKey, sorting); + update(sorting); + }, nextPage: () => { if (query.data?.nextCursor && !query.isPlaceholderData) { setPrevious([...previous, queryData?.cursor]); diff --git a/modules/resources/src/web/resources/list/controllers/use-resource-list-navigation.ts b/modules/resources/src/web/resources/list/controllers/use-resource-list-navigation.ts new file mode 100644 index 00000000..35efc811 --- /dev/null +++ b/modules/resources/src/web/resources/list/controllers/use-resource-list-navigation.ts @@ -0,0 +1,34 @@ +import { useListLocation } from "@metis/core/client"; + +import { + readResourceListSorting, + resourceListStorageKey, + writeResourceListSorting, +} from "../helpers/resource-list-preferences"; + +/** La URL explícita prevalece; sin ella se recupera el orden preferido de esta compañía y pestaña. */ +export function useResourceListNavigation( + companyId: string, + templateId: string | null, + ready: boolean +) { + const location = useListLocation({ sort: "sort", direction: "direction", page: "page" }, ready); + const storageKey = resourceListStorageKey(companyId, templateId); + const storedSorting = readResourceListSorting(storageKey); + const hasUrlSorting = location.values.sort !== null || location.values.direction !== null; + + return { + ...location, + values: + !hasUrlSorting && storedSorting + ? { ...location.values, sort: storedSorting.sort, direction: storedSorting.direction } + : location.values, + onChange: (value: Parameters[0]) => { + writeResourceListSorting(storageKey, { + sort: value.sort ?? "name", + direction: value.direction ?? "asc", + }); + location.onChange(value); + }, + }; +} diff --git a/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts b/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts index 947d7dcf..34ced6e4 100644 --- a/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts +++ b/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts @@ -15,6 +15,7 @@ import { catalogApi, catalogKeys } from "../../../catalog/shared/api"; import { inventoryApi, inventoryKeys } from "../../shared/api"; import { resourceRelatedQueryKeys } from "../../shared/api/resource-related-query-keys"; import { RESOURCE_DETAIL_BUDGET } from "../helpers/resource-detail-budget"; +import { applyResourceListTab } from "../helpers/resource-list-preferences"; import { resourceListQueryDenied } from "../helpers/resource-list-query-state"; import type { ResourcesPageProps } from "../types"; @@ -151,24 +152,13 @@ export function useResourcesPageController({ const setTemplateId = (value: string) => setSearchParams((params) => { - params.delete("page"); - params.delete("cursor"); - params.delete("active"); - params.delete("sort"); - params.delete("direction"); - params.delete("pageSize"); - if (value === "all") { - params.delete("template"); - } else { - params.set("template", value); - } + applyResourceListTab(params, value === "all" ? null : value); }); const clearFilters = () => setSearchParams((params) => { - params.delete("page"); params.delete("q"); - params.delete("template"); + applyResourceListTab(params, null); }); const denied = resourceListQueryDenied(query.error); @@ -188,6 +178,7 @@ export function useResourcesPageController({ }); return { + companyId, returnReady: !(query.isPending || query.isFetching || catalog.isPending || catalog.isFetching), canChangeStatus: query.isSuccess && !statusMutation.isPending, deleteId, diff --git a/modules/resources/src/web/resources/list/helpers/resource-list-preferences.test.tsx b/modules/resources/src/web/resources/list/helpers/resource-list-preferences.test.tsx new file mode 100644 index 00000000..2bcabbed --- /dev/null +++ b/modules/resources/src/web/resources/list/helpers/resource-list-preferences.test.tsx @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { renderToStaticMarkup } from "react-dom/server"; + +import { useResourceListNavigation } from "../controllers/use-resource-list-navigation"; + +import { + applyResourceListTab, + readResourceListSorting, + resourceListStorageKey, + writeResourceListSorting, +} from "./resource-list-preferences"; + +function withStorage(run: (storage: Map) => void, search = "") { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "window"); + const storage = new Map(); + + Object.defineProperty(globalThis, "window", { + configurable: true, + value: { + location: { pathname: "/companies/one/resources", search }, + localStorage: { + getItem: (key: string) => storage.get(key) ?? null, + setItem: (key: string, value: string) => storage.set(key, value), + }, + }, + }); + try { + run(storage); + } finally { + if (descriptor) { + Object.defineProperty(globalThis, "window", descriptor); + } else { + Reflect.deleteProperty(globalThis, "window"); + } + } +} + +function NavigationProbe({ templateId }: { templateId: string | null }) { + const navigation = useResourceListNavigation("one", templateId, true); + + return {JSON.stringify(navigation.values)}; +} + +test("orden de recursos: aislamiento por compañía y pestaña, incluida Todo", () => { + withStorage(() => { + const all = resourceListStorageKey("one", null); + const folders = resourceListStorageKey("one", "folders"); + const other = resourceListStorageKey("two", "folders"); + + writeResourceListSorting(all, { sort: "name", direction: "asc" }); + writeResourceListSorting(folders, { sort: "state", direction: "desc" }); + assert.deepEqual(readResourceListSorting(all), { sort: "name", direction: "asc" }); + assert.deepEqual(readResourceListSorting(folders), { sort: "state", direction: "desc" }); + assert.equal(readResourceListSorting(other), undefined); + const markup = renderToStaticMarkup(); + + assert.ok(markup.includes("state")); + assert.ok(markup.includes("desc")); + }); +}); + +test("orden de recursos: una URL explícita prevalece sobre la preferencia sin sobrescribirla", () => { + withStorage(() => { + const key = resourceListStorageKey("one", "folders"); + + writeResourceListSorting(key, { sort: "state", direction: "desc" }); + const markup = renderToStaticMarkup(); + + assert.ok(markup.includes("notes")); + assert.ok(markup.includes("asc")); + assert.deepEqual(readResourceListSorting(key), { sort: "state", direction: "desc" }); + }, "?sort=notes&direction=asc"); +}); + +test("preferencias opcionales: datos inválidos y almacenamiento bloqueado no impiden listar", () => { + withStorage((storage) => { + const key = resourceListStorageKey("one", null); + + for (const serialized of [ + "{", + "null", + "[]", + '{"sort":true,"direction":"asc"}', + '{"sort":"name","direction":"up"}', + ]) { + storage.set(`${key}:sorting`, serialized); + assert.equal(readResourceListSorting(key), undefined); + } + Object.defineProperty(window, "localStorage", { + get: () => { + throw new Error("Bloqueado"); + }, + }); + assert.equal(readResourceListSorting(key), undefined); + assert.doesNotThrow(() => writeResourceListSorting(key, { sort: "name", direction: "asc" })); + }); +}); + +test("cambiar pestaña limpia el orden y la paginación anteriores conservando la búsqueda", () => { + const params = new URLSearchParams( + "q=Administración&template=old&sort=state&direction=desc&page=3&cursor=old&active=true&pageSize=50" + ); + + applyResourceListTab(params, "folders"); + assert.equal(params.toString(), "q=Administraci%C3%B3n&template=folders"); + applyResourceListTab(params, null); + assert.equal(params.toString(), "q=Administraci%C3%B3n"); +}); diff --git a/modules/resources/src/web/resources/list/helpers/resource-list-preferences.ts b/modules/resources/src/web/resources/list/helpers/resource-list-preferences.ts new file mode 100644 index 00000000..a5db18c2 --- /dev/null +++ b/modules/resources/src/web/resources/list/helpers/resource-list-preferences.ts @@ -0,0 +1,51 @@ +import { z } from "zod/v4"; + +const ResourceListSortingSchema = z.strictObject({ + sort: z.string().min(1).max(100), + direction: z.enum(["asc", "desc"]), +}); + +export function resourceListStorageKey(companyId: string, templateId: string | null) { + return `resource-inventory:${companyId}:${templateId ?? "all"}`; +} + +export function readResourceListSorting(storageKey: string) { + if (typeof window === "undefined") { + return undefined; + } + try { + const serialized = window.localStorage.getItem(`${storageKey}:sorting`); + const parsed = ResourceListSortingSchema.safeParse(serialized ? JSON.parse(serialized) : null); + + return parsed.success ? parsed.data : undefined; + } catch { + // Las preferencias son opcionales si el navegador impide leerlas o contiene JSON inválido. + return undefined; + } +} + +export function writeResourceListSorting( + storageKey: string, + sorting: z.infer +) { + if (typeof window === "undefined") { + return; + } + try { + window.localStorage.setItem(`${storageKey}:sorting`, JSON.stringify(sorting)); + } catch { + // La ordenación sigue operativa en la URL aunque no pueda persistirse. + } +} + +/** Prepara el cambio de colección sin arrastrar orden explícito, cursores o página de otra pestaña. */ +export function applyResourceListTab(params: URLSearchParams, templateId: string | null) { + for (const key of ["page", "cursor", "active", "sort", "direction", "pageSize"]) { + params.delete(key); + } + if (templateId) { + params.set("template", templateId); + } else { + params.delete("template"); + } +} diff --git a/modules/resources/src/web/resources/list/ui/company-applications-list.tsx b/modules/resources/src/web/resources/list/ui/company-applications-list.tsx index c26533f9..9006d395 100644 --- a/modules/resources/src/web/resources/list/ui/company-applications-list.tsx +++ b/modules/resources/src/web/resources/list/ui/company-applications-list.tsx @@ -139,6 +139,7 @@ export function CompanyApplicationsList({

{t("companyApplications.description")}

query.refetch()} onSearchChange={(q) => controller.update({ q })} onSortingChange={(sorting) => - controller.update({ + controller.changeSorting({ sort: sorting[0]?.id ?? "name", direction: sorting[0]?.desc ? "desc" : "asc", }) diff --git a/modules/resources/src/web/resources/list/ui/resource-inventory-table.tsx b/modules/resources/src/web/resources/list/ui/resource-inventory-table.tsx index c59a9964..ebf5d151 100644 --- a/modules/resources/src/web/resources/list/ui/resource-inventory-table.tsx +++ b/modules/resources/src/web/resources/list/ui/resource-inventory-table.tsx @@ -15,6 +15,7 @@ import { ResourceTemplateIcon } from "../../../catalog-templates/shared/ui/resou import type { useResourcesPageController } from "../controllers"; import { resourceAdditionalColumns } from "../helpers/resource-additional-columns"; import { resourceCharacteristicColumns } from "../helpers/resource-characteristic-columns"; +import { resourceListStorageKey } from "../helpers/resource-list-preferences"; export function ResourceInventoryTable({ controller, @@ -172,6 +173,7 @@ export function ResourceInventoryTable({ }, ]} columnsDisabled={!catalog.data} + columnVisibilityStorageKey={`${resourceListStorageKey(controller.companyId, templateId)}:columns`} emptyMessage={t("empty")} filterKey={tabValue} filtersApplied={templateId !== null} diff --git a/modules/resources/src/web/resources/list/ui/resources-page.tsx b/modules/resources/src/web/resources/list/ui/resources-page.tsx index 2ce115fb..f18c95ed 100644 --- a/modules/resources/src/web/resources/list/ui/resources-page.tsx +++ b/modules/resources/src/web/resources/list/ui/resources-page.tsx @@ -1,4 +1,4 @@ -import { EntityNotFound, useListLocation, useListReturn } from "@metis/core/client"; +import { EntityNotFound, useListReturn } from "@metis/core/client"; import { EntityListHeader } from "@repo/rdx-ui/components/entity-list"; import { EntityListStatus } from "@repo/rdx-ui/components/entity-list-status"; import { ScrollableTabsList } from "@repo/rdx-ui/components/scrollable-tabs-list"; @@ -17,6 +17,7 @@ import { ResourceDetailDialog, ResourceSelectionPanel } from "../../detail"; import { DeleteResourceDialog } from "../../lifecycle/ui/delete-resource-dialog"; import { ResourceEditorDialog } from "../../shared"; import { useResourcesPageController } from "../controllers"; +import { useResourceListNavigation } from "../controllers/use-resource-list-navigation"; import { RESOURCE_DETAIL_BUDGET } from "../helpers/resource-detail-budget"; import { resourceListQueryState } from "../helpers/resource-list-query-state"; import type { ResourcesPageProps } from "../types"; @@ -61,10 +62,7 @@ export function ResourcesPage(props: ResourcesPageProps) { const inventoryState = resourceListQueryState(query, "inventory", t); const catalogState = resourceListQueryState(catalog, "catalog", t); const canMutate = query.isSuccess && catalog.isSuccess; - const navigation = useListLocation( - { sort: "sort", direction: "direction", page: "page" }, - canMutate - ); + const navigation = useResourceListNavigation(companyId, templateId, canMutate); useListReturn({ scope: `/companies/${companyId}/resources`, diff --git a/packages/rdx-ui/src/components/cursor-entity-table.tsx b/packages/rdx-ui/src/components/cursor-entity-table.tsx index 54052c02..6021345c 100644 --- a/packages/rdx-ui/src/components/cursor-entity-table.tsx +++ b/packages/rdx-ui/src/components/cursor-entity-table.tsx @@ -3,8 +3,12 @@ import { Input } from "@repo/shadcn-ui/components/input"; import type { SortingState } from "@tanstack/react-table"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import type { ReactNode } from "react"; -import { useId, useRef, useState } from "react"; +import { useEffect, useId, useRef, useState } from "react"; +import { + readColumnVisibility, + writeColumnVisibility, +} from "../lib/entity-column-visibility-storage"; import type { EntityListState } from "../lib/entity-list-state"; import type { EntityColumn } from "./entity-list"; @@ -17,6 +21,7 @@ import { SelectWithAction } from "./select-with-action"; export function CursorEntityTable({ items, columns, + columnVisibilityStorageKey, title, locale, search, @@ -35,6 +40,7 @@ export function CursorEntityTable({ }: { items: readonly T[]; columns: readonly EntityColumn[]; + columnVisibilityStorageKey?: string | undefined; title: string; locale: string; search: string; @@ -68,7 +74,18 @@ export function CursorEntityTable({ }) { const prefix = useId(); const searchRef = useRef(null); - const [visibility, setVisibility] = useState({}); + const [visibility, setVisibility] = useState(() => + readColumnVisibility(columnVisibilityStorageKey) + ); + const [previousStorageKey, setPreviousStorageKey] = useState(columnVisibilityStorageKey); + + if (previousStorageKey !== columnVisibilityStorageKey) { + setPreviousStorageKey(columnVisibilityStorageKey); + setVisibility(readColumnVisibility(columnVisibilityStorageKey)); + } + useEffect(() => { + writeColumnVisibility(columnVisibilityStorageKey, visibility); + }, [columnVisibilityStorageKey, visibility]); const table = useReactTable({ data: [...items], columns: columns.map((column) => ({ @@ -83,7 +100,14 @@ export function CursorEntityTable({ manualPagination: true, state: { sorting, - columnVisibility: visibility, + columnVisibility: Object.fromEntries( + columns.map((column) => [ + column.id, + column.hideable === false || column.id === "actions" + ? true + : (visibility[column.id] ?? column.defaultVisible ?? true), + ]) + ), pagination: { pageIndex: pagination.index, pageSize: pagination.size }, columnPinning: { right: columns.some((column) => column.id === "actions") ? ["actions"] : [], diff --git a/packages/rdx-ui/src/entity-column-visibility.test.tsx b/packages/rdx-ui/src/entity-column-visibility.test.tsx index 059f8625..b74fec68 100644 --- a/packages/rdx-ui/src/entity-column-visibility.test.tsx +++ b/packages/rdx-ui/src/entity-column-visibility.test.tsx @@ -3,6 +3,7 @@ import { test } from "node:test"; import { renderToStaticMarkup } from "react-dom/server"; +import { CursorEntityTable } from "./components/cursor-entity-table"; import { EntityTable } from "./components/entity-list"; import { groupColumnOptions, normalizeColumnSearch } from "./lib/entity-column-configuration"; import { @@ -265,3 +266,65 @@ test("los contadores distinguen visibilidad, total y coincidencias en es/en", () assert.equal(entityListLabels("es").matchingColumns(1), "1 coincidencia"); assert.equal(entityListLabels("en").matchingColumns(0), "0 matches"); }); + +test("listado remoto: recupera columnas por ámbito y protege nombre y acciones", () => { + withStorage(() => { + writeColumnVisibility("applications-one", { provider: false, name: false, actions: false }); + + const render = (key: string) => + renderToStaticMarkup( + "Aplicación", + render: () => "Aplicación", + }, + { id: "provider", label: "Proveedor", render: () => "Proveedor de prueba" }, + { id: "actions", label: "Acciones", render: () => "Editar" }, + ]} + columnVisibilityStorageKey={key} + emptyMessage="Sin aplicaciones" + filters={null} + filtersApplied={false} + items={[{ id: "one" }]} + labels={{ + range: "Una aplicación", + page: "Página 1", + previous: "Anterior", + next: "Siguiente", + pageSize: "Tamaño", + clear: "Limpiar", + retry: "Reintentar", + }} + locale="es" + onClearFilters={() => undefined} + onRetry={async () => undefined} + onSearchChange={() => undefined} + onSortingChange={() => undefined} + pagination={{ + index: 0, + size: 25, + hasNext: false, + onPrevious: () => undefined, + onNext: () => undefined, + onSizeChange: () => undefined, + }} + search="" + searchLabel="Buscar" + sorting={[{ id: "name", desc: false }]} + state={{ status: "ready" }} + title="Aplicaciones" + /> + ); + + const restored = render("applications-one"); + + assert.ok(restored.includes("Aplicación")); + assert.ok(restored.includes("Editar")); + assert.equal(restored.includes("Proveedor de prueba"), false); + assert.ok(render("applications-two").includes("Proveedor de prueba")); + }); +});