Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions .changeset/wicked-wings-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': minor
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

Disable role selection in `OrganizationProfile` during role set migration
9 changes: 8 additions & 1 deletion packages/clerk-js/src/core/resources/Organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,18 @@ export class Organization extends BaseResource implements OrganizationResource {
forceUpdateClient: true,
},
).then(res => {
const { data: roles, total_count } = res?.response as unknown as ClerkPaginatedResponse<RoleJSON>;
const {
data: roles,
total_count,
has_role_set_migration,
} = res?.response as unknown as ClerkPaginatedResponse<RoleJSON> & {
has_role_set_migration?: boolean;
};

return {
total_count,
data: roles.map(role => new Role(role)),
has_role_set_migration,
};
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const ActiveMembersList = ({ memberships, pageSize }: ActiveMembersListPr
const card = useCardState();
const { organization } = useOrganization();

const { options, isLoading: loadingRoles } = useFetchRoles();
const { options, isLoading: loadingRoles, hasRoleSetMigration } = useFetchRoles();

if (!organization) {
return null;
Expand Down Expand Up @@ -61,6 +61,7 @@ export const ActiveMembersList = ({ memberships, pageSize }: ActiveMembersListPr
options={options}
onRoleChange={handleRoleChange(m)}
onRemove={handleRemove(m)}
hasRoleSetMigration={hasRoleSetMigration}
/>
))}
/>
Expand All @@ -73,8 +74,9 @@ const MemberRow = (props: {
onRemove: () => unknown;
options: Parameters<typeof RoleSelect>[0]['roles'];
onRoleChange: (role: string) => unknown;
hasRoleSetMigration: boolean;
}) => {
const { membership, onRemove, onRoleChange, options } = props;
const { membership, onRemove, onRoleChange, options, hasRoleSetMigration } = props;
const { localizeCustomRole } = useLocalizeCustomRoles();
const card = useCardState();
const { user } = useUser();
Expand Down Expand Up @@ -112,7 +114,7 @@ const MemberRow = (props: {
}
>
<RoleSelect
isDisabled={card.isLoading || !onRoleChange}
isDisabled={card.isLoading || !onRoleChange || hasRoleSetMigration}
value={membership.role}
fallbackLabel={membership.roleName}
onChange={onRoleChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => {
};

const AsyncRoleSelect = (field: ReturnType<typeof useFormControl<'role'>>) => {
const { options, isLoading } = useFetchRoles();
const { options, isLoading, hasRoleSetMigration } = useFetchRoles();

const { t } = useLocalizations();

Expand All @@ -212,7 +212,7 @@ const AsyncRoleSelect = (field: ReturnType<typeof useFormControl<'role'>>) => {
<RoleSelect
{...field.props}
roles={options}
isDisabled={isLoading}
isDisabled={isLoading || hasRoleSetMigration}
onChange={value => field.setValue(value)}
triggerSx={t => ({ minWidth: t.sizes.$40, justifyContent: 'space-between', display: 'flex' })}
optionListSx={t => ({ minWidth: t.sizes.$48 })}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useOrganization } from '@clerk/shared/react';
import { useState } from 'react';

import { Alert } from '@/ui/elements/Alert';
import { Animated } from '@/ui/elements/Animated';
import { Card } from '@/ui/elements/Card';
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { Header } from '@/ui/elements/Header';
import { Tab, TabPanel, TabPanels, Tabs, TabsList } from '@/ui/elements/Tabs';
import { useFetchRoles } from '@/ui/hooks/useFetchRoles';

import { NotificationCountBadge, useProtect } from '../../common';
import { useEnvironment, useOrganizationProfileContext } from '../../contexts';
Expand All @@ -24,6 +26,7 @@ export const ACTIVE_MEMBERS_PAGE_SIZE = 10;
export const OrganizationMembers = withCardStateProvider(() => {
const { organizationSettings } = useEnvironment();
const card = useCardState();
const { hasRoleSetMigration } = useFetchRoles();
const canManageMemberships = useProtect({ permission: 'org:sys_memberships:manage' });
const canReadMemberships = useProtect({ permission: 'org:sys_memberships:read' });
const isDomainsEnabled = organizationSettings?.domains?.enabled && canManageMemberships;
Expand Down Expand Up @@ -142,6 +145,17 @@ export const OrganizationMembers = withCardStateProvider(() => {
/>
}
/>
{hasRoleSetMigration && (
<Alert
variant='warning'
title={localizationKeys(
'organizationProfile.membersPage.alerts.roleSetMigrationInProgress.title',
)}
subtitle={localizationKeys(
'organizationProfile.membersPage.alerts.roleSetMigrationInProgress.subtitle',
)}
/>
)}
<ActiveMembersList
pageSize={ACTIVE_MEMBERS_PAGE_SIZE}
memberships={memberships}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ClerkAPIResponseError } from '@clerk/shared/error';
import type { OrganizationInvitationResource } from '@clerk/shared/types';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';
Expand All @@ -21,6 +20,57 @@ describe('InviteMembersPage', () => {
clearFetchCache();
});

it('disables the role select when role set migration is in progress', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['[email protected]'],
organization_memberships: [{ name: 'Org1', role: 'admin' }],
});
});

fixtures.clerk.organization?.getInvitations.mockRejectedValue(null);
fixtures.clerk.organization?.getRoles.mockResolvedValue({
total_count: 2,
has_role_set_migration: true,
data: [
{
pathRoot: '',
reload: vi.fn(),
id: 'member',
key: 'member',
name: 'member',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: vi.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

const { getByRole } = render(
<Action.Root>
<InviteMembersScreen />
</Action.Root>,
{ wrapper },
);

await waitFor(() => {
expect(getByRole('button', { name: /select role/i })).toBeDisabled();
});
});

it('renders the component', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
Expand Down
Loading
Loading