// The catalog module's pages, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web // shell runs on the server under `////catalog`, and the view of what it returned. // // /catalog the organization's products, and a search // /catalog/imports the imports so far, and the form that applies one // /catalog/ one product // // Every read is a procedure through the same caller the API uses, and every label is translated in the // loader. A refusal that is the platform's -- the service is not enabled for the organization, the // deployment lacks a scope, the service did not answer -- is the page's own state, not an error page, in the // kit's words for it (`businessServiceNotices` of `@conusai-kit/business/refusals`). import { businessRefusalNotices, businessServiceNotices } from "@conusai-kit/business/refusals" import { type Container, notFound, type TransportCaller } from "@conusai-kit/core" import type { I18n, Locale } from "@conusai-kit/i18n" import { type ProjectPage, type ProjectPageViewProps, projectPage } from "@conusai-kit/ui" import { Can } from "@conusai-kit/ui/data" import { PageHeader, ServiceStatePage, serviceNotice } from "@conusai-kit/ui/patterns" import type { ReactElement } from "react" import { CATALOG_PERMISSIONS, type ImportView, importList, type ProductView, productGet, productList, } from "../contract/index.js" import { ImportForm, type ImportFormLabels } from "./import-form.js" import { ImportList, type ImportListLabels } from "./import-list.js" import { importFormLabels, importListLabels, productListLabels, type Translate } from "./labels.js" import { ProductList, type ProductListLabels } from "./product-list.js" /** A platform refusal as the page shows it: the problem's code and its text in the reader's language. */ export interface ServiceNotice { readonly code: string readonly text: string } type StatusNames = Readonly> export interface CatalogListData { readonly kind: "list" readonly title: string readonly intro: string readonly importsLink: string readonly list: ProductListLabels readonly statuses: StatusNames readonly products: { rows: ProductView[]; after?: string | undefined } } export interface CatalogProductData { readonly kind: "product" readonly title: string readonly back: string readonly product: ProductView readonly status: string readonly labels: { readonly code: string readonly unit: string readonly status: string readonly group: string readonly description: string readonly revision: string readonly identifiers: string readonly none: string } } export interface CatalogImportsData { readonly kind: "imports" readonly title: string readonly intro: string readonly back: string readonly form: ImportFormLabels readonly list: ImportListLabels readonly imports: readonly ImportView[] } /** The service cannot answer: the notice in place of the page. */ export interface CatalogStateData { readonly kind: "state" readonly title: string readonly notice: ServiceNotice } export type CatalogPageData = CatalogListData | CatalogProductData | CatalogImportsData | CatalogStateData export type CatalogPage = ProjectPage /** A product id as the service mints it: one path segment. */ const PRODUCT_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/ /** The product a path segment names; a segment that does not decode names none. */ function productIdOf(segment: string): string { try { return decodeURIComponent(segment) } catch { throw notFound("no such page") } } /** The notice for a platform refusal, or undefined for any other error (which the shell answers). */ export function serviceState(error: unknown, locale: Locale): ServiceNotice | undefined { return serviceNotice(error, businessServiceNotices(locale)) } const statusNames = (t: Translate): StatusNames => ({ active: t("statuses.active"), archived: t("statuses.archived"), deleted: t("statuses.deleted"), }) export const catalogPage: CatalogPage = projectPage({ async load({ slugs, segments, caller, i18n }): Promise { const t: Translate = (key, values) => i18n.message(`catalog.${key}`, values) const failed = i18n.message("app.failed") const refusals = businessRefusalNotices(i18n.locale) const title = t("title") const [segment, ...rest] = segments if (rest.length > 0) throw notFound("no such page") const productId = segment === undefined || segment === "imports" ? undefined : productIdOf(segment) if (productId !== undefined && !PRODUCT_ID.test(productId)) throw notFound("no such page") try { if (segment === "imports") { const { rows } = await caller.call(importList, slugs) return { kind: "imports", title: t("imports.title"), intro: t("imports.intro"), back: t("nav.products"), form: importFormLabels(t, failed, refusals), list: importListLabels(t, rows.length), imports: rows, } } if (productId === undefined) { return { kind: "list", title, intro: t("intro"), importsLink: t("nav.imports"), list: productListLabels(t, failed, refusals), statuses: statusNames(t), products: await caller.call(productList, slugs), } } const product = await caller.call(productGet, { ...slugs, productId }) return { kind: "product", title, back: t("product.back"), product, status: statusNames(t)[product.status], labels: { code: t("product.code"), unit: t("product.unit"), status: t("product.status"), group: t("product.group"), description: t("product.description"), revision: t("product.revision"), identifiers: t("product.identifiers"), none: t("product.none"), }, } } catch (error) { const notice = serviceState(error, i18n.locale) if (!notice) throw error return { kind: "state", title, notice } } }, View({ slugs, data }: ProjectPageViewProps): ReactElement { const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/catalog` return data.kind === "state" ? ( ) : ( <> {data.kind === "list" ? ( <>

{data.intro}

{data.importsLink}

) : data.kind === "imports" ? ( <>

{data.back}

{data.intro}

) : ( <>

{data.back}

{data.product.name}

{data.labels.code}
{data.product.code}
{data.labels.unit}
{data.product.baseUnit}
{data.labels.status}
{data.status}
{data.labels.revision}
{data.product.revision}
{data.product.groupCode === null ? null : ( <>
{data.labels.group}
{data.product.groupCode}
)} {data.product.description === "" ? null : ( <>
{data.labels.description}
{data.product.description}
)}
{data.labels.identifiers}
{data.product.identifiers.length === 0 ? ( data.labels.none ) : (
    {data.product.identifiers.map((identifier) => (
  • {identifier.kind}: {identifier.value}
  • ))}
)}
)} ) }, })