Skip to content

Commit fba4781

Browse files
authored
feat(clerk-js,types): Display organization slug based on environment settings (#6903)
1 parent 67bd037 commit fba4781

File tree

11 files changed

+271
-40
lines changed

11 files changed

+271
-40
lines changed

.changeset/dull-paws-march.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/types': patch
4+
---
5+
6+
Display organization slug based on environment settings

packages/clerk-js/src/core/resources/OrganizationSettings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe
1818
enrollmentModes: [],
1919
defaultRole: null,
2020
};
21+
slug: {
22+
disabled: boolean;
23+
} = {
24+
disabled: false,
25+
};
2126
enabled: boolean = false;
2227
maxAllowedMemberships: number = 1;
2328
forceOrganizationSelection!: boolean;
@@ -42,6 +47,10 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe
4247
this.domains.defaultRole = this.withDefault(data.domains.default_role, this.domains.defaultRole);
4348
}
4449

50+
if (data.slug) {
51+
this.slug.disabled = this.withDefault(data.slug.disabled, this.slug.disabled);
52+
}
53+
4554
this.enabled = this.withDefault(data.enabled, this.enabled);
4655
this.maxAllowedMemberships = this.withDefault(data.max_allowed_memberships, this.maxAllowedMemberships);
4756
this.forceOrganizationSelection = this.withDefault(

packages/clerk-js/src/test/fixture-helpers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,22 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON)
342342
const withForceOrganizationSelection = () => {
343343
os.force_organization_selection = true;
344344
};
345+
const withOrganizationSlug = (enabled = false) => {
346+
os.slug.disabled = !enabled;
347+
};
345348

346349
const withOrganizationDomains = (modes?: OrganizationEnrollmentMode[], defaultRole?: string) => {
347350
os.domains.enabled = true;
348351
os.domains.enrollment_modes = modes || ['automatic_invitation', 'manual_invitation'];
349352
os.domains.default_role = defaultRole ?? null;
350353
};
351-
return { withOrganizations, withMaxAllowedMemberships, withOrganizationDomains, withForceOrganizationSelection };
354+
return {
355+
withOrganizations,
356+
withMaxAllowedMemberships,
357+
withOrganizationDomains,
358+
withForceOrganizationSelection,
359+
withOrganizationSlug,
360+
};
352361
};
353362

354363
const createBillingSettingsFixtureHelpers = (environment: EnvironmentJSON) => {

packages/clerk-js/src/test/fixtures.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ const createBaseOrganizationSettings = (): OrganizationSettingsJSON => {
8989
enabled: false,
9090
enrollment_modes: [],
9191
},
92+
slug: {
93+
disabled: true,
94+
},
9295
} as unknown as OrganizationSettingsJSON;
9396
};
9497

packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useOrganization, useOrganizationList } from '@clerk/shared/react';
22
import type { CreateOrganizationParams, OrganizationResource } from '@clerk/types';
33
import React from 'react';
44

5+
import { useEnvironment } from '@/ui/contexts';
56
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
67
import { Form } from '@/ui/elements/Form';
78
import { FormButtonContainer } from '@/ui/elements/FormButtons';
@@ -33,6 +34,11 @@ type CreateOrganizationFormProps = {
3334
headerTitle?: LocalizationKey;
3435
headerSubtitle?: LocalizationKey;
3536
};
37+
/**
38+
* @deprecated
39+
* This prop will be removed in a future version.
40+
* Configure whether organization slug is enabled via the Clerk Dashboard under Organization Settings.
41+
*/
3642
hideSlug?: boolean;
3743
};
3844

@@ -45,6 +51,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
4551
userMemberships: organizationListParams.userMemberships,
4652
});
4753
const { organization } = useOrganization();
54+
const { organizationSettings } = useEnvironment();
4855
const [file, setFile] = React.useState<File | null>();
4956

5057
const nameField = useFormControl('name', '', {
@@ -62,6 +69,9 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
6269
const dataChanged = !!nameField.value;
6370
const canSubmit = dataChanged;
6471

72+
// Environment setting takes precedence over prop
73+
const organizationSlugEnabled = !organizationSettings.slug.disabled && !props.hideSlug;
74+
6575
const onSubmit = async (e: React.FormEvent) => {
6676
e.preventDefault();
6777
if (!canSubmit) {
@@ -75,7 +85,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
7585
try {
7686
const createOrgParams: CreateOrganizationParams = { name: nameField.value };
7787

78-
if (!props.hideSlug) {
88+
if (organizationSlugEnabled) {
7989
createOrgParams.slug = slugField.value;
8090
}
8191

@@ -188,7 +198,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
188198
ignorePasswordManager
189199
/>
190200
</Form.ControlRow>
191-
{!props.hideSlug && (
201+
{organizationSlugEnabled && (
192202
<Form.ControlRow elementId={slugField.id}>
193203
<Form.PlainInput
194204
{...slugField.props}

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

Lines changed: 155 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -80,35 +80,169 @@ describe('CreateOrganization', () => {
8080
expect(getByRole('heading', { name: 'Create organization', level: 1 })).toBeInTheDocument();
8181
});
8282

83-
it('renders component without slug field', async () => {
84-
const { wrapper, fixtures, props } = await createFixtures(f => {
85-
f.withOrganizations();
86-
f.withUser({
87-
email_addresses: ['[email protected]'],
83+
describe('with `hideSlug` prop', () => {
84+
it('renders component without slug field', async () => {
85+
const { wrapper, fixtures, props } = await createFixtures(f => {
86+
f.withOrganizations();
87+
f.withUser({
88+
email_addresses: ['[email protected]'],
89+
});
90+
});
91+
92+
fixtures.clerk.createOrganization.mockReturnValue(
93+
Promise.resolve(
94+
getCreatedOrg({
95+
maxAllowedMemberships: 1,
96+
slug: 'new-org-1722578361',
97+
}),
98+
),
99+
);
100+
101+
props.setProps({ hideSlug: true });
102+
const { userEvent, getByRole, queryByText, queryByLabelText, getByLabelText } = render(<CreateOrganization />, {
103+
wrapper,
104+
});
105+
106+
expect(queryByLabelText(/Slug/i)).not.toBeInTheDocument();
107+
108+
await userEvent.type(getByLabelText(/Name/i), 'new org');
109+
await userEvent.click(getByRole('button', { name: /create organization/i }));
110+
111+
await waitFor(() => {
112+
expect(queryByText(/Invite new members/i)).toBeInTheDocument();
88113
});
89114
});
115+
});
90116

91-
fixtures.clerk.createOrganization.mockReturnValue(
92-
Promise.resolve(
93-
getCreatedOrg({
94-
maxAllowedMemberships: 1,
95-
slug: 'new-org-1722578361',
96-
}),
97-
),
98-
);
117+
describe('with organization slug configured on environment', () => {
118+
it('when disabled, renders component without slug field', async () => {
119+
const { wrapper, fixtures } = await createFixtures(f => {
120+
f.withOrganizations();
121+
f.withOrganizationSlug(false);
122+
f.withUser({
123+
email_addresses: ['[email protected]'],
124+
});
125+
});
99126

100-
props.setProps({ hideSlug: true });
101-
const { userEvent, getByRole, queryByText, queryByLabelText, getByLabelText } = render(<CreateOrganization />, {
102-
wrapper,
127+
fixtures.clerk.createOrganization.mockReturnValue(
128+
Promise.resolve(
129+
getCreatedOrg({
130+
maxAllowedMemberships: 1,
131+
slug: 'new-org-1722578361',
132+
}),
133+
),
134+
);
135+
136+
const { userEvent, getByRole, queryByText, queryByLabelText, getByLabelText } = render(<CreateOrganization />, {
137+
wrapper,
138+
});
139+
140+
expect(queryByLabelText(/Slug/i)).not.toBeInTheDocument();
141+
142+
await userEvent.type(getByLabelText(/Name/i), 'new org');
143+
await userEvent.click(getByRole('button', { name: /create organization/i }));
144+
145+
await waitFor(() => {
146+
expect(queryByText(/Invite new members/i)).toBeInTheDocument();
147+
});
148+
});
149+
150+
it('when enabled, renders component slug field', async () => {
151+
const { wrapper, fixtures } = await createFixtures(f => {
152+
f.withOrganizations();
153+
f.withOrganizationSlug(true);
154+
f.withUser({
155+
email_addresses: ['[email protected]'],
156+
});
157+
});
158+
159+
fixtures.clerk.createOrganization.mockReturnValue(
160+
Promise.resolve(
161+
getCreatedOrg({
162+
maxAllowedMemberships: 1,
163+
slug: 'new-org-1722578361',
164+
}),
165+
),
166+
);
167+
168+
const { userEvent, getByRole, queryByText, queryByLabelText, getByLabelText } = render(<CreateOrganization />, {
169+
wrapper,
170+
});
171+
172+
expect(queryByLabelText(/Slug/i)).toBeInTheDocument();
173+
174+
await userEvent.type(getByLabelText(/Name/i), 'new org');
175+
await userEvent.click(getByRole('button', { name: /create organization/i }));
176+
177+
await waitFor(() => {
178+
expect(queryByText(/Invite new members/i)).toBeInTheDocument();
179+
});
103180
});
104181

105-
expect(queryByLabelText(/Slug/i)).not.toBeInTheDocument();
182+
it('when enabled and `hideSlug` prop is passed, renders component without slug field', async () => {
183+
const { wrapper, fixtures, props } = await createFixtures(f => {
184+
f.withOrganizations();
185+
f.withOrganizationSlug(true);
186+
f.withUser({
187+
email_addresses: ['[email protected]'],
188+
});
189+
});
106190

107-
await userEvent.type(getByLabelText(/Name/i), 'new org');
108-
await userEvent.click(getByRole('button', { name: /create organization/i }));
191+
fixtures.clerk.createOrganization.mockReturnValue(
192+
Promise.resolve(
193+
getCreatedOrg({
194+
maxAllowedMemberships: 1,
195+
slug: 'new-org-1722578361',
196+
}),
197+
),
198+
);
199+
200+
props.setProps({ hideSlug: true });
201+
const { userEvent, getByRole, queryByText, queryByLabelText, getByLabelText } = render(<CreateOrganization />, {
202+
wrapper,
203+
});
109204

110-
await waitFor(() => {
111-
expect(queryByText(/Invite new members/i)).toBeInTheDocument();
205+
expect(queryByLabelText(/Slug/i)).not.toBeInTheDocument();
206+
207+
await userEvent.type(getByLabelText(/Name/i), 'new org');
208+
await userEvent.click(getByRole('button', { name: /create organization/i }));
209+
210+
await waitFor(() => {
211+
expect(queryByText(/Invite new members/i)).toBeInTheDocument();
212+
});
213+
});
214+
215+
it('when disabled and `hideSlug` prop is passed, renders component without slug field', async () => {
216+
const { wrapper, fixtures, props } = await createFixtures(f => {
217+
f.withOrganizations();
218+
f.withOrganizationSlug(false); // Environment disables slug
219+
f.withUser({
220+
email_addresses: ['[email protected]'],
221+
});
222+
});
223+
224+
fixtures.clerk.createOrganization.mockReturnValue(
225+
Promise.resolve(
226+
getCreatedOrg({
227+
maxAllowedMemberships: 1,
228+
slug: 'new-org-1722578361',
229+
}),
230+
),
231+
);
232+
233+
props.setProps({ hideSlug: true });
234+
const { userEvent, getByRole, queryByText, queryByLabelText, getByLabelText } = render(<CreateOrganization />, {
235+
wrapper,
236+
});
237+
238+
expect(queryByLabelText(/Slug/i)).not.toBeInTheDocument();
239+
240+
await userEvent.type(getByLabelText(/Name/i), 'new org');
241+
await userEvent.click(getByRole('button', { name: /create organization/i }));
242+
243+
await waitFor(() => {
244+
expect(queryByText(/Invite new members/i)).toBeInTheDocument();
245+
});
112246
});
113247
});
114248

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe('OrganizationList', () => {
8585
it('hides the personal account with no data to list', async () => {
8686
const { wrapper, props } = await createFixtures(f => {
8787
f.withOrganizations();
88+
f.withOrganizationSlug(true);
8889
f.withUser({
8990
email_addresses: ['[email protected]'],
9091
organization_memberships: [{ name: 'Org1', id: '1', role: 'admin' }],
@@ -210,6 +211,7 @@ describe('OrganizationList', () => {
210211
it('display CreateOrganization within OrganizationList', async () => {
211212
const { wrapper } = await createFixtures(f => {
212213
f.withOrganizations();
214+
f.withOrganizationSlug(true);
213215
f.withUser({
214216
email_addresses: ['[email protected]'],
215217
create_organization_enabled: true,

packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useOrganizationList } from '@clerk/shared/react';
2+
import type { CreateOrganizationParams } from '@clerk/types';
23

4+
import { useEnvironment } from '@/ui/contexts';
35
import { useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks';
46
import { localizationKeys } from '@/ui/customizables';
57
import { useCardState } from '@/ui/elements/contexts';
@@ -25,6 +27,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
2527
const { createOrganization, isLoaded, setActive } = useOrganizationList({
2628
userMemberships: organizationListParams.userMemberships,
2729
});
30+
const { organizationSettings } = useEnvironment();
2831

2932
const nameField = useFormControl('name', '', {
3033
type: 'text',
@@ -37,6 +40,8 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
3740
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__slug'),
3841
});
3942

43+
const organizationSlugEnabled = !organizationSettings.slug.disabled;
44+
4045
const onSubmit = async (e: React.FormEvent) => {
4146
e.preventDefault();
4247

@@ -45,7 +50,13 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
4550
}
4651

4752
try {
48-
const organization = await createOrganization({ name: nameField.value, slug: slugField.value });
53+
const createOrgParams: CreateOrganizationParams = { name: nameField.value };
54+
55+
if (organizationSlugEnabled) {
56+
createOrgParams.slug = slugField.value;
57+
}
58+
59+
const organization = await createOrganization(createOrgParams);
4960

5061
await setActive({
5162
organization,
@@ -90,15 +101,17 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
90101
ignorePasswordManager
91102
/>
92103
</Form.ControlRow>
93-
<Form.ControlRow elementId={slugField.id}>
94-
<Form.PlainInput
95-
{...slugField.props}
96-
onChange={event => updateSlugField(event.target.value)}
97-
isRequired
98-
pattern='^(?=.*[a-z0-9])[a-z0-9\-]+$'
99-
ignorePasswordManager
100-
/>
101-
</Form.ControlRow>
104+
{organizationSlugEnabled && (
105+
<Form.ControlRow elementId={slugField.id}>
106+
<Form.PlainInput
107+
{...slugField.props}
108+
onChange={event => updateSlugField(event.target.value)}
109+
isRequired
110+
pattern='^(?=.*[a-z0-9])[a-z0-9\-]+$'
111+
ignorePasswordManager
112+
/>
113+
</Form.ControlRow>
114+
)}
102115

103116
<FormButtonContainer sx={() => ({ flexDirection: 'column' })}>
104117
<Form.SubmitButton

0 commit comments

Comments
 (0)