Añadir secciones de detalle y mejorar la presentación de activos y recursos
This commit is contained in:
parent
5a7960302e
commit
d7eb91ee68
@ -301,3 +301,11 @@ El desplazamiento vertical se conserva cuando el contenido supera la altura disp
|
|||||||
La pantalla `CompanyOverview` tiene composición independiente de `DashboardPage` (Inicio). Comparte tarjetas, bloques de trabajo/avisos/actividad y consultas existentes, siempre fijando el ámbito al UUID de la compañía. El encabezado «De un vistazo» precede las acciones rápidas (ticket, intervención, activo y recurso), cuatro indicadores operativos y una franja navegable de personas, departamentos y sedes. No muestra compañías activas ni el resumen por compañías. La variante compacta de `StatisticCard` mejora la jerarquía del valor y la etiqueta sin modificar la presentación de Inicio.
|
La pantalla `CompanyOverview` tiene composición independiente de `DashboardPage` (Inicio). Comparte tarjetas, bloques de trabajo/avisos/actividad y consultas existentes, siempre fijando el ámbito al UUID de la compañía. El encabezado «De un vistazo» precede las acciones rápidas (ticket, intervención, activo y recurso), cuatro indicadores operativos y una franja navegable de personas, departamentos y sedes. No muestra compañías activas ni el resumen por compañías. La variante compacta de `StatisticCard` mejora la jerarquía del valor y la etiqueta sin modificar la presentación de Inicio.
|
||||||
|
|
||||||
La navegación de compañía empieza por Resumen, Tickets e Intervenciones; después agrupa Personas, Puestos de trabajo y Perfiles de usuario y finalmente las demás secciones. Los grupos tienen separación visual. Las operaciones de usuario permanecen en el menú superior y dejan de repetirse al pie del menú de compañía.
|
La navegación de compañía empieza por Resumen, Tickets e Intervenciones; después agrupa Personas, Puestos de trabajo y Perfiles de usuario y finalmente las demás secciones. Los grupos tienen separación visual. Las operaciones de usuario permanecen en el menú superior y dejan de repetirse al pie del menú de compañía.
|
||||||
|
|
||||||
|
## Fichas de consulta y resumen lateral
|
||||||
|
|
||||||
|
Las secciones de consulta evitan recuadros anidados y rellenos laterales: usan títulos compactos y un separador tenue entre grupos. Las agrupaciones de características siguen este mismo criterio en lectura; los formularios conservan su organización propia.
|
||||||
|
|
||||||
|
`DetailSection` ofrece agrupación plegable nativa (`details/summary`), abierta inicialmente, accesible por teclado. `DetailField` devuelve una fila semántica `dt/dd` para incluir en un `dl`: etiqueta y valor se alinean horizontalmente cuando el contenedor dispone de al menos 240 px y se apilan en espacios menores. El consumidor aporta los enlaces; el componente común les aplica cursor de enlace, color semántico y subrayado al pasar el puntero. Los valores ausentes permanecen como texto.
|
||||||
|
|
||||||
|
La ficha lateral de activos utiliza un ancho adaptable entre 320 y 400 px, acciones debajo del título y bloques de identificación, ubicación/asignación, características, observaciones, relaciones y asistencia. Conserva las secciones capturadas de la plantilla, incluyendo la compatibilidad con especificaciones antiguas sin agrupación; no modifica valores ni instantáneas. Las relaciones llevan a destinos existentes: persona y activo relacionado a su ficha; sede, puesto y plantilla a sus listados/catálogos actuales. La vista completa e historial mantienen sus funciones. Las fichas de recursos reutilizan las filas de características. No se crean nuevas fichas laterales ni nuevas rutas.
|
||||||
|
|||||||
@ -303,5 +303,10 @@
|
|||||||
"formLayout": {
|
"formLayout": {
|
||||||
"identity": "Identification and assignment",
|
"identity": "Identification and assignment",
|
||||||
"template": "Template details"
|
"template": "Template details"
|
||||||
|
},
|
||||||
|
"detailSections": {
|
||||||
|
"identification": "Identification",
|
||||||
|
"assignment": "Location and assignment",
|
||||||
|
"notes": "Notes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -303,5 +303,10 @@
|
|||||||
"formLayout": {
|
"formLayout": {
|
||||||
"identity": "Identificación y asignación",
|
"identity": "Identificación y asignación",
|
||||||
"template": "Datos de la plantilla"
|
"template": "Datos de la plantilla"
|
||||||
|
},
|
||||||
|
"detailSections": {
|
||||||
|
"identification": "Identificación",
|
||||||
|
"assignment": "Ubicación y asignación",
|
||||||
|
"notes": "Observaciones"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type AssetRelationshipsProps = {
|
export type AssetRelationshipsProps = {
|
||||||
|
compact?: boolean;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
csrfToken: string;
|
csrfToken: string;
|
||||||
assetId: string;
|
assetId: string;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { BrowserLink } from "@metis/core/client";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ import { useAssetRelationshipsController } from "../controllers";
|
|||||||
import type { AssetRelationshipsProps } from "../types";
|
import type { AssetRelationshipsProps } from "../types";
|
||||||
|
|
||||||
export function AssetRelationships(props: AssetRelationshipsProps) {
|
export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||||
const { companyId, csrfToken, assetId } = props;
|
const { companyId, csrfToken, assetId, compact = false } = props;
|
||||||
const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } =
|
const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } =
|
||||||
useAssetRelationshipsController(props);
|
useAssetRelationshipsController(props);
|
||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
@ -20,7 +21,12 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
|||||||
<li className="space-y-2 rounded-md border p-3 text-sm" key={link.id}>
|
<li className="space-y-2 rounded-md border p-3 text-sm" key={link.id}>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{source ? link.values.name : link.values.inverseName} ·{" "}
|
{source ? link.values.name : link.values.inverseName} ·{" "}
|
||||||
{nameOf(source ? link.values.targetId : link.values.sourceId)}
|
<BrowserLink
|
||||||
|
className="cursor-pointer text-primary underline-offset-4 hover:underline"
|
||||||
|
href={`/companies/${companyId}/assets/${source ? link.values.targetId : link.values.sourceId}`}
|
||||||
|
>
|
||||||
|
{nameOf(source ? link.values.targetId : link.values.sourceId)}
|
||||||
|
</BrowserLink>
|
||||||
</p>
|
</p>
|
||||||
{link.values.notes && (
|
{link.values.notes && (
|
||||||
<p className="whitespace-pre-wrap break-words">{link.values.notes}</p>
|
<p className="whitespace-pre-wrap break-words">{link.values.notes}</p>
|
||||||
@ -49,7 +55,7 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
|||||||
return (
|
return (
|
||||||
<section aria-label={t("relations")} className="space-y-3">
|
<section aria-label={t("relations")} className="space-y-3">
|
||||||
<header className="flex flex-wrap items-center justify-between gap-2">
|
<header className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<h3 className="font-medium">{t("relations")}</h3>
|
{!compact && <h3 className="font-medium">{t("relations")}</h3>}
|
||||||
<Button onClick={() => setAdding(true)} size="sm" variant="outline">
|
<Button onClick={() => setAdding(true)} size="sm" variant="outline">
|
||||||
{t("linkAsset")}
|
{t("linkAsset")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
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 { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -22,49 +25,78 @@ export function AssetDetail(props: AssetDetailProps) {
|
|||||||
? t(value ? "yes" : "no")
|
? t(value ? "yes" : "no")
|
||||||
: String(value);
|
: String(value);
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-2">
|
||||||
{!compact && <p className="text-sm text-muted-foreground">{t("snapshotNotice")}</p>}
|
{!compact && <p className="text-sm text-muted-foreground">{t("snapshotNotice")}</p>}
|
||||||
<dl className={compact ? "grid gap-4" : "grid gap-3 sm:grid-cols-2"}>
|
<DetailSection title={t("detailSections.identification")}>
|
||||||
<div>
|
<dl>
|
||||||
<dt className="text-sm text-muted-foreground">{t("template")}</dt>
|
<DetailField label={t("template")}>
|
||||||
<dd>{asset.specification.templateName}</dd>
|
<BrowserLink href={`/companies/${asset.companyId}/catalogs/assets?tab=templates`}>
|
||||||
</div>
|
{asset.specification.templateName}
|
||||||
<div>
|
</BrowserLink>
|
||||||
<dt className="text-sm text-muted-foreground">{t("status")}</dt>
|
</DetailField>
|
||||||
<dd>{t(asset.values.status)}</dd>
|
<DetailField label={t("status")}>{t(asset.values.status)}</DetailField>
|
||||||
</div>
|
</dl>
|
||||||
<div>
|
</DetailSection>
|
||||||
<dt className="text-sm text-muted-foreground">{t("site")}</dt>
|
<DetailSection title={t("detailSections.assignment")}>
|
||||||
<dd>{nameOf(asset.values.siteId, lookups.sites)}</dd>
|
<dl>
|
||||||
</div>
|
<DetailField label={t("site")}>
|
||||||
<div>
|
{asset.values.siteId ? (
|
||||||
<dt className="text-sm text-muted-foreground">{t("person")}</dt>
|
<BrowserLink href={`/companies/${asset.companyId}/sites`}>
|
||||||
<dd>{nameOf(asset.values.personId, lookups.people)}</dd>
|
{nameOf(asset.values.siteId, lookups.sites)}
|
||||||
</div>
|
</BrowserLink>
|
||||||
<div>
|
) : (
|
||||||
<dt className="text-sm text-muted-foreground">{t("workstationInAsset")}</dt>
|
t("noAssignment")
|
||||||
<dd>{workstations?.map((item) => item.values.name).join(" · ") || t("noWorkstation")}</dd>
|
)}
|
||||||
</div>
|
</DetailField>
|
||||||
</dl>
|
<DetailField label={t("person")}>
|
||||||
<FieldSections
|
{asset.values.personId ? (
|
||||||
items={asset.specification.fields}
|
<BrowserLink href={`/companies/${asset.companyId}/people/${asset.values.personId}`}>
|
||||||
renderItem={(field) => (
|
{nameOf(asset.values.personId, lookups.people)}
|
||||||
<div key={field.fieldId}>
|
</BrowserLink>
|
||||||
<dt className="text-sm text-muted-foreground">
|
) : (
|
||||||
{field.name}
|
t("noAssignment")
|
||||||
{field.unit ? ` (${field.unit})` : ""}
|
)}
|
||||||
</dt>
|
</DetailField>
|
||||||
<dd className="whitespace-pre-wrap break-words">
|
<DetailField label={t("workstationInAsset")}>
|
||||||
|
{workstations?.length
|
||||||
|
? workstations.map((item) => (
|
||||||
|
<BrowserLink
|
||||||
|
className="block"
|
||||||
|
href={`/companies/${asset.companyId}/workstations`}
|
||||||
|
key={item.id}
|
||||||
|
>
|
||||||
|
{item.values.name}
|
||||||
|
</BrowserLink>
|
||||||
|
))
|
||||||
|
: t("noWorkstation")}
|
||||||
|
</DetailField>
|
||||||
|
</dl>
|
||||||
|
</DetailSection>
|
||||||
|
<DetailSection title={t("characteristics")}>
|
||||||
|
<FieldSections
|
||||||
|
items={asset.specification.fields}
|
||||||
|
renderItem={(field) => (
|
||||||
|
<DetailField
|
||||||
|
key={field.fieldId}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
{field.name}
|
||||||
|
{field.unit ? ` (${field.unit})` : ""}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
{display(asset.values.characteristics[field.fieldId])}
|
{display(asset.values.characteristics[field.fieldId])}
|
||||||
</dd>
|
</DetailField>
|
||||||
</div>
|
)}
|
||||||
)}
|
sections={asset.specification.sections}
|
||||||
sections={asset.specification.sections}
|
ungroupedLabel={t("sections.ungrouped")}
|
||||||
ungroupedLabel={t("sections.ungrouped")}
|
values
|
||||||
values
|
/>
|
||||||
/>
|
</DetailSection>
|
||||||
{asset.values.notes && (
|
{asset.values.notes && (
|
||||||
<p className="whitespace-pre-wrap break-words">{asset.values.notes}</p>
|
<DetailSection title={t("detailSections.notes")}>
|
||||||
|
<p className="whitespace-pre-wrap break-words text-sm">{asset.values.notes}</p>
|
||||||
|
</DetailSection>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
{!compact && (
|
{!compact && (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { DetailSection } from "@repo/rdx-ui/components/detail-section";
|
||||||
|
import { XIcon } from "@repo/rdx-ui/lib/icons";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -36,31 +38,41 @@ export function AssetSelectionPanel({
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label={t("assetPanel")}
|
aria-label={t("assetPanel")}
|
||||||
className="hidden min-w-0 grid-cols-[minmax(0,1fr)] gap-6 rounded-panel border border-border bg-muted p-5 [overflow-wrap:anywhere] inventory:grid"
|
className="@container hidden min-w-0 grid-cols-[minmax(0,1fr)] gap-4 rounded-panel border border-border bg-card p-4 [overflow-wrap:anywhere] inventory:grid"
|
||||||
>
|
>
|
||||||
<header className="flex items-start justify-between gap-3">
|
<header className="flex items-start justify-between gap-3">
|
||||||
<h2 className="min-w-0 break-words text-lg font-semibold">{asset.values.name}</h2>
|
<h2 className="min-w-0 break-words text-lg font-semibold">{asset.values.name}</h2>
|
||||||
<Button aria-label={t("closeAssetPanel")} onClick={onClose} size="sm" variant="ghost">
|
<Button aria-label={t("closeAssetPanel")} onClick={onClose} size="icon-sm" variant="ghost">
|
||||||
×
|
<XIcon aria-hidden="true" className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<AssetDetail asset={asset} compact lookups={lookups}>
|
|
||||||
<AssetRelationships assetId={asset.id} companyId={companyId} csrfToken={csrfToken} />
|
|
||||||
<AssetSupport
|
|
||||||
assetId={asset.id}
|
|
||||||
companyId={companyId}
|
|
||||||
csrfToken={csrfToken}
|
|
||||||
retired={asset.values.status === "retired"}
|
|
||||||
/>
|
|
||||||
</AssetDetail>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button onClick={() => onView(asset.id)} variant="outline">
|
<Button onClick={() => onView(asset.id)} size="sm" variant="outline">
|
||||||
{t("viewAsset")}
|
{t("viewAsset")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={!canEdit} onClick={() => onEdit(asset.id)} variant="outline">
|
<Button disabled={!canEdit} onClick={() => onEdit(asset.id)} size="sm" variant="outline">
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<AssetDetail asset={asset} compact lookups={lookups}>
|
||||||
|
<DetailSection title={t("relations")}>
|
||||||
|
<AssetRelationships
|
||||||
|
assetId={asset.id}
|
||||||
|
compact
|
||||||
|
companyId={companyId}
|
||||||
|
csrfToken={csrfToken}
|
||||||
|
/>
|
||||||
|
</DetailSection>
|
||||||
|
<DetailSection title={t("support.title")}>
|
||||||
|
<AssetSupport
|
||||||
|
assetId={asset.id}
|
||||||
|
compact
|
||||||
|
companyId={companyId}
|
||||||
|
csrfToken={csrfToken}
|
||||||
|
retired={asset.values.status === "retired"}
|
||||||
|
/>
|
||||||
|
</DetailSection>
|
||||||
|
</AssetDetail>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 inventory:grid-cols-[minmax(0,1fr)_280px] inventory:data-[detail-open=false]:grid-cols-[minmax(0,1fr)]"
|
className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 inventory:grid-cols-[minmax(0,1fr)_clamp(320px,24vw,400px)] inventory:data-[detail-open=false]:grid-cols-[minmax(0,1fr)]"
|
||||||
data-detail-open={detailOpen && Boolean(selected)}
|
data-detail-open={detailOpen && Boolean(selected)}
|
||||||
data-slot="inventory-layout"
|
data-slot="inventory-layout"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { buildCatalogResponse } from "../../../api/application/asset-catalog-res
|
|||||||
import { AssetCatalog } from "../../../api/domain/asset-catalog.aggregate";
|
import { AssetCatalog } from "../../../api/domain/asset-catalog.aggregate";
|
||||||
import { seedAssetCatalog } from "../../../api/domain/asset-catalog-seed";
|
import { seedAssetCatalog } from "../../../api/domain/asset-catalog-seed";
|
||||||
import type { RelationshipDTO } from "../../../common";
|
import type { RelationshipDTO } from "../../../common";
|
||||||
|
import { AssetSchema } from "../../../common/inventory";
|
||||||
import { AssetRelationships } from "../../asset-relationships";
|
import { AssetRelationships } from "../../asset-relationships";
|
||||||
import { catalogKeys } from "../../catalog/shared/api";
|
import { catalogKeys } from "../../catalog/shared/api";
|
||||||
import { CatalogRelationForm } from "../../catalog-relations";
|
import { CatalogRelationForm } from "../../catalog-relations";
|
||||||
@ -20,6 +21,7 @@ import { CatalogTemplateForm } from "../../catalog-templates";
|
|||||||
import { assetsEnglish, assetsSpanish } from "../../shared";
|
import { assetsEnglish, assetsSpanish } from "../../shared";
|
||||||
import { inventoryKeys } from "../../shared/queries";
|
import { inventoryKeys } from "../../shared/queries";
|
||||||
import { WorkstationForm, WorkstationsPage } from "../../workstations";
|
import { WorkstationForm, WorkstationsPage } from "../../workstations";
|
||||||
|
import { AssetSelectionPanel } from "../detail/ui/asset-selection-panel";
|
||||||
import { AssetInventoryPage } from "../list";
|
import { AssetInventoryPage } from "../list";
|
||||||
|
|
||||||
import { AssetForm } from "./ui";
|
import { AssetForm } from "./ui";
|
||||||
@ -218,3 +220,80 @@ test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no
|
|||||||
assert.ok(!nested.includes(`aria-label="${label}"`));
|
assert.ok(!nested.includes(`aria-label="${label}"`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("la ficha lateral agrupa información y ofrece acciones superiores y enlaces reales", async () => {
|
||||||
|
const fieldId = UniqueID.generateNewID().toPrimitive();
|
||||||
|
const personId = UniqueID.generateNewID().toPrimitive();
|
||||||
|
const siteId = UniqueID.generateNewID().toPrimitive();
|
||||||
|
const asset = AssetSchema.parse({
|
||||||
|
id: UniqueID.generateNewID().toPrimitive(),
|
||||||
|
companyId,
|
||||||
|
specification: {
|
||||||
|
templateId: fieldId,
|
||||||
|
templateName: "Impresora",
|
||||||
|
fields: [
|
||||||
|
{ fieldId, name: "Número de serie", type: "text", unit: "", choices: [], required: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
name: "Impresora de administración",
|
||||||
|
templateId: fieldId,
|
||||||
|
siteId,
|
||||||
|
personId,
|
||||||
|
status: "in_use",
|
||||||
|
notes: "Uso compartido",
|
||||||
|
characteristics: { [fieldId]: "ABC123" },
|
||||||
|
},
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
id: fieldId,
|
||||||
|
actorId: personId,
|
||||||
|
actorName: "Técnico",
|
||||||
|
recordedAt: "2026-10-09T10:00:00.000Z",
|
||||||
|
values: {
|
||||||
|
name: "Impresora de administración",
|
||||||
|
templateId: fieldId,
|
||||||
|
siteId,
|
||||||
|
personId,
|
||||||
|
status: "in_use",
|
||||||
|
notes: "Uso compartido",
|
||||||
|
characteristics: { [fieldId]: "ABC123" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const html = await render(
|
||||||
|
"es",
|
||||||
|
<AssetSelectionPanel
|
||||||
|
asset={asset}
|
||||||
|
canEdit
|
||||||
|
companyId={companyId}
|
||||||
|
csrfToken="test"
|
||||||
|
lookups={{
|
||||||
|
...lookups,
|
||||||
|
people: [{ id: personId, name: "Ana", active: true }],
|
||||||
|
sites: [{ id: siteId, name: "Fábrica" }],
|
||||||
|
}}
|
||||||
|
onClose={() => undefined}
|
||||||
|
onEdit={() => undefined}
|
||||||
|
onView={() => undefined}
|
||||||
|
open
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
assert.ok(html.indexOf("Ficha e historial") < html.indexOf("Identificación"));
|
||||||
|
assert.ok(html.indexOf(">Editar<") < html.indexOf("Identificación"));
|
||||||
|
for (const group of [
|
||||||
|
"Identificación",
|
||||||
|
"Ubicación y asignación",
|
||||||
|
"Características",
|
||||||
|
"Observaciones",
|
||||||
|
]) {
|
||||||
|
assert.ok(html.includes(group));
|
||||||
|
}
|
||||||
|
assert.ok(html.includes("<summary"));
|
||||||
|
assert.ok(html.includes('open=""'));
|
||||||
|
assert.ok(html.includes(`href="/companies/${companyId}/people/${personId}"`));
|
||||||
|
assert.ok(html.includes(`href="/companies/${companyId}/sites"`));
|
||||||
|
assert.ok(html.includes(`href="/companies/${companyId}/catalogs/assets?tab=templates"`));
|
||||||
|
assert.ok(html.includes("ABC123"));
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type AssetSupportProps = {
|
export type AssetSupportProps = {
|
||||||
|
compact?: boolean;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
assetId: string;
|
assetId: string;
|
||||||
csrfToken: string;
|
csrfToken: string;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useAssetSupportController } from "../controllers";
|
|||||||
import type { AssetSupportProps } from "../types";
|
import type { AssetSupportProps } from "../types";
|
||||||
|
|
||||||
export function AssetSupport(props: AssetSupportProps) {
|
export function AssetSupport(props: AssetSupportProps) {
|
||||||
const { companyId, assetId, csrfToken, retired = false } = props;
|
const { companyId, assetId, csrfToken, retired = false, compact = false } = props;
|
||||||
const {
|
const {
|
||||||
query,
|
query,
|
||||||
open,
|
open,
|
||||||
@ -27,9 +27,9 @@ export function AssetSupport(props: AssetSupportProps) {
|
|||||||
} = useAssetSupportController(props);
|
} = useAssetSupportController(props);
|
||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3 rounded-md border p-4">
|
<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">
|
<header className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h3 className="font-semibold">{t("support.title")}</h3>
|
{!compact && <h3 className="font-semibold">{t("support.title")}</h3>}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button onClick={() => setToolsOpen(true)} size="sm" variant="outline">
|
<Button onClick={() => setToolsOpen(true)} size="sm" variant="outline">
|
||||||
{t("support.tools")}
|
{t("support.tools")}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { DetailField } from "@repo/rdx-ui/components/detail-field";
|
||||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -35,15 +36,17 @@ export function ResourceDetail({
|
|||||||
<FieldSections
|
<FieldSections
|
||||||
items={resource.specification.fields}
|
items={resource.specification.fields}
|
||||||
renderItem={(field) => (
|
renderItem={(field) => (
|
||||||
<div key={field.fieldId}>
|
<DetailField
|
||||||
<dt className="text-sm text-muted-foreground">
|
key={field.fieldId}
|
||||||
{field.name}
|
label={
|
||||||
{field.unit ? ` (${field.unit})` : ""}
|
<>
|
||||||
</dt>
|
{field.name}
|
||||||
<dd className="whitespace-pre-wrap break-words">
|
{field.unit ? ` (${field.unit})` : ""}
|
||||||
{display(resource.values.characteristics[field.fieldId])}
|
</>
|
||||||
</dd>
|
}
|
||||||
</div>
|
>
|
||||||
|
{display(resource.values.characteristics[field.fieldId])}
|
||||||
|
</DetailField>
|
||||||
)}
|
)}
|
||||||
sections={resource.specification.sections}
|
sections={resource.specification.sections}
|
||||||
ungroupedLabel={t("sections.ungrouped")}
|
ungroupedLabel={t("sections.ungrouped")}
|
||||||
|
|||||||
13
packages/rdx-ui/src/components/detail-field.tsx
Normal file
13
packages/rdx-ui/src/components/detail-field.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
/** Etiqueta y valor responden al ancho de su ficha, no al ancho total del navegador. */
|
||||||
|
export function DetailField({ label, children }: { label: ReactNode; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="grid min-w-0 gap-1 border-b border-border/60 py-2.5 first:pt-0 last:border-0 last:pb-0 @min-[240px]:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] @min-[240px]:gap-3">
|
||||||
|
<dt className="text-xs leading-5 text-muted-foreground">{label}</dt>
|
||||||
|
<dd className="min-w-0 whitespace-pre-wrap break-words text-sm font-medium leading-5 [&_a]:cursor-pointer [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-ring">
|
||||||
|
{children}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
packages/rdx-ui/src/components/detail-section.tsx
Normal file
13
packages/rdx-ui/src/components/detail-section.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
/** Agrupación plegable accesible para fichas; comienza abierta sin ocultar información. */
|
||||||
|
export function DetailSection({ title, children }: { title: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<details className="@container border-t border-border/60 first:border-t-0" open>
|
||||||
|
<summary className="cursor-pointer py-2 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-ring">
|
||||||
|
{title}
|
||||||
|
</summary>
|
||||||
|
<div className="space-y-3 pb-2 pt-1">{children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -29,9 +29,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
}, [hasErrors]);
|
}, [hasErrors]);
|
||||||
const content = (fields: readonly T[]) =>
|
const content = (fields: readonly T[]) =>
|
||||||
values ? (
|
values ? (
|
||||||
<dl className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
<dl className="@container">{fields.map(renderItem)}</dl>
|
||||||
{fields.map(renderItem)}
|
|
||||||
</dl>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
||||||
{fields.map(renderItem)}
|
{fields.map(renderItem)}
|
||||||
@ -62,11 +60,17 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<details className="@container rounded-md border" key={section.id} open>
|
<details
|
||||||
<summary className="cursor-pointer rounded-md px-4 py-3 text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring">
|
className={values ? "@container" : "@container rounded-md border"}
|
||||||
|
key={section.id}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
className={`cursor-pointer text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring ${values ? "py-2" : "rounded-md px-4 py-3"}`}
|
||||||
|
>
|
||||||
{section.name}
|
{section.name}
|
||||||
</summary>
|
</summary>
|
||||||
<div className="space-y-3 border-t p-4">
|
<div className={values ? "space-y-3 pt-1" : "space-y-3 border-t p-4"}>
|
||||||
{section.description && (
|
{section.description && (
|
||||||
<p className="text-sm text-muted-foreground">{section.description}</p>
|
<p className="text-sm text-muted-foreground">{section.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -23,4 +23,5 @@ export {
|
|||||||
UserRoundXIcon,
|
UserRoundXIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user