metis-desk/modules/assets/src/web/assets/detail/ui/asset-detail-page.tsx
David ded45e1c42 feat: add AssetRelatedInformation component for displaying asset profiles and software installations
test: implement resource template propagation tests for resource revisions

feat: create prepareResourceTemplateUpdates function for handling resource template updates

feat: add resource revision specifications for managing resource revisions in the database

test: add tests for entity column visibility and local storage handling

feat: implement entity column visibility storage for managing column preferences

feat: create synchronizeTemplateStructure utility for managing template field and section synchronization
2026-10-19 11:45:09 +02:00

125 lines
4.8 KiB
TypeScript

import { BrowserLink, EntityNotFound, useBrowserSearchParams } from "@metis/core/client";
import { EntityAction, EntityRowActions } from "@repo/rdx-ui/components/entity-list";
import { ChevronLeftIcon, PencilIcon, Trash2Icon } from "@repo/rdx-ui/lib/icons";
import { Button } from "@repo/shadcn-ui/components/button";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { AssetDTO } from "../../../../common";
import { AssetTemplateIcon } from "../../../catalog-templates/shared/ui/asset-template-icon";
import { InventoryFeedback } from "../../../shared";
import { AssetEditorDialog, DeleteAssetDialog } from "../../shared";
import type { InventoryLookups } from "../../shared/types";
import { useAssetDetailPageController } from "../controllers/use-asset-detail-page-controller";
import { AssetDetailContent } from "./asset-detail-content";
export function AssetDetailPage({
companyId,
assetId,
lookups,
relatedContent,
}: {
companyId: string;
assetId: string;
lookups: InventoryLookups;
relatedContent?: (asset: AssetDTO) => ReactNode;
}) {
const { t } = useTranslation("assets");
const { searchParams } = useBrowserSearchParams();
const search = searchParams.toString();
const backHref = `/companies/${companyId}/assets${search ? `?${search}` : ""}`;
const c = useAssetDetailPageController(companyId, assetId, backHref);
const { asset } = c;
const template = c.catalog.data?.items.find((item) => item.id === asset?.values.templateId);
if (c.inventory.isSuccess && !asset) {
return <EntityNotFound />;
}
return (
<div className="@container min-w-0 space-y-5">
<BrowserLink className="inline-flex items-center gap-1 text-sm" href={backHref}>
<ChevronLeftIcon aria-hidden="true" className="size-4" />
{t("assetDetail.back")}
</BrowserLink>
{c.inventory.isPending && <p role="status">{t("loading")}</p>}
<InventoryFeedback error={c.inventory.error ?? c.catalog.error} />
{(c.inventory.isError || c.catalog.isError) && (
<Button
onClick={() => {
Promise.all([c.inventory.refetch(), c.catalog.refetch()]).catch(() => undefined);
}}
variant="outline"
>
{t("retry")}
</Button>
)}
{asset && (
<>
<header className="flex flex-wrap items-start justify-between gap-4 rounded-xl border border-border/70 bg-card p-5 sm:p-6">
<div className="flex min-w-0 items-start gap-3">
<span className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<AssetTemplateIcon
className="size-6"
icon={template?.values.kind === "template" ? template.values.icon : undefined}
/>
</span>
<div className="min-w-0 space-y-2">
<h1 className="break-words text-2xl font-semibold">{asset.values.name}</h1>
<p className="text-sm text-muted-foreground">
{asset.specification.templateName} · {t(asset.displayStatus)}
</p>
</div>
</div>
<EntityRowActions
label={t("actions")}
primaryAction={{
label: t("editAsset"),
icon: <PencilIcon aria-hidden="true" />,
onClick: () => c.setEditing(true),
disabled: !c.catalog.isSuccess,
}}
>
<EntityAction
className="text-destructive data-highlighted:text-destructive"
onClick={() => c.setDeleting(true)}
>
<Trash2Icon aria-hidden="true" />
{t("deleteAsset")}
</EntityAction>
</EntityRowActions>
</header>
<AssetDetailContent
asset={asset}
assets={c.inventory.data?.assets ?? []}
lookups={lookups}
relationships={c.inventory.data?.relationships ?? []}
>
{relatedContent?.(asset)}
</AssetDetailContent>
{c.editing && c.catalog.data && (
<AssetEditorDialog
asset={asset}
companyId={companyId}
items={c.catalog.data.items}
lookups={lookups}
onClose={() => c.setEditing(false)}
onSaved={() => c.setEditing(false)}
/>
)}
<DeleteAssetDialog
asset={asset}
errorCode={c.remove.error instanceof Error ? c.remove.error.message : undefined}
onClose={() => c.setDeleting(false)}
onConfirm={async () => {
await c.remove.mutateAsync();
}}
open={c.deleting}
/>
</>
)}
</div>
);
}