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