ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+113
View File
@@ -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}
</>
)
}