Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-boats-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationMembers component.
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/common/Gate.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { IsAuthorized } from '@clerk/types';
import type { IsAuthorized, OrganizationPermission } from '@clerk/types';
import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
import React, { useEffect } from 'react';

import { useCoreSession } from '../contexts';
import { useFetch } from '../hooks';
import { useRouter } from '../router';

type GateParams = Parameters<IsAuthorized>[0];
type GateParams = Omit<Parameters<IsAuthorized>[0], 'permission'> & { permission: OrganizationPermission };
type GateProps = PropsWithChildren<
GateParams & {
fallback?: ReactNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MembershipRole, OrganizationMembershipResource } from '@clerk/types';

import { Gate } from '../../common/Gate';
import { useCoreOrganization, useCoreUser } from '../../contexts';
import { Badge, localizationKeys, Td, Text } from '../../customizables';
import { ThreeDotsMenu, useCardState, UserPreview } from '../../elements';
Expand All @@ -8,23 +9,10 @@ import { DataTable, RoleSelect, RowContainer } from './MemberListTable';

export const ActiveMembersList = () => {
const card = useCardState();
const {
organization,
membership: currentUserMembership,
memberships,
...rest
} = useCoreOrganization({
const { organization, memberships, ...rest } = useCoreOrganization({
memberships: true,
});

const { memberships: adminMembers } = useCoreOrganization({
memberships: {
role: ['admin'],
},
});

const isAdmin = currentUserMembership?.role === 'admin';

const mutateSwrState = () => {
const unstable__mutate = (rest as any).unstable__mutate;
if (unstable__mutate && typeof unstable__mutate === 'function') {
Expand All @@ -37,25 +25,17 @@ export const ActiveMembersList = () => {
}

const handleRoleChange = (membership: OrganizationMembershipResource) => (newRole: MembershipRole) => {
if (!isAdmin) {
return;
}
return card
.runAsync(async () => {
await membership.update({ role: newRole });
await (adminMembers as any).unstable__mutate?.();
})
.catch(err => handleError(err, [], card.setError));
};

const handleRemove = (membership: OrganizationMembershipResource) => () => {
if (!isAdmin) {
return;
}
return card
.runAsync(async () => {
const destroyedMembership = membership.destroy();
await (adminMembers as any).unstable__mutate?.();
const destroyedMembership = await membership.destroy();
return destroyedMembership;
})
.then(mutateSwrState)
Expand All @@ -82,27 +62,23 @@ export const ActiveMembersList = () => {
membership={m}
onRoleChange={handleRoleChange(m)}
onRemove={handleRemove(m)}
adminCount={adminMembers?.count || 0}
/>
))}
/>
);
};

// TODO: Find a way to disable Role select based on last admin by using permissions
const MemberRow = (props: {
membership: OrganizationMembershipResource;
onRemove: () => unknown;
adminCount: number;
onRoleChange?: (role: MembershipRole) => unknown;
}) => {
const { membership, onRemove, onRoleChange, adminCount } = props;
const { membership, onRemove, onRoleChange } = props;
const card = useCardState();
const { membership: currentUserMembership } = useCoreOrganization();
const user = useCoreUser();

const isAdmin = currentUserMembership?.role === 'admin';
const isCurrentUser = user.id === membership.publicUserData.userId;
const isLastAdmin = adminCount <= 1 && membership.role === 'admin';

return (
<RowContainer>
Expand All @@ -123,21 +99,24 @@ const MemberRow = (props: {
</Td>
<Td>{membership.createdAt.toLocaleDateString()}</Td>
<Td>
{isAdmin ? (
<Gate
permission={'org:sys_memberships:manage'}
fallback={
<Text
sx={t => ({ opacity: t.opacity.$inactive })}
localizationKey={roleLocalizationKey(membership.role)}
/>
}
>
<RoleSelect
isDisabled={card.isLoading || !onRoleChange || isLastAdmin}
isDisabled={card.isLoading || !onRoleChange}
value={membership.role}
onChange={onRoleChange}
/>
) : (
<Text
sx={t => ({ opacity: t.opacity.$inactive })}
localizationKey={roleLocalizationKey(membership.role)}
/>
)}
</Gate>
</Td>
<Td>
{isAdmin && (
<Gate permission={'org:sys_memberships:delete'}>
<ThreeDotsMenu
actions={[
{
Expand All @@ -149,7 +128,7 @@ const MemberRow = (props: {
]}
elementId={'member'}
/>
)}
</Gate>
</Td>
</RowContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, useGate } from '../../common';
import { useCoreOrganization, useEnvironment, useOrganizationProfileContext } from '../../contexts';
import { Col, descriptors, Flex, localizationKeys } from '../../customizables';
import {
Expand All @@ -21,15 +21,19 @@ import { OrganizationMembersTabRequests } from './OrganizationMembersTabRequests
export const OrganizationMembers = withCardStateProvider(() => {
const { organizationSettings } = useEnvironment();
const card = useCardState();
const { membership } = useCoreOrganization();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { isAuthorizedUser: canManageMemberships } = useGate({ permission: 'org:sys_memberships:manage' });
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
membershipRequests: isDomainsEnabled || undefined,
});
//@ts-expect-error

// @ts-expect-error
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();

if (canManageMemberships === null) {
Comment thread
chanioxaris marked this conversation as resolved.
return null;
}

return (
<Col
elementDescriptor={descriptors.page}
Expand All @@ -52,12 +56,12 @@ export const OrganizationMembers = withCardStateProvider(() => {
<Tabs>
<TabsList>
<Tab localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__members')} />
{isAdmin && (
{canManageMemberships && (
<Tab
localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__invitations')}
/>
)}
{allowRequests && (
{canManageMemberships && isDomainsEnabled && (
<Tab localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__requests')}>
<NotificationCountBadge notificationCount={membershipRequests?.count || 0} />
</Tab>
Expand All @@ -72,16 +76,16 @@ export const OrganizationMembers = withCardStateProvider(() => {
width: '100%',
}}
>
{isAdmin && __unstable_manageBillingUrl && <MembershipWidget />}
{canManageMemberships && __unstable_manageBillingUrl && <MembershipWidget />}
<ActiveMembersList />
</Flex>
</TabPanel>
{isAdmin && (
{canManageMemberships && (
Comment thread
chanioxaris marked this conversation as resolved.
<TabPanel sx={{ width: '100%' }}>
<OrganizationMembersTabInvitations />
</TabPanel>
)}
{allowRequests && (
{canManageMemberships && isDomainsEnabled && (
<TabPanel sx={{ width: '100%' }}>
<OrganizationMembersTabRequests />
</TabPanel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BlockButton } from '../../common';
import { useCoreOrganization, useEnvironment, useOrganizationProfileContext } from '../../contexts';
import { BlockButton, Gate } from '../../common';
import { useEnvironment, useOrganizationProfileContext } from '../../contexts';
import { Col, descriptors, Flex, Icon, localizationKeys } from '../../customizables';
import { Header, IconButton } from '../../elements';
import { UserAdd } from '../../icons';
Expand All @@ -11,17 +11,10 @@ import { MembershipWidget } from './MembershipWidget';
export const OrganizationMembersTabInvitations = () => {
const { organizationSettings } = useEnvironment();
const { navigate } = useRouter();
const { membership } = useCoreOrganization();
//@ts-expect-error
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();

const isAdmin = membership?.role === 'admin';

const allowDomains = organizationSettings?.domains?.enabled;

if (!isAdmin) {
return null;
}
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return (
<Col
Expand All @@ -32,43 +25,45 @@ export const OrganizationMembersTabInvitations = () => {
>
{__unstable_manageBillingUrl && <MembershipWidget />}

{allowDomains && (
<Col
gap={2}
sx={{
width: '100%',
}}
>
<Header.Root>
<Header.Title
localizationKey={localizationKeys(
'organizationProfile.membersPage.invitationsTab.autoInvitations.headerTitle',
)}
textVariant='largeMedium'
/>
<Header.Subtitle
localizationKey={localizationKeys(
'organizationProfile.membersPage.invitationsTab.autoInvitations.headerSubtitle',
)}
variant='regularRegular'
/>
</Header.Root>
<DomainList
fallback={
<BlockButton
colorScheme='primary'
textLocalizationKey={localizationKeys(
'organizationProfile.membersPage.invitationsTab.autoInvitations.primaryButton',
{isDomainsEnabled && (
<Gate permission={'org:sys_domains:manage'}>
<Col
gap={2}
sx={{
width: '100%',
}}
>
<Header.Root>
<Header.Title
localizationKey={localizationKeys(
'organizationProfile.membersPage.invitationsTab.autoInvitations.headerTitle',
)}
id='manageVerifiedDomains'
onClick={() => navigate('organization-settings/domain')}
textVariant='largeMedium'
/>
}
redirectSubPath={'organization-settings/domain/'}
verificationStatus={'verified'}
enrollmentMode={'automatic_invitation'}
/>
</Col>
<Header.Subtitle
localizationKey={localizationKeys(
'organizationProfile.membersPage.invitationsTab.autoInvitations.headerSubtitle',
)}
variant='regularRegular'
/>
</Header.Root>
<DomainList
fallback={
<BlockButton
colorScheme='primary'
textLocalizationKey={localizationKeys(
'organizationProfile.membersPage.invitationsTab.autoInvitations.primaryButton',
)}
id='manageVerifiedDomains'
onClick={() => navigate('organization-settings/domain')}
/>
}
redirectSubPath={'organization-settings/domain/'}
verificationStatus={'verified'}
enrollmentMode={'automatic_invitation'}
/>
</Col>
</Gate>
)}

<Flex
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BlockButton } from '../../common';
import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
import { useOrganizationProfileContext } from '../../contexts';
import { Col, Flex, localizationKeys } from '../../customizables';
import { Header } from '../../elements';
import { useRouter } from '../../router';
Expand All @@ -9,15 +9,9 @@ import { RequestToJoinList } from './RequestToJoinList';

export const OrganizationMembersTabRequests = () => {
const { navigate } = useRouter();
const { membership } = useCoreOrganization();
//@ts-expect-error
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();

const isAdmin = membership?.role === 'admin';

if (!isAdmin) {
Comment thread
chanioxaris marked this conversation as resolved.
return null;
}
return (
<Col
gap={8}
Expand Down
Loading