211 lines
7.1 KiB
TypeScript
211 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import type { AdapterResult, RegistryAvailability } from "../types";
|
|
import { CommercialEmptyState } from "./CommercialEmptyState";
|
|
import {
|
|
commercialEntityName,
|
|
fieldLabel,
|
|
friendlyMessage,
|
|
getRegistryPresentation,
|
|
isTechnicalField,
|
|
presentValue,
|
|
registryKindForField,
|
|
} from "../presentation";
|
|
|
|
function availabilityLabel(a: RegistryAvailability): string {
|
|
switch (a) {
|
|
case "loading":
|
|
return "در حال بارگذاری…";
|
|
case "empty":
|
|
return "خالی";
|
|
case "unavailable":
|
|
return "در دسترس نیست";
|
|
case "permission_denied":
|
|
return "بدون دسترسی";
|
|
case "error":
|
|
return "خطا";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function PresentedValue({ value, contextKey }: { value: unknown; contextKey?: string }) {
|
|
const presented = presentValue(value);
|
|
if (Array.isArray(presented)) {
|
|
return (
|
|
<div className="grid gap-2">
|
|
{presented.map((item, index) => (
|
|
typeof item === "string" ? (
|
|
<span key={index} className="w-fit rounded-full bg-gray-100 px-2.5 py-1 text-xs text-gray-700">
|
|
{item}
|
|
</span>
|
|
) : (
|
|
<div key={index} className="rounded-xl border border-gray-100 bg-white p-2">
|
|
<PresentedValue value={item} contextKey={contextKey} />
|
|
</div>
|
|
)
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
if (presented && typeof presented === "object") {
|
|
const entries = Object.entries(presented).filter(([key]) => !isTechnicalField(key));
|
|
const referencedKind = registryKindForField(contextKey);
|
|
const codeEntry = Object.entries(presented).find(([key]) => /(^code$|_code$)/.test(key));
|
|
const explicitName =
|
|
typeof presented.display_name === "string"
|
|
? presented.display_name
|
|
: typeof presented.title === "string"
|
|
? presented.title
|
|
: null;
|
|
const objectTitle =
|
|
referencedKind && codeEntry
|
|
? commercialEntityName(referencedKind, codeEntry[1], explicitName)
|
|
: null;
|
|
return (
|
|
<details className="rounded-xl border border-gray-100 bg-gray-50 p-3">
|
|
<summary className="cursor-pointer text-xs font-semibold text-secondary">
|
|
{objectTitle || "مشاهده جزئیات"}
|
|
</summary>
|
|
<dl className="mt-3 grid gap-3 sm:grid-cols-2">
|
|
{entries.map(([key, nested]) => (
|
|
<div key={key}>
|
|
<dt className="text-[11px] text-gray-500">{fieldLabel(key)}</dt>
|
|
<dd className="mt-1 text-xs text-secondary"><PresentedValue value={nested} contextKey={key} /></dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</details>
|
|
);
|
|
}
|
|
return <>{presented}</>;
|
|
}
|
|
|
|
/** Generic human-facing catalog list. Implementation identifiers are deliberately hidden. */
|
|
export function RegistryBrowser({
|
|
kind,
|
|
title,
|
|
subtitle,
|
|
result,
|
|
codeKey,
|
|
nameKey = "display_name",
|
|
emptyTitle,
|
|
}: {
|
|
kind: string;
|
|
title?: string;
|
|
subtitle?: string;
|
|
result: AdapterResult<any[]> | null;
|
|
codeKey: string;
|
|
nameKey?: string;
|
|
emptyTitle?: string;
|
|
}) {
|
|
const presentation = getRegistryPresentation(kind);
|
|
const sectionTitle = title || presentation.sectionTitle;
|
|
if (!result || result.availability === "loading") {
|
|
return (
|
|
<section>
|
|
<h3 className="text-sm font-semibold text-secondary">{sectionTitle}</h3>
|
|
<p className="mt-2 text-xs text-gray-500">{availabilityLabel("loading")}</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (result.availability !== "ready" || !result.data.length) {
|
|
return (
|
|
<section>
|
|
<h3 className="text-sm font-semibold text-secondary">{sectionTitle}</h3>
|
|
{subtitle ? <p className="mt-1 text-xs text-gray-500">{subtitle}</p> : null}
|
|
<div className="mt-2">
|
|
<CommercialEmptyState
|
|
title={emptyTitle || presentation.emptyStateTitle}
|
|
description={friendlyMessage(result.message, presentation.emptyStateDescription)}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section>
|
|
<h3 className="text-sm font-semibold text-secondary">{sectionTitle}</h3>
|
|
{subtitle ? <p className="mt-1 text-xs text-gray-500">{subtitle}</p> : null}
|
|
<ul className="mt-3 grid gap-2 sm:grid-cols-2">
|
|
{result.data.map((raw, idx) => {
|
|
const item = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
|
const code = String(item[codeKey] ?? idx);
|
|
const explicitName = typeof item[nameKey] === "string" ? String(item[nameKey]) : null;
|
|
const name = commercialEntityName(kind, code, explicitName);
|
|
const meta = item.metadata;
|
|
const detailEntries = Object.entries(item).filter(
|
|
([key, value]) =>
|
|
key !== codeKey &&
|
|
key !== nameKey &&
|
|
key !== "metadata" &&
|
|
!isTechnicalField(key) &&
|
|
value != null
|
|
);
|
|
return (
|
|
<li
|
|
key={code}
|
|
className="rounded-xl border border-gray-100 bg-white px-3 py-2 text-sm shadow-sm"
|
|
>
|
|
<p className="font-medium text-secondary">{name}</p>
|
|
{detailEntries.length ? (
|
|
<dl className="mt-3 space-y-2">
|
|
{detailEntries.slice(0, 4).map(([key, value]) => (
|
|
<div key={key} className="flex items-start justify-between gap-3 text-xs">
|
|
<dt className="text-gray-500">{fieldLabel(key)}</dt>
|
|
<dd className="text-left text-secondary"><PresentedValue value={value} contextKey={key} /></dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
) : null}
|
|
{meta && typeof meta === "object" && !Array.isArray(meta) ? (
|
|
<div className="mt-3"><PresentedValue value={meta} contextKey="metadata" /></div>
|
|
) : null}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/** Renders an arbitrary metadata bag without whitelisting attribute keys. */
|
|
export function MetadataBagView({
|
|
title,
|
|
attributes,
|
|
message,
|
|
availability,
|
|
}: {
|
|
title: string;
|
|
attributes?: Record<string, unknown> | null;
|
|
message?: string;
|
|
availability?: RegistryAvailability;
|
|
}) {
|
|
const entries = attributes ? Object.entries(attributes) : [];
|
|
if (!entries.length) {
|
|
return (
|
|
<CommercialEmptyState
|
|
title={title}
|
|
description={friendlyMessage(message, availabilityLabel(availability || "empty"))}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div className="rounded-2xl border border-gray-100 bg-white p-4">
|
|
<h3 className="text-sm font-semibold text-secondary">{title}</h3>
|
|
<dl className="mt-3 space-y-2">
|
|
{entries.filter(([key]) => !isTechnicalField(key)).map(([key, value]) => (
|
|
<div key={key} className="rounded-xl bg-gray-50 p-3 text-xs">
|
|
<dt className="text-gray-500">{fieldLabel(key)}</dt>
|
|
<dd className="mt-2 text-secondary">
|
|
<PresentedValue value={value} contextKey={key} />
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
);
|
|
}
|