114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// Members, their roles and the organization's open invitations (spec §3.2). Every change is a
|
|
// procedure call: the role scale, the "nobody grants a role above their own" rule and the last
|
|
// owner's protection are kit-tenancy's, not this page's. What this page decides is only who is shown
|
|
// the controls: administrators, who hold `tenancy.member.invite` and `tenancy.member.manage`.
|
|
|
|
import { invitationList, memberList, ORG_ROLES } from "@conusai-kit/tenancy"
|
|
import type { ReactElement } from "react"
|
|
import { orFallback } from "../../../src/server/guard"
|
|
import { holdsAtLeast, pageContext } from "../../../src/server/page"
|
|
import { InviteMember } from "../../../src/ui/invite-member"
|
|
import { ProcedureForm } from "../../../src/ui/procedure-form"
|
|
|
|
export const dynamic = "force-dynamic"
|
|
|
|
export default async function Members({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
|
|
const { org } = await params
|
|
const page = await pageContext()
|
|
const { caller, i18n, path } = page
|
|
const { members } = await orFallback(path, () => caller.call(memberList, { org }))
|
|
const administers = await holdsAtLeast(page, org, "admin")
|
|
const { invitations } = administers
|
|
? await orFallback(path, () => caller.call(invitationList, { org }))
|
|
: { invitations: [] }
|
|
const roleLabel = (role: string): string => i18n.message(`roles.${role}`)
|
|
const roleOptions = ORG_ROLES.map((role) => ({ value: role, label: roleLabel(role) }))
|
|
|
|
return (
|
|
<>
|
|
<h1>{i18n.message("app.members")}</h1>
|
|
<p>{i18n.message("app.memberCount", { count: members.length })}</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{i18n.message("app.name")}</th>
|
|
<th scope="col">{i18n.message("app.email")}</th>
|
|
<th scope="col">{i18n.message("app.role")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{members.map((member) => (
|
|
<tr key={member.userId}>
|
|
<td>{member.displayName}</td>
|
|
<td>{member.email}</td>
|
|
<td>{roleLabel(member.role)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
{administers ? (
|
|
<>
|
|
<h2>{i18n.message("app.invite")}</h2>
|
|
<InviteMember
|
|
org={org}
|
|
roles={roleOptions}
|
|
labels={{
|
|
email: i18n.message("app.email"),
|
|
role: i18n.message("app.role"),
|
|
submit: i18n.message("app.invite"),
|
|
created: i18n.message("members.created"),
|
|
link: i18n.message("members.link"),
|
|
failed: i18n.message("app.failed"),
|
|
}}
|
|
/>
|
|
|
|
<h2>{i18n.message("members.openInvitations")}</h2>
|
|
{invitations.length === 0 ? (
|
|
<p>{i18n.message("members.noInvitations")}</p>
|
|
) : (
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{i18n.message("app.email")}</th>
|
|
<th scope="col">{i18n.message("app.role")}</th>
|
|
<th scope="col">{i18n.message("members.invitedBy")}</th>
|
|
<th scope="col">{i18n.message("members.expires")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{invitations.map((invitation) => (
|
|
<tr key={invitation.invitationId}>
|
|
<td>{invitation.email}</td>
|
|
<td>{roleLabel(invitation.role)}</td>
|
|
<td>{invitation.inviter.displayName}</td>
|
|
<td>{i18n.date(new Date(invitation.expiresAt))}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
|
|
<h2>{i18n.message("members.changeRole")}</h2>
|
|
<ProcedureForm
|
|
procedure="tenancy.member.change-role"
|
|
hidden={{ org }}
|
|
submitLabel={i18n.message("app.save")}
|
|
failedTemplate={i18n.message("app.failed")}
|
|
fields={[
|
|
{
|
|
name: "userId",
|
|
label: i18n.message("app.members"),
|
|
required: true,
|
|
options: members.map((member) => ({ value: member.userId, label: member.email })),
|
|
},
|
|
{ name: "role", label: i18n.message("app.role"), required: true, options: roleOptions },
|
|
]}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</>
|
|
)
|
|
}
|