mejora de la ficha de plantillas de activos
This commit is contained in:
parent
108e810c3b
commit
853011cec3
@ -332,3 +332,9 @@ Los módulos conectan este componente mediante `DeleteRecordDialog`, separado de
|
|||||||
## IconPicker
|
## IconPicker
|
||||||
|
|
||||||
`@repo/rdx-ui/components/icon-picker` presenta opciones visuales mediante botones con nombre accesible, tooltip nativo y `aria-pressed`. Recibe opciones (valor, etiqueta traducida e icono), valor actual, callback y estado deshabilitado. No conoce entidades ni endpoints. El catálogo de plantillas de activos aporta los iconos Lucide y valida sus claves; el componente admite teclado y distribución responsive.
|
`@repo/rdx-ui/components/icon-picker` presenta opciones visuales mediante botones con nombre accesible, tooltip nativo y `aria-pressed`. Recibe opciones (valor, etiqueta traducida e icono), valor actual, callback y estado deshabilitado. No conoce entidades ni endpoints. El catálogo de plantillas de activos aporta los iconos Lucide y valida sus claves; el componente admite teclado y distribución responsive.
|
||||||
|
|
||||||
|
## Consulta compacta de plantillas
|
||||||
|
|
||||||
|
Las fichas de plantillas de activos y recursos usan `FieldSections layout="rows"` y `TemplateFieldSummary`. Secciones plegables abiertas inicialmente, con títulos en mayúsculas de tamaño pequeño y color `muted-foreground`, sin tarjetas ni rellenos anidados. El indicador ocupa una anchura fija y los campos se alinean con el inicio del texto del título; filas compactas con nombre, tipo/unidad y obligatoriedad. Los campos desactivados conservan su aviso y los campos sin sección siguen visibles. En pantallas amplias se muestran dos columnas de secciones; en anchos menores, una. Cada fila adapta sus metadatos al ancho real de su sección.
|
||||||
|
|
||||||
|
La ayuda de consulta usa `ContextHelp variant="plain"`: separación superior discreta, sin caja exterior ni fondo en el desplegable. Los formularios mantienen la presentación existente por defecto. `FormDialog.title` admite contenido React para colocar un icono decorativo junto al título sin una segunda cabecera repetida.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||||
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
||||||
|
import { TemplateFieldSummary } from "@repo/rdx-ui/components/template-field-summary";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { CatalogItemDTO } from "../../../../common";
|
import type { CatalogItemDTO } from "../../../../common";
|
||||||
@ -31,16 +32,21 @@ export function CatalogDefinitionDialog({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
open
|
open
|
||||||
title={t("viewDefinition", { name: values.name })}
|
title={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{values.kind === "template" && (
|
||||||
|
<AssetTemplateIcon
|
||||||
|
className="size-5 shrink-0 text-muted-foreground"
|
||||||
|
icon={values.icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{t("viewDefinition", { name: values.name })}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
wide={values.kind === "template"}
|
wide={values.kind === "template"}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{values.kind === "template" && values.icon && (
|
{item.builtin && <p className="text-sm text-muted-foreground">{t("protected")}</p>}
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<AssetTemplateIcon icon={values.icon} />
|
|
||||||
{t(`templateIcon.options.${values.icon}`)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{values.description && (
|
{values.description && (
|
||||||
<p className="text-sm text-muted-foreground">{values.description}</p>
|
<p className="text-sm text-muted-foreground">{values.description}</p>
|
||||||
)}
|
)}
|
||||||
@ -71,36 +77,29 @@ export function CatalogDefinitionDialog({
|
|||||||
) : (
|
) : (
|
||||||
<FieldSections
|
<FieldSections
|
||||||
items={values.fields}
|
items={values.fields}
|
||||||
|
layout="rows"
|
||||||
renderItem={(binding) => {
|
renderItem={(binding) => {
|
||||||
const field = items.find((entry) => entry.id === binding.fieldId);
|
const field = items.find((entry) => entry.id === binding.fieldId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<TemplateFieldSummary
|
||||||
className="flex justify-between gap-4 rounded-md border p-3 text-sm"
|
|
||||||
key={binding.fieldId}
|
key={binding.fieldId}
|
||||||
>
|
metadata={
|
||||||
<div>
|
field?.values.kind === "field"
|
||||||
<p className="font-medium">{field?.values.name}</p>
|
? [t(field.values.type), field.values.unit].filter(Boolean).join(" · ")
|
||||||
{field?.values.kind === "field" && (
|
: ""
|
||||||
<p className="text-muted-foreground">
|
}
|
||||||
{t(field.values.type)}
|
name={field?.values.name ?? binding.fieldId}
|
||||||
{field.values.unit ? ` · ${field.values.unit}` : ""}
|
notice={field && !field.effectiveActive ? t("disabledField") : undefined}
|
||||||
</p>
|
requirement={t(binding.required ? "required" : "optional")}
|
||||||
)}
|
/>
|
||||||
{field && !field.effectiveActive && (
|
|
||||||
<p className="text-muted-foreground">{t("disabledField")}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>{t(binding.required ? "required" : "optional")}</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
sections={values.sections}
|
sections={values.sections}
|
||||||
ungroupedLabel={t("sections.ungrouped")}
|
ungroupedLabel={t("sections.ungrouped")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.builtin && <p className="text-sm text-muted-foreground">{t("protected")}</p>}
|
<CatalogHelp kind={values.kind} scope={item.companyId ?? "global"} variant="plain" />
|
||||||
<CatalogHelp kind={values.kind} scope={item.companyId ?? "global"} />
|
|
||||||
</div>
|
</div>
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
export function CatalogHelp({
|
export function CatalogHelp({
|
||||||
kind,
|
kind,
|
||||||
scope,
|
scope,
|
||||||
|
variant = "card",
|
||||||
}: {
|
}: {
|
||||||
kind: "field" | "template" | "relation";
|
kind: "field" | "template" | "relation";
|
||||||
scope: string;
|
scope: string;
|
||||||
|
variant?: "card" | "plain";
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ export function CatalogHelp({
|
|||||||
guidance={t(`${kind}Guidance`)}
|
guidance={t(`${kind}Guidance`)}
|
||||||
scopeNote={t(scope === "global" ? "globalHelp" : "companyHelp")}
|
scopeNote={t(scope === "global" ? "globalHelp" : "companyHelp")}
|
||||||
title={t("helpTitle")}
|
title={t("helpTitle")}
|
||||||
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||||
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
import { FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
||||||
|
import { TemplateFieldSummary } from "@repo/rdx-ui/components/template-field-summary";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { CatalogItemDTO } from "../../../../common";
|
import type { CatalogItemDTO } from "../../../../common";
|
||||||
@ -34,6 +35,7 @@ export function CatalogDefinitionDialog({
|
|||||||
wide={values.kind === "template"}
|
wide={values.kind === "template"}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{item.builtin && <p className="text-sm text-muted-foreground">{t("protected")}</p>}
|
||||||
{values.description && (
|
{values.description && (
|
||||||
<p className="text-sm text-muted-foreground">{values.description}</p>
|
<p className="text-sm text-muted-foreground">{values.description}</p>
|
||||||
)}
|
)}
|
||||||
@ -59,36 +61,29 @@ export function CatalogDefinitionDialog({
|
|||||||
) : (
|
) : (
|
||||||
<FieldSections
|
<FieldSections
|
||||||
items={values.fields}
|
items={values.fields}
|
||||||
|
layout="rows"
|
||||||
renderItem={(binding) => {
|
renderItem={(binding) => {
|
||||||
const field = items.find((entry) => entry.id === binding.fieldId);
|
const field = items.find((entry) => entry.id === binding.fieldId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<TemplateFieldSummary
|
||||||
className="flex justify-between gap-4 rounded-md border p-3 text-sm"
|
|
||||||
key={binding.fieldId}
|
key={binding.fieldId}
|
||||||
>
|
metadata={
|
||||||
<div>
|
field?.values.kind === "field"
|
||||||
<p className="font-medium">{field?.values.name}</p>
|
? [t(field.values.type), field.values.unit].filter(Boolean).join(" · ")
|
||||||
{field?.values.kind === "field" && (
|
: ""
|
||||||
<p className="text-muted-foreground">
|
}
|
||||||
{t(field.values.type)}
|
name={field?.values.name ?? binding.fieldId}
|
||||||
{field.values.unit ? ` · ${field.values.unit}` : ""}
|
notice={field && !field.effectiveActive ? t("disabledField") : undefined}
|
||||||
</p>
|
requirement={t(binding.required ? "required" : "optional")}
|
||||||
)}
|
/>
|
||||||
{field && !field.effectiveActive && (
|
|
||||||
<p className="text-muted-foreground">{t("disabledField")}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>{t(binding.required ? "required" : "optional")}</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
sections={values.sections}
|
sections={values.sections}
|
||||||
ungroupedLabel={t("sections.ungrouped")}
|
ungroupedLabel={t("sections.ungrouped")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.builtin && <p className="text-sm text-muted-foreground">{t("protected")}</p>}
|
<CatalogHelp kind={values.kind} scope={item.companyId ?? "global"} variant="plain" />
|
||||||
<CatalogHelp kind={values.kind} scope={item.companyId ?? "global"} />
|
|
||||||
</div>
|
</div>
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function CatalogHelp({ kind, scope }: { kind: "field" | "template"; scope: string }) {
|
export function CatalogHelp({
|
||||||
|
kind,
|
||||||
|
scope,
|
||||||
|
variant = "card",
|
||||||
|
}: {
|
||||||
|
kind: "field" | "template";
|
||||||
|
scope: string;
|
||||||
|
variant?: "card" | "plain";
|
||||||
|
}) {
|
||||||
const { t } = useTranslation("resources");
|
const { t } = useTranslation("resources");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -12,6 +20,7 @@ export function CatalogHelp({ kind, scope }: { kind: "field" | "template"; scope
|
|||||||
guidance={t(`${kind}Guidance`)}
|
guidance={t(`${kind}Guidance`)}
|
||||||
scopeNote={t(scope === "global" ? "globalHelp" : "companyHelp")}
|
scopeNote={t(scope === "global" ? "globalHelp" : "companyHelp")}
|
||||||
title={t("helpTitle")}
|
title={t("helpTitle")}
|
||||||
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export function ContextHelp({
|
|||||||
examplesLabel,
|
examplesLabel,
|
||||||
scopeNote,
|
scopeNote,
|
||||||
icon = <InfoIcon />,
|
icon = <InfoIcon />,
|
||||||
|
variant = "card",
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -17,16 +18,25 @@ export function ContextHelp({
|
|||||||
examplesLabel: string;
|
examplesLabel: string;
|
||||||
scopeNote?: string | undefined;
|
scopeNote?: string | undefined;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
variant?: "card" | "plain";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
className="space-y-4 rounded-xl border bg-muted/30 p-4 text-sm sm:p-5"
|
className={
|
||||||
|
variant === "plain"
|
||||||
|
? "space-y-1 border-t border-border/60 pt-3 text-sm"
|
||||||
|
: "space-y-4 rounded-xl border bg-muted/30 p-4 text-sm sm:p-5"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary [&_svg]:size-5"
|
className={
|
||||||
|
variant === "plain"
|
||||||
|
? "flex size-5 shrink-0 items-center justify-center pt-1 text-muted-foreground [&_svg]:size-4"
|
||||||
|
: "flex size-10 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary [&_svg]:size-5"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
@ -36,8 +46,16 @@ export function ContextHelp({
|
|||||||
{scopeNote && <p className="leading-relaxed text-muted-foreground">{scopeNote}</p>}
|
{scopeNote && <p className="leading-relaxed text-muted-foreground">{scopeNote}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<details className="group overflow-hidden rounded-lg border border-primary/10">
|
<details
|
||||||
<summary className="flex cursor-pointer list-none items-center gap-3 rounded-lg bg-primary/5 px-4 py-3 font-semibold text-primary focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring [&::-webkit-details-marker]:hidden">
|
className={
|
||||||
|
variant === "plain"
|
||||||
|
? "group"
|
||||||
|
: "group overflow-hidden rounded-lg border border-primary/10"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
className={`flex cursor-pointer list-none items-center gap-3 font-semibold text-primary focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring [&::-webkit-details-marker]:hidden ${variant === "plain" ? "py-2 text-sm hover:underline" : "rounded-lg bg-primary/5 px-4 py-3"}`}
|
||||||
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="size-4 shrink-0 -rotate-90 transition-transform group-open:rotate-0 motion-reduce:transition-none"
|
className="size-4 shrink-0 -rotate-90 transition-transform group-open:rotate-0 motion-reduce:transition-none"
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { ChevronRightIcon } from "../lib/icons";
|
||||||
|
|
||||||
export type FieldSection = { id: string; name: string; description: string };
|
export type FieldSection = { id: string; name: string; description: string };
|
||||||
|
|
||||||
/** Presentación compartida; no conoce catálogos, valores ni endpoints. */
|
/** Presentación compartida; no conoce catálogos, valores ni endpoints. */
|
||||||
@ -11,6 +13,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
renderItem,
|
renderItem,
|
||||||
values = false,
|
values = false,
|
||||||
hasErrors = false,
|
hasErrors = false,
|
||||||
|
layout = "fields",
|
||||||
}: {
|
}: {
|
||||||
sections?: readonly FieldSection[] | undefined;
|
sections?: readonly FieldSection[] | undefined;
|
||||||
items: readonly T[];
|
items: readonly T[];
|
||||||
@ -18,6 +21,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
renderItem: (item: T) => ReactNode;
|
renderItem: (item: T) => ReactNode;
|
||||||
values?: boolean;
|
values?: boolean;
|
||||||
hasErrors?: boolean;
|
hasErrors?: boolean;
|
||||||
|
layout?: "fields" | "rows";
|
||||||
}) {
|
}) {
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -33,7 +37,13 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
values ? (
|
values ? (
|
||||||
<dl className="@container">{fields.map(renderItem)}</dl>
|
<dl className="@container">{fields.map(renderItem)}</dl>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
<div
|
||||||
|
className={
|
||||||
|
layout === "rows"
|
||||||
|
? "divide-y divide-border/50 pl-5"
|
||||||
|
: "grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3"
|
||||||
|
}
|
||||||
|
>
|
||||||
{fields.map(renderItem)}
|
{fields.map(renderItem)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -44,9 +54,32 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
const known = new Set(sections.map((section) => section.id));
|
const known = new Set(sections.map((section) => section.id));
|
||||||
const ungrouped = items.filter((item) => !(item.sectionId && known.has(item.sectionId)));
|
const ungrouped = items.filter((item) => !(item.sectionId && known.has(item.sectionId)));
|
||||||
|
|
||||||
|
const sectionClassName =
|
||||||
|
layout === "rows"
|
||||||
|
? "@container group/field-section"
|
||||||
|
: values
|
||||||
|
? "@container"
|
||||||
|
: "@container rounded-md border";
|
||||||
|
const summaryClassName =
|
||||||
|
layout === "rows"
|
||||||
|
? "flex cursor-pointer list-none items-center gap-2 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden"
|
||||||
|
: `cursor-pointer text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring ${values ? "py-2" : "rounded-md px-4 py-3"}`;
|
||||||
|
const bodyClassName =
|
||||||
|
layout === "rows"
|
||||||
|
? "border-t border-border/60"
|
||||||
|
: values
|
||||||
|
? "space-y-3 pt-1"
|
||||||
|
: "space-y-3 border-t p-4";
|
||||||
|
const descriptionClassName =
|
||||||
|
layout === "rows" ? "pl-5 text-sm text-muted-foreground" : "text-sm text-muted-foreground";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="grid items-start gap-4 @min-[900px]:grid-cols-2"
|
className={
|
||||||
|
layout === "rows"
|
||||||
|
? "grid items-start gap-x-8 gap-y-3 @min-[900px]:grid-cols-2"
|
||||||
|
: "grid items-start gap-4 @min-[900px]:grid-cols-2"
|
||||||
|
}
|
||||||
onInvalidCapture={(event) => {
|
onInvalidCapture={(event) => {
|
||||||
// Abrir antes de que el navegador intente enfocar un control inválido oculto.
|
// Abrir antes de que el navegador intente enfocar un control inválido oculto.
|
||||||
if (event.target instanceof HTMLElement) {
|
if (event.target instanceof HTMLElement) {
|
||||||
@ -67,20 +100,18 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details
|
<details className={sectionClassName} key={section.id} open>
|
||||||
className={values ? "@container" : "@container rounded-md border"}
|
<summary className={summaryClassName}>
|
||||||
key={section.id}
|
{layout === "rows" && (
|
||||||
open
|
<ChevronRightIcon
|
||||||
>
|
aria-hidden="true"
|
||||||
<summary
|
className="size-3 shrink-0 transition-transform group-open/field-section:rotate-90 motion-reduce:transition-none"
|
||||||
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={values ? "space-y-3 pt-1" : "space-y-3 border-t p-4"}>
|
<div className={bodyClassName}>
|
||||||
{section.description && (
|
{section.description && <p className={descriptionClassName}>{section.description}</p>}
|
||||||
<p className="text-sm text-muted-foreground">{section.description}</p>
|
|
||||||
)}
|
|
||||||
{content(fields)}
|
{content(fields)}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@ -88,7 +119,15 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
})}
|
})}
|
||||||
{ungrouped.length > 0 && (
|
{ungrouped.length > 0 && (
|
||||||
<section className="col-span-full space-y-3">
|
<section className="col-span-full space-y-3">
|
||||||
<h4 className="text-sm font-medium">{ungroupedLabel}</h4>
|
<h4
|
||||||
|
className={
|
||||||
|
layout === "rows"
|
||||||
|
? "pl-5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
||||||
|
: "text-sm font-medium"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ungroupedLabel}
|
||||||
|
</h4>
|
||||||
{content(ungrouped)}
|
{content(ungrouped)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export function FormDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
title: string;
|
title: ReactNode;
|
||||||
cancelLabel: string;
|
cancelLabel: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
wide?: boolean;
|
wide?: boolean;
|
||||||
|
|||||||
25
packages/rdx-ui/src/components/template-field-summary.tsx
Normal file
25
packages/rdx-ui/src/components/template-field-summary.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/** Fila de consulta compacta; los textos y reglas pertenecen al catálogo consumidor. */
|
||||||
|
export function TemplateFieldSummary({
|
||||||
|
name,
|
||||||
|
metadata,
|
||||||
|
requirement,
|
||||||
|
notice,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
metadata: string;
|
||||||
|
requirement: string;
|
||||||
|
notice?: string | undefined;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-3 gap-y-0.5 py-2 text-sm @min-[420px]:grid-cols-[minmax(0,1fr)_minmax(100px,0.8fr)_auto]">
|
||||||
|
<span className="min-w-0 font-medium [overflow-wrap:anywhere]">{name}</span>
|
||||||
|
<span className="col-start-1 row-start-2 text-xs text-muted-foreground @min-[420px]:col-start-2 @min-[420px]:row-start-1">
|
||||||
|
{metadata}
|
||||||
|
</span>
|
||||||
|
<span className="col-start-2 row-start-1 text-xs text-muted-foreground @min-[420px]:col-start-3">
|
||||||
|
{requirement}
|
||||||
|
</span>
|
||||||
|
{notice && <p className="col-span-full text-xs text-muted-foreground">{notice}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { test } from "node:test";
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
import { FieldSections } from "./components/field-sections";
|
import { FieldSections } from "./components/field-sections";
|
||||||
|
import { TemplateFieldSummary } from "./components/template-field-summary";
|
||||||
import { copyTemplateLayout, removeTemplateSection } from "./lib/template-sections";
|
import { copyTemplateLayout, removeTemplateSection } from "./lib/template-sections";
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
@ -65,3 +66,43 @@ test("secciones visuales ordenadas, abiertas inicialmente y campos sin sección"
|
|||||||
assert.equal(plain.includes("<summary"), false);
|
assert.equal(plain.includes("<summary"), false);
|
||||||
assert.equal(plain.includes("Sin sección"), false);
|
assert.equal(plain.includes("Sin sección"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("consulta compacta conserva grupos, campos sin sección y avisos sin tarjetas anidadas", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<FieldSections
|
||||||
|
items={fields}
|
||||||
|
layout="rows"
|
||||||
|
renderItem={(field) => (
|
||||||
|
<TemplateFieldSummary
|
||||||
|
key={field.fieldId}
|
||||||
|
metadata="Texto"
|
||||||
|
name={field.fieldId}
|
||||||
|
notice={field.fieldId === "ram" ? "Campo desactivado" : undefined}
|
||||||
|
requirement={field.required ? "Obligatorio" : "Opcional"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
sections={sections}
|
||||||
|
ungroupedLabel="Sin sección"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(html.includes("<summary"));
|
||||||
|
assert.ok(html.includes('open=""'));
|
||||||
|
assert.ok(html.includes("Campo desactivado"));
|
||||||
|
assert.ok(html.includes("Obligatorio"));
|
||||||
|
assert.ok(html.includes("Opcional"));
|
||||||
|
assert.ok(html.includes("Sin sección"));
|
||||||
|
assert.equal((html.match(/>ram</g) ?? []).length, 1);
|
||||||
|
assert.equal(html.includes("rounded-md border"), false);
|
||||||
|
const withoutSections = renderToStaticMarkup(
|
||||||
|
<FieldSections
|
||||||
|
items={fields}
|
||||||
|
layout="rows"
|
||||||
|
renderItem={(field) => <span key={field.fieldId}>{field.fieldId}</span>}
|
||||||
|
ungroupedLabel="Sin sección"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(withoutSections.includes("<summary"), false);
|
||||||
|
assert.ok(withoutSections.includes(">notes<"));
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user