(props: {
domainsSection?: React.ReactNode;
// Cloud injects the plan-upgrade call to action (a link to billing/plans).
// When set and the org is at its seat limit, clicking "Invite member" opens
// an upgrade prompt instead of the invite form. Self-host has no seat limit,
// so it never reaches this and can omit it.
upgradeAction?: React.ReactNode;
// Cloud injects a destructive "delete organization" section here. It hangs
// off the cloud-only `/auth/delete-organization` endpoint (WorkOS + billing
// teardown), so self-host omits it. Rendered last, below members.
dangerZoneSection?: React.ReactNode;
})
| 134 | } |
| 135 | |
| 136 | export function OrgPage(props: { |
| 137 | domainsSection?: React.ReactNode; |
| 138 | // Cloud injects the plan-upgrade call to action (a link to billing/plans). |
| 139 | // When set and the org is at its seat limit, clicking "Invite member" opens |
| 140 | // an upgrade prompt instead of the invite form. Self-host has no seat limit, |
| 141 | // so it never reaches this and can omit it. |
| 142 | upgradeAction?: React.ReactNode; |
| 143 | // Cloud injects a destructive "delete organization" section here. It hangs |
| 144 | // off the cloud-only `/auth/delete-organization` endpoint (WorkOS + billing |
| 145 | // teardown), so self-host omits it. Rendered last, below members. |
| 146 | dangerZoneSection?: React.ReactNode; |
| 147 | }) { |
| 148 | useExecutorDocumentTitle("Organization"); |
| 149 | const auth = useAuth(); |
| 150 | const organizationName = |
| 151 | auth.status === "authenticated" ? (auth.organization?.name ?? "Organization") : "Organization"; |
| 152 | const membersResult = useAtomValue(orgMembersAtom); |
| 153 | const refreshMembers = useAtomRefresh(orgMembersAtom); |
| 154 | const rolesResult = useAtomValue(orgRolesAtom); |
| 155 | const doRemove = useAtomSet(removeMember, { mode: "promiseExit" }); |
| 156 | const doUpdateRole = useAtomSet(updateMemberRole, { mode: "promiseExit" }); |
| 157 | const doUpdateOrgName = useAtomSet(updateOrgName, { mode: "promiseExit" }); |
| 158 | const [inviteOpen, setInviteOpen] = useState(false); |
| 159 | const [upgradeOpen, setUpgradeOpen] = useState(false); |
| 160 | const [editName, setEditName] = useState(organizationName); |
| 161 | const [savingName, setSavingName] = useState(false); |
| 162 | const [search, setSearch] = useState(""); |
| 163 | |
| 164 | const roles = AsyncResult.match(rolesResult, { |
| 165 | onInitial: () => [] as readonly RoleData[], |
| 166 | onFailure: () => [] as readonly RoleData[], |
| 167 | onSuccess: ({ value }) => value.roles, |
| 168 | }); |
| 169 | |
| 170 | const seats = AsyncResult.match(membersResult, { |
| 171 | onInitial: () => undefined, |
| 172 | onFailure: () => undefined, |
| 173 | onSuccess: ({ value }) => value.seats, |
| 174 | }); |
| 175 | // At the plan's member-seat limit, "Invite member" opens an upgrade prompt |
| 176 | // (when cloud provided one) instead of the invite form, so the admin gets a |
| 177 | // clear next step rather than a refused invite. |
| 178 | const atSeatLimit = !!seats && !seats.unlimited && seats.used >= seats.granted; |
| 179 | const showUpgradeOnInvite = atSeatLimit && !!props.upgradeAction; |
| 180 | |
| 181 | const handleRemove = async (membershipId: string, name: string) => { |
| 182 | const exit = await doRemove({ |
| 183 | params: { membershipId }, |
| 184 | reactivityKeys: orgMemberWriteKeys, |
| 185 | }); |
| 186 | trackEvent("org_member_removed", { success: Exit.isSuccess(exit) }); |
| 187 | toast[Exit.isSuccess(exit) ? "success" : "error"]( |
| 188 | Exit.isSuccess(exit) ? `Removed ${name}` : "Failed to remove member", |
| 189 | ); |
| 190 | }; |
| 191 | |
| 192 | const handleChangeRole = async (membershipId: string, roleSlug: string, roleName: string) => { |
| 193 | const exit = await doUpdateRole({ |
nothing calls this directly
no test coverage detected