Skip to content

Commit 920c9e1

Browse files
authored
fix(clerk-js): Hide Add domain button when user is missing org:sys_domains:manage (#2240)
1 parent 9868084 commit 920c9e1

File tree

5 files changed

+120
-55
lines changed

5 files changed

+120
-55
lines changed

.changeset/strong-cows-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Hide "Add domain" button inside `<OrganizationProfile/>` when user is missing the `org:sys_domains:manage` permission.

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationSettings.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ const OrganizationDomainsSection = () => {
8989
>
9090
<DomainList redirectSubPath={'domain'} />
9191

92-
<AddBlockButton
93-
textLocalizationKey={localizationKeys('organizationProfile.profilePage.domainSection.primaryButton')}
94-
id='addOrganizationDomain'
95-
onClick={() => navigate('domain')}
96-
/>
92+
<Gate permission='org:sys_domains:manage'>
93+
<AddBlockButton
94+
textLocalizationKey={localizationKeys('organizationProfile.profilePage.domainSection.primaryButton')}
95+
id='addOrganizationDomain'
96+
onClick={() => navigate('domain')}
97+
/>
98+
</Gate>
9799
</ProfileSection>
98100
);
99101
};

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { OrganizationDomainResource, OrganizationMembershipResource } from '@clerk/types';
1+
import type { ClerkPaginatedResponse, OrganizationDomainResource, OrganizationMembershipResource } from '@clerk/types';
22
import { describe, it } from '@jest/globals';
33
import userEvent from '@testing-library/user-event';
44

@@ -30,12 +30,14 @@ describe('OrganizationSettings', () => {
3030
);
3131

3232
const { getByText } = render(<OrganizationSettings />, { wrapper });
33+
3334
await waitFor(() => {
3435
expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled();
35-
expect(getByText('Settings')).toBeDefined();
36-
expect(getByText('Org1', { exact: false }).closest('button')).not.toBeNull();
37-
expect(getByText(/leave organization/i, { exact: false }).closest('button')).toHaveAttribute('disabled');
3836
});
37+
38+
expect(getByText('Settings')).toBeDefined();
39+
expect(getByText('Org1', { exact: false }).closest('button')).not.toBeNull();
40+
expect(getByText(/leave organization/i, { exact: false }).closest('button')).toHaveAttribute('disabled');
3941
});
4042

4143
it('enables organization profile button and enables leave when user is admin and there is more', async () => {
@@ -63,7 +65,10 @@ describe('OrganizationSettings', () => {
6365
});
6466

6567
it.skip('disables organization profile button and enables leave when user is not admin', async () => {
66-
const adminsList: OrganizationMembershipResource[] = [createFakeMember({ id: '1', orgId: '1', role: 'admin' })];
68+
const adminsList: ClerkPaginatedResponse<OrganizationMembershipResource> = {
69+
data: [createFakeMember({ id: '1', orgId: '1', role: 'admin' })],
70+
total_count: 1,
71+
};
6772

6873
const { wrapper, fixtures } = await createFixtures(f => {
6974
f.withOrganizations();
@@ -98,7 +103,7 @@ describe('OrganizationSettings', () => {
98103
expect(fixtures.clerk.organization?.getDomains).not.toBeCalled();
99104
});
100105

101-
it('shows domains when `read` permission exists', async () => {
106+
it('shows domains when `read` permission exists but hides the Add domain button', async () => {
102107
const { wrapper, fixtures } = await createFixtures(f => {
103108
f.withOrganizations();
104109
f.withOrganizationDomains();
@@ -117,6 +122,30 @@ describe('OrganizationSettings', () => {
117122

118123
await new Promise(r => setTimeout(r, 100));
119124
expect(queryByText('Verified domains')).toBeInTheDocument();
125+
expect(queryByText('Add domain')).not.toBeInTheDocument();
126+
expect(fixtures.clerk.organization?.getDomains).toBeCalled();
127+
});
128+
129+
it('shows domains and shows the Add domain button when `org:sys_domains:manage` exists', async () => {
130+
const { wrapper, fixtures } = await createFixtures(f => {
131+
f.withOrganizations();
132+
f.withOrganizationDomains();
133+
f.withUser({
134+
email_addresses: ['[email protected]'],
135+
organization_memberships: [{ name: 'Org1', permissions: ['org:sys_domains:read', 'org:sys_domains:manage'] }],
136+
});
137+
});
138+
fixtures.clerk.organization?.getDomains.mockReturnValue(
139+
Promise.resolve({
140+
data: [],
141+
total_count: 0,
142+
}),
143+
);
144+
const { queryByText } = await act(() => render(<OrganizationSettings />, { wrapper }));
145+
146+
await new Promise(r => setTimeout(r, 100));
147+
expect(queryByText('Verified domains')).toBeInTheDocument();
148+
expect(queryByText('Add domain')).toBeInTheDocument();
120149
expect(fixtures.clerk.organization?.getDomains).toBeCalled();
121150
});
122151

@@ -156,18 +185,21 @@ describe('OrganizationSettings', () => {
156185
});
157186

158187
it.skip('disabled leave organization button with delete organization button', async () => {
159-
const adminsList: OrganizationMembershipResource[] = [
160-
createFakeMember({
161-
id: '1',
162-
orgId: '1',
163-
role: 'admin',
164-
}),
165-
createFakeMember({
166-
id: '2',
167-
orgId: '1',
168-
role: 'admin',
169-
}),
170-
];
188+
const adminsList: ClerkPaginatedResponse<OrganizationMembershipResource> = {
189+
data: [
190+
createFakeMember({
191+
id: '1',
192+
orgId: '1',
193+
role: 'admin',
194+
}),
195+
createFakeMember({
196+
id: '2',
197+
orgId: '1',
198+
role: 'admin',
199+
}),
200+
],
201+
total_count: 2,
202+
};
171203

172204
const { wrapper, fixtures } = await createFixtures(f => {
173205
f.withOrganizations();
@@ -212,21 +244,18 @@ describe('OrganizationSettings', () => {
212244
expect(fixtures.router.navigate).toHaveBeenCalledWith('profile');
213245
});
214246

215-
it('navigates to Leave Organization page when clicking on the respective button and user is not admin', async () => {
216-
const adminsList: OrganizationMembershipResource[] = [createFakeMember({ id: '1', orgId: '1', role: 'admin' })];
217-
247+
// TODO(@panteliselef): Update this test to allow user to leave an org, only if there will be at least one person left with the minimum set of permissions
248+
it('navigates to Leave Organization page when clicking on the respective button', async () => {
218249
const { wrapper, fixtures } = await createFixtures(f => {
219250
f.withOrganizations();
220251
f.withUser({
221252
email_addresses: ['[email protected]'],
222-
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
253+
organization_memberships: [{ name: 'Org1', permissions: [] }],
223254
});
224255
});
225256

226-
fixtures.clerk.organization?.getMemberships.mockReturnValue(Promise.resolve(adminsList));
227257
const { findByText } = render(<OrganizationSettings />, { wrapper });
228258
await waitFor(async () => {
229-
// expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled();
230259
await userEvent.click(await findByText(/leave organization/i, { exact: false }));
231260
});
232261
expect(fixtures.router.navigate).toHaveBeenCalledWith('leave');

packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationSettings.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ const OrganizationDomainsSection = () => {
8989
>
9090
<DomainList redirectSubPath={'domain'} />
9191

92-
<AddBlockButton
93-
textLocalizationKey={localizationKeys('organizationProfile.profilePage.domainSection.primaryButton')}
94-
id='addOrganizationDomain'
95-
onClick={() => navigate('domain')}
96-
/>
92+
<Gate permission='org:sys_domains:manage'>
93+
<AddBlockButton
94+
textLocalizationKey={localizationKeys('organizationProfile.profilePage.domainSection.primaryButton')}
95+
id='addOrganizationDomain'
96+
onClick={() => navigate('domain')}
97+
/>
98+
</Gate>
9799
</ProfileSection>
98100
);
99101
};

packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { OrganizationDomainResource, OrganizationMembershipResource } from '@clerk/types';
1+
import type { ClerkPaginatedResponse, OrganizationDomainResource, OrganizationMembershipResource } from '@clerk/types';
22
import { describe, it } from '@jest/globals';
33
import userEvent from '@testing-library/user-event';
44
import React from 'react';
@@ -66,7 +66,10 @@ describe('OrganizationSettings', () => {
6666
});
6767

6868
it.skip('disables organization profile button and enables leave when user is not admin', async () => {
69-
const adminsList: OrganizationMembershipResource[] = [createFakeMember({ id: '1', orgId: '1', role: 'admin' })];
69+
const adminsList: ClerkPaginatedResponse<OrganizationMembershipResource> = {
70+
data: [createFakeMember({ id: '1', orgId: '1', role: 'admin' })],
71+
total_count: 1,
72+
};
7073

7174
const { wrapper, fixtures } = await createFixtures(f => {
7275
f.withOrganizations();
@@ -101,7 +104,7 @@ describe('OrganizationSettings', () => {
101104
expect(fixtures.clerk.organization?.getDomains).not.toBeCalled();
102105
});
103106

104-
it('shows domains when `read` permission exists', async () => {
107+
it('shows domains when `read` permission exists but hides the Add domain button', async () => {
105108
const { wrapper, fixtures } = await createFixtures(f => {
106109
f.withOrganizations();
107110
f.withOrganizationDomains();
@@ -120,6 +123,30 @@ describe('OrganizationSettings', () => {
120123

121124
await new Promise(r => setTimeout(r, 100));
122125
expect(queryByText('Verified domains')).toBeInTheDocument();
126+
expect(queryByText('Add domain')).not.toBeInTheDocument();
127+
expect(fixtures.clerk.organization?.getDomains).toBeCalled();
128+
});
129+
130+
it('shows domains and shows the Add domain button when `org:sys_domains:manage` exists', async () => {
131+
const { wrapper, fixtures } = await createFixtures(f => {
132+
f.withOrganizations();
133+
f.withOrganizationDomains();
134+
f.withUser({
135+
email_addresses: ['[email protected]'],
136+
organization_memberships: [{ name: 'Org1', permissions: ['org:sys_domains:read', 'org:sys_domains:manage'] }],
137+
});
138+
});
139+
fixtures.clerk.organization?.getDomains.mockReturnValue(
140+
Promise.resolve({
141+
data: [],
142+
total_count: 0,
143+
}),
144+
);
145+
const { queryByText } = await act(() => render(<OrganizationSettings />, { wrapper }));
146+
147+
await new Promise(r => setTimeout(r, 100));
148+
expect(queryByText('Verified domains')).toBeInTheDocument();
149+
expect(queryByText('Add domain')).toBeInTheDocument();
123150
expect(fixtures.clerk.organization?.getDomains).toBeCalled();
124151
});
125152

@@ -159,18 +186,21 @@ describe('OrganizationSettings', () => {
159186
});
160187

161188
it.skip('disabled leave organization button with delete organization button', async () => {
162-
const adminsList: OrganizationMembershipResource[] = [
163-
createFakeMember({
164-
id: '1',
165-
orgId: '1',
166-
role: 'admin',
167-
}),
168-
createFakeMember({
169-
id: '2',
170-
orgId: '1',
171-
role: 'admin',
172-
}),
173-
];
189+
const adminsList: ClerkPaginatedResponse<OrganizationMembershipResource> = {
190+
data: [
191+
createFakeMember({
192+
id: '1',
193+
orgId: '1',
194+
role: 'admin',
195+
}),
196+
createFakeMember({
197+
id: '2',
198+
orgId: '1',
199+
role: 'admin',
200+
}),
201+
],
202+
total_count: 2,
203+
};
174204

175205
const { wrapper, fixtures } = await createFixtures(f => {
176206
f.withOrganizations();
@@ -215,21 +245,18 @@ describe('OrganizationSettings', () => {
215245
expect(fixtures.router.navigate).toHaveBeenCalledWith('profile');
216246
});
217247

218-
it('navigates to Leave Organization page when clicking on the respective button and user is not admin', async () => {
219-
const adminsList: OrganizationMembershipResource[] = [createFakeMember({ id: '1', orgId: '1', role: 'admin' })];
220-
248+
// TODO(@panteliselef): Update this test to allow user to leave an org, only if there will be at least one person left with the minimum set of permissions
249+
it('navigates to Leave Organization page when clicking on the respective button', async () => {
221250
const { wrapper, fixtures } = await createFixtures(f => {
222251
f.withOrganizations();
223252
f.withUser({
224253
email_addresses: ['[email protected]'],
225-
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
254+
organization_memberships: [{ name: 'Org1', permissions: [] }],
226255
});
227256
});
228257

229-
fixtures.clerk.organization?.getMemberships.mockReturnValue(Promise.resolve(adminsList));
230258
const { findByText } = render(<OrganizationSettings />, { wrapper });
231259
await waitFor(async () => {
232-
// expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled();
233260
await userEvent.click(await findByText(/leave organization/i, { exact: false }));
234261
});
235262
expect(fixtures.router.navigate).toHaveBeenCalledWith('leave');

0 commit comments

Comments
 (0)