This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The loading state of this page (src/ui/loading-page.tsx says why it is here and not above the layouts).
|
||||
|
||||
export { LoadingPage as default } from "../../../src/ui/loading-page"
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user