@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
|
||||
// Adds a contact point (an e-mail address, a phone number, a web address) to a party. The service
|
||||
// checks the value against its kind and answers a malformed one in its own words.
|
||||
|
||||
import type { ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { describeError, useWrite } from "@conusai-kit/ui/data"
|
||||
import {
|
||||
CheckField,
|
||||
FormLayout,
|
||||
type FormValues,
|
||||
PageSection,
|
||||
SelectField,
|
||||
serviceNotice,
|
||||
TextField,
|
||||
usePageNavigation,
|
||||
} from "@conusai-kit/ui/patterns"
|
||||
import { FieldGroup, FieldLegend, FieldSet } from "@conusai-kit/ui/primitives"
|
||||
import { type ReactElement, useState } from "react"
|
||||
import { CONTACT_KINDS, CONTACT_PURPOSES } from "../contract/constants.js"
|
||||
|
||||
export interface ContactFormLabels {
|
||||
readonly heading: string
|
||||
readonly kind: string
|
||||
readonly value: string
|
||||
readonly purposes: string
|
||||
readonly primary: string
|
||||
readonly submit: string
|
||||
readonly busy: string
|
||||
readonly kinds: Readonly<Record<(typeof CONTACT_KINDS)[number], string>>
|
||||
readonly purposeNames: Readonly<Record<(typeof CONTACT_PURPOSES)[number], string>>
|
||||
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
|
||||
readonly refusals: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
const INPUT_TYPE = { email: "email", phone: "tel", web: "url" } as const
|
||||
|
||||
export function ContactForm({
|
||||
slugs,
|
||||
partyId,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
partyId: string
|
||||
labels: ContactFormLabels
|
||||
}): ReactElement {
|
||||
const navigation = usePageNavigation()
|
||||
const add = useWrite<Record<string, unknown>, unknown>("crm.contact.add")
|
||||
const [requestId, setRequestId] = useState(() => crypto.randomUUID())
|
||||
const [kind, setKind] = useState<(typeof CONTACT_KINDS)[number]>("email")
|
||||
|
||||
const submit = async (values: FormValues): Promise<void> => {
|
||||
await add.mutate({
|
||||
...slugs,
|
||||
partyId,
|
||||
kind,
|
||||
value: values.value,
|
||||
purposes: CONTACT_PURPOSES.filter((purpose) => values[`purpose-${purpose}`] === "on"),
|
||||
isPrimary: values.isPrimary === "on",
|
||||
requestId,
|
||||
})
|
||||
setRequestId(crypto.randomUUID())
|
||||
// The page is rendered on the server: a reload lists the new contact point.
|
||||
await navigation.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection title={labels.heading} variant="card">
|
||||
<FormLayout
|
||||
submitLabel={labels.submit}
|
||||
busyLabel={labels.busy}
|
||||
onSubmit={submit}
|
||||
describeError={(error) => serviceNotice(error, labels.refusals)?.text ?? describeError(error)}
|
||||
>
|
||||
<SelectField
|
||||
label={labels.kind}
|
||||
value={kind}
|
||||
onValueChange={(value) => setKind(CONTACT_KINDS.find((option) => option === value) ?? "email")}
|
||||
options={CONTACT_KINDS.map((option) => ({ value: option, label: labels.kinds[option] }))}
|
||||
/>
|
||||
<TextField name="value" label={labels.value} type={INPUT_TYPE[kind]} required maxLength={256} />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">{labels.purposes}</FieldLegend>
|
||||
<FieldGroup>
|
||||
{CONTACT_PURPOSES.map((purpose) => (
|
||||
<CheckField key={purpose} name={`purpose-${purpose}`} label={labels.purposeNames[purpose]} />
|
||||
))}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<CheckField name="isPrimary" label={labels.primary} />
|
||||
</FormLayout>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// A party's contact points. Part of a page's view (F10): it holds no state, and every string is passed in.
|
||||
|
||||
import { PageSection, type ResourceColumn, ResourceList } from "@conusai-kit/ui/patterns"
|
||||
import type { ReactElement } from "react"
|
||||
import type { ContactView } from "../contract/index.js"
|
||||
|
||||
export interface ContactsLabels {
|
||||
readonly title: string
|
||||
/** How many contact points, already filled with the plural form. */
|
||||
readonly count: string
|
||||
readonly caption: string
|
||||
readonly empty: string
|
||||
readonly kind: string
|
||||
readonly value: string
|
||||
readonly purposes: string
|
||||
readonly primary: string
|
||||
readonly erased: string
|
||||
readonly yes: string
|
||||
readonly no: string
|
||||
readonly kinds: Readonly<Record<ContactView["kind"], string>>
|
||||
readonly purposeNames: Readonly<Record<ContactView["purposes"][number], string>>
|
||||
}
|
||||
|
||||
export function Contacts({
|
||||
contacts,
|
||||
labels,
|
||||
}: {
|
||||
contacts: readonly ContactView[]
|
||||
labels: ContactsLabels
|
||||
}): ReactElement {
|
||||
const columns: ResourceColumn<ContactView>[] = [
|
||||
{ id: "kind", header: labels.kind, cell: (contact) => labels.kinds[contact.kind] },
|
||||
{
|
||||
id: "value",
|
||||
header: labels.value,
|
||||
wrap: true,
|
||||
cell: (contact) => (contact.erased ? labels.erased : contact.value),
|
||||
},
|
||||
{
|
||||
id: "purposes",
|
||||
header: labels.purposes,
|
||||
wrap: true,
|
||||
cell: (contact) => contact.purposes.map((purpose) => labels.purposeNames[purpose]).join(", "),
|
||||
},
|
||||
{ id: "isPrimary", header: labels.primary, cell: (contact) => (contact.isPrimary ? labels.yes : labels.no) },
|
||||
]
|
||||
return (
|
||||
<PageSection title={labels.title}>
|
||||
<ResourceList
|
||||
label={labels.caption}
|
||||
columns={columns}
|
||||
data={contacts}
|
||||
rowId={(contact) => contact.contactId}
|
||||
count={contacts.length === 0 ? undefined : labels.count}
|
||||
rowLabel={(contact) => labels.kinds[contact.kind]}
|
||||
empty={{ title: labels.empty }}
|
||||
/>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// The crm module's pages and components. `crmPage` is what the web shell mounts; the rest is exported
|
||||
// for an app that composes its own page from the parts.
|
||||
|
||||
export { ContactForm, type ContactFormLabels } from "./contact-form.js"
|
||||
export { Contacts, type ContactsLabels } from "./contacts.js"
|
||||
export {
|
||||
contactFormLabels,
|
||||
contactsLabels,
|
||||
partyFormLabels,
|
||||
partyListLabels,
|
||||
type Translate,
|
||||
} from "./labels.js"
|
||||
export { type CrmPage, crmPage } from "./pages.js"
|
||||
export { PartyForm, type PartyFormLabels } from "./party-form.js"
|
||||
export { PartyList, type PartyListLabels } from "./party-list.js"
|
||||
@@ -0,0 +1,90 @@
|
||||
// The labels each component takes, built from a message lookup. Every key this module's pages use is
|
||||
// named here or in pages.tsx, so `__tests__/i18n.test.ts` can run these builders against the
|
||||
// catalogues and fail on a key either language lacks.
|
||||
//
|
||||
// `t` takes a key relative to the module (`form.submit`); the page binds it to
|
||||
// `i18n.message("crm." + key)`. `refusals` is the kit's text for each business refusal in the reader's
|
||||
// language (`businessRefusalNotices`), which a form shows when the service refuses.
|
||||
|
||||
import { CONTACT_KINDS, CONTACT_PURPOSES } from "../contract/constants.js"
|
||||
import type { ContactFormLabels } from "./contact-form.js"
|
||||
import type { ContactsLabels } from "./contacts.js"
|
||||
import type { PartyFormLabels } from "./party-form.js"
|
||||
import type { PartyListLabels } from "./party-list.js"
|
||||
|
||||
export type Translate = (key: string, values?: Record<string, string | number>) => string
|
||||
export type Refusals = Readonly<Record<string, string>>
|
||||
|
||||
const ALL_KINDS = ["postal", "email", "phone", "web", "peppol_endpoint"] as const
|
||||
|
||||
const purposeNames = (t: Translate) =>
|
||||
Object.fromEntries(
|
||||
CONTACT_PURPOSES.map((purpose) => [purpose, t(`purposes.${purpose}`)]),
|
||||
) as ContactsLabels["purposeNames"]
|
||||
|
||||
export function partyFormLabels(t: Translate, refusals: Refusals): PartyFormLabels {
|
||||
return {
|
||||
heading: t("form.heading"),
|
||||
kind: t("form.kind"),
|
||||
person: t("kinds.person"),
|
||||
organization: t("kinds.organization"),
|
||||
displayName: t("form.displayName"),
|
||||
legalName: t("form.legalName"),
|
||||
country: t("form.country"),
|
||||
countryHint: t("form.countryHint"),
|
||||
linkSelf: t("form.linkSelf"),
|
||||
submit: t("form.submit"),
|
||||
busy: t("form.busy"),
|
||||
refusals,
|
||||
}
|
||||
}
|
||||
|
||||
export function partyListLabels(t: Translate): PartyListLabels {
|
||||
return {
|
||||
caption: t("parties.caption"),
|
||||
empty: t("parties.empty"),
|
||||
name: t("parties.name"),
|
||||
kind: t("parties.kind"),
|
||||
country: t("parties.country"),
|
||||
person: t("kinds.person"),
|
||||
organization: t("kinds.organization"),
|
||||
// The placeholder is the component's to fill, so it is passed through untouched.
|
||||
open: t("parties.open", { name: "{name}" }),
|
||||
search: t("parties.search"),
|
||||
}
|
||||
}
|
||||
|
||||
export function contactsLabels(t: Translate, count: number): ContactsLabels {
|
||||
return {
|
||||
title: t("contacts.title"),
|
||||
count: t("contacts.count", { count }),
|
||||
caption: t("contacts.caption"),
|
||||
empty: t("contacts.empty"),
|
||||
kind: t("contacts.kind"),
|
||||
value: t("contacts.value"),
|
||||
purposes: t("contacts.purposes"),
|
||||
primary: t("contacts.primary"),
|
||||
erased: t("contacts.erased"),
|
||||
yes: t("contacts.yes"),
|
||||
no: t("contacts.no"),
|
||||
kinds: Object.fromEntries(ALL_KINDS.map((kind) => [kind, t(`contactKinds.${kind}`)])) as ContactsLabels["kinds"],
|
||||
purposeNames: purposeNames(t),
|
||||
}
|
||||
}
|
||||
|
||||
export function contactFormLabels(t: Translate, refusals: Refusals): ContactFormLabels {
|
||||
return {
|
||||
heading: t("contactForm.heading"),
|
||||
kind: t("contactForm.kind"),
|
||||
value: t("contactForm.value"),
|
||||
purposes: t("contactForm.purposes"),
|
||||
primary: t("contactForm.primary"),
|
||||
submit: t("contactForm.submit"),
|
||||
busy: t("contactForm.busy"),
|
||||
kinds: Object.fromEntries(
|
||||
CONTACT_KINDS.map((kind) => [kind, t(`contactKinds.${kind}`)]),
|
||||
) as ContactFormLabels["kinds"],
|
||||
purposeNames: purposeNames(t),
|
||||
refusals,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// The crm module's pages, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web shell
|
||||
// runs on the server under `/<org>/<workspace>/<project>/crm`, and the view of what it returned.
|
||||
//
|
||||
// /crm the organization's parties, and the form that adds one
|
||||
// /crm/<partyId> one party: how to reach it, and the form that adds a contact point
|
||||
//
|
||||
// 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 } from "@conusai-kit/i18n"
|
||||
import { type ProjectPage, type ProjectPageViewProps, projectPage } from "@conusai-kit/ui"
|
||||
import { Can } from "@conusai-kit/ui/data"
|
||||
import {
|
||||
DetailLayout,
|
||||
Page,
|
||||
PageHeader,
|
||||
type ServiceNotice,
|
||||
ServiceStatePage,
|
||||
serviceNotice,
|
||||
} from "@conusai-kit/ui/patterns"
|
||||
import type { ReactElement } from "react"
|
||||
import { type ContactView, type PartyView, partyGet, partyList } from "../contract/index.js"
|
||||
import { ContactForm, type ContactFormLabels } from "./contact-form.js"
|
||||
import { Contacts, type ContactsLabels } from "./contacts.js"
|
||||
import { contactFormLabels, contactsLabels, partyFormLabels, partyListLabels, type Translate } from "./labels.js"
|
||||
import { PartyForm, type PartyFormLabels } from "./party-form.js"
|
||||
import { PartyList, type PartyListLabels } from "./party-list.js"
|
||||
|
||||
/** The parties, and the form that adds one. */
|
||||
export interface CrmListData {
|
||||
readonly kind: "list"
|
||||
readonly title: string
|
||||
readonly intro: string
|
||||
readonly form: PartyFormLabels
|
||||
readonly list: PartyListLabels
|
||||
readonly parties: { rows: PartyView[]; after?: string | undefined }
|
||||
}
|
||||
|
||||
/** One party, its kind and country already in a sentence. */
|
||||
export interface CrmPartyData {
|
||||
readonly kind: "party"
|
||||
readonly title: string
|
||||
readonly back: string
|
||||
readonly partyId: string
|
||||
readonly displayName: string
|
||||
readonly summary: string
|
||||
/** The note that the party is a member's account, when it is. */
|
||||
readonly linked?: string
|
||||
readonly contacts: readonly ContactView[]
|
||||
readonly contactLabels: ContactsLabels
|
||||
readonly form: ContactFormLabels
|
||||
}
|
||||
|
||||
/** The service cannot answer: the notice in place of the page. */
|
||||
export interface CrmStateData {
|
||||
readonly kind: "state"
|
||||
readonly title: string
|
||||
readonly notice: ServiceNotice
|
||||
}
|
||||
|
||||
export type CrmPageData = CrmListData | CrmPartyData | CrmStateData
|
||||
|
||||
export type CrmPage = ProjectPage<TransportCaller, I18n, Container, CrmPageData>
|
||||
|
||||
/** A party id as the service mints it: one path segment. */
|
||||
const PARTY_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/
|
||||
|
||||
/** The party a path segment names; a segment that does not decode names none. */
|
||||
function partyIdOf(segment: string): string {
|
||||
try {
|
||||
return decodeURIComponent(segment)
|
||||
} catch {
|
||||
throw notFound("no such page")
|
||||
}
|
||||
}
|
||||
|
||||
export const crmPage: CrmPage = projectPage({
|
||||
async load({ slugs, segments, caller, i18n }): Promise<CrmPageData> {
|
||||
const t: Translate = (key, values) => i18n.message(`crm.${key}`, values)
|
||||
const title = t("title")
|
||||
const refusals = businessRefusalNotices(i18n.locale)
|
||||
|
||||
const [segment, ...rest] = segments
|
||||
const partyId = segment === undefined ? undefined : partyIdOf(segment)
|
||||
if (rest.length > 0 || (partyId !== undefined && !PARTY_ID.test(partyId))) throw notFound("no such page")
|
||||
|
||||
try {
|
||||
if (partyId === undefined) {
|
||||
return {
|
||||
kind: "list",
|
||||
title,
|
||||
intro: t("intro"),
|
||||
form: partyFormLabels(t, refusals),
|
||||
list: partyListLabels(t),
|
||||
parties: await caller.call(partyList, slugs),
|
||||
}
|
||||
}
|
||||
const { party, contacts } = await caller.call(partyGet, { ...slugs, partyId })
|
||||
return {
|
||||
kind: "party",
|
||||
title,
|
||||
back: t("party.back"),
|
||||
partyId: party.partyId,
|
||||
displayName: party.displayName,
|
||||
summary: `${t(`kinds.${party.kind}`)}, ${party.country}${party.legalName ? `, ${party.legalName}` : ""}`,
|
||||
...(party.linked ? { linked: t("party.linked") } : {}),
|
||||
contacts,
|
||||
contactLabels: contactsLabels(t, contacts.length),
|
||||
form: contactFormLabels(t, refusals),
|
||||
}
|
||||
} catch (error) {
|
||||
const notice = serviceNotice(error, businessServiceNotices(i18n.locale))
|
||||
if (!notice) throw error
|
||||
return { kind: "state", title, notice }
|
||||
}
|
||||
},
|
||||
|
||||
View({ slugs, data }: ProjectPageViewProps<CrmPageData>): ReactElement {
|
||||
const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/crm`
|
||||
if (data.kind === "state") return <ServiceStatePage title={data.title} notice={data.notice} />
|
||||
if (data.kind === "list") {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title={data.title} description={data.intro} />
|
||||
<DetailLayout
|
||||
aside={
|
||||
<Can permission="crm.party.write">
|
||||
<PartyForm slugs={slugs} listHref={listHref} labels={data.form} />
|
||||
</Can>
|
||||
}
|
||||
>
|
||||
<PartyList slugs={slugs} seed={data.parties} listHref={listHref} labels={data.list} />
|
||||
</DetailLayout>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title={data.displayName} description={data.summary} breadcrumb={<a href={listHref}>{data.back}</a>}>
|
||||
{data.linked ? <span>{data.linked}</span> : null}
|
||||
</PageHeader>
|
||||
<DetailLayout
|
||||
aside={
|
||||
<Can permission="crm.party.write">
|
||||
<ContactForm slugs={slugs} partyId={data.partyId} labels={data.form} />
|
||||
</Can>
|
||||
}
|
||||
>
|
||||
<Contacts contacts={data.contacts} labels={data.contactLabels} />
|
||||
</DetailLayout>
|
||||
</Page>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
// Adds a party (a person or an organization), then opens it. "This is me" links a person to the
|
||||
// member's own account, so the platform's export or erasure of that member reaches the party.
|
||||
|
||||
import type { ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { describeError, useWrite } from "@conusai-kit/ui/data"
|
||||
import {
|
||||
CheckField,
|
||||
FormLayout,
|
||||
type FormValues,
|
||||
PageSection,
|
||||
SelectField,
|
||||
serviceNotice,
|
||||
TextField,
|
||||
usePageNavigation,
|
||||
} from "@conusai-kit/ui/patterns"
|
||||
import { type ReactElement, useState } from "react"
|
||||
|
||||
export interface PartyFormLabels {
|
||||
readonly heading: string
|
||||
readonly kind: string
|
||||
readonly person: string
|
||||
readonly organization: string
|
||||
readonly displayName: string
|
||||
readonly legalName: string
|
||||
readonly country: string
|
||||
readonly countryHint: string
|
||||
readonly linkSelf: string
|
||||
readonly submit: string
|
||||
readonly busy: string
|
||||
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
|
||||
readonly refusals: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
export function PartyForm({
|
||||
slugs,
|
||||
listHref,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
listHref: string
|
||||
labels: PartyFormLabels
|
||||
}): ReactElement {
|
||||
const navigation = usePageNavigation()
|
||||
const create = useWrite<Record<string, unknown>, { partyId: string }>("crm.party.create")
|
||||
const [requestId] = useState(() => crypto.randomUUID())
|
||||
const [kind, setKind] = useState("organization")
|
||||
|
||||
const submit = async ({ displayName, legalName, country = "", linkSelf }: FormValues): Promise<void> => {
|
||||
const created = await create.mutate({
|
||||
...slugs,
|
||||
kind,
|
||||
displayName,
|
||||
...(legalName ? { legalName } : {}),
|
||||
country: country.toUpperCase(),
|
||||
...(kind === "person" && linkSelf === "on" ? { linkSelf: true } : {}),
|
||||
requestId,
|
||||
})
|
||||
navigation.navigate(`${listHref}/${encodeURIComponent(created.partyId)}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection title={labels.heading}>
|
||||
<FormLayout
|
||||
submitLabel={labels.submit}
|
||||
busyLabel={labels.busy}
|
||||
onSubmit={submit}
|
||||
describeError={(error) => serviceNotice(error, labels.refusals)?.text ?? describeError(error)}
|
||||
>
|
||||
<SelectField
|
||||
label={labels.kind}
|
||||
value={kind}
|
||||
onValueChange={setKind}
|
||||
options={[
|
||||
{ value: "organization", label: labels.organization },
|
||||
{ value: "person", label: labels.person },
|
||||
]}
|
||||
/>
|
||||
<TextField name="displayName" label={labels.displayName} required maxLength={300} />
|
||||
<TextField name="legalName" label={labels.legalName} maxLength={300} />
|
||||
<TextField
|
||||
name="country"
|
||||
label={labels.country}
|
||||
description={labels.countryHint}
|
||||
errorMessage={labels.countryHint}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={2}
|
||||
pattern="[A-Za-z]{2}"
|
||||
/>
|
||||
{kind === "person" ? <CheckField name="linkSelf" label={labels.linkSelf} /> : null}
|
||||
</FormLayout>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
// The organization's parties, a page at a time. The server renders the first page (`seed`); a search
|
||||
// and "Show more" are reads made on a person's action, through the page's data client.
|
||||
|
||||
import type { ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { useDataClient } from "@conusai-kit/ui/data"
|
||||
import { type ResourceColumn, ResourceList, useResourceList } from "@conusai-kit/ui/patterns"
|
||||
import type { ReactElement } from "react"
|
||||
import type { PartyView } from "../contract/index.js"
|
||||
|
||||
export interface PartyListLabels {
|
||||
readonly caption: string
|
||||
readonly empty: string
|
||||
readonly name: string
|
||||
readonly kind: string
|
||||
readonly country: string
|
||||
readonly person: string
|
||||
readonly organization: string
|
||||
/** `Open {name}`: the link's accessible name. */
|
||||
readonly open: string
|
||||
readonly search: string
|
||||
}
|
||||
|
||||
type Page = { rows: PartyView[]; after?: string | undefined }
|
||||
|
||||
export function PartyList({
|
||||
slugs,
|
||||
seed,
|
||||
listHref,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
seed: Page
|
||||
listHref: string
|
||||
labels: PartyListLabels
|
||||
}): ReactElement {
|
||||
const client = useDataClient()
|
||||
const { list } = useResourceList<PartyView>({
|
||||
seed,
|
||||
rowId: (party) => party.partyId,
|
||||
searchLabel: labels.search,
|
||||
read: async ({ q, after }) => {
|
||||
const input = { ...slugs, ...(q ? { q } : {}), ...(after ? { after } : {}) }
|
||||
return (await client.call<Page>("crm.party.list", input)).value
|
||||
},
|
||||
})
|
||||
const columns: ResourceColumn<PartyView>[] = [
|
||||
{ id: "displayName", header: labels.name, wrap: true, minWidth: "10rem" },
|
||||
{
|
||||
id: "kind",
|
||||
header: labels.kind,
|
||||
cell: (party) => (party.kind === "person" ? labels.person : labels.organization),
|
||||
},
|
||||
{ id: "country", header: labels.country },
|
||||
]
|
||||
return (
|
||||
<ResourceList
|
||||
{...list}
|
||||
label={labels.caption}
|
||||
columns={columns}
|
||||
rowHref={(party) => `${listHref}/${encodeURIComponent(party.partyId)}`}
|
||||
rowLinkLabel={(party) => labels.open.replace("{name}", party.displayName)}
|
||||
rowLabel={(party) => party.displayName}
|
||||
empty={{ title: labels.empty }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user