Skip to content

Commit

Permalink
feat(clerk-js,types): Supports default role on OrganizationProfile
Browse files Browse the repository at this point in the history
…invitations (#4210)
  • Loading branch information
LauraBeatris authored Sep 24, 2024
1 parent e3b2304 commit 19d3808
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/large-chefs-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clerk/clerk-js": patch
"@clerk/types": patch
---

Supports default role on `OrganizationProfile` invitations. When inviting a member, the default role will be automatically selected, otherwise it falls back to the only available role.
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/resources/OrganizationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe
domains!: {
enabled: boolean;
enrollmentModes: OrganizationEnrollmentMode[];
defaultRole: string | null;
};

public constructor(data: OrganizationSettingsJSON) {
Expand All @@ -26,6 +27,7 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe
this.domains = {
enabled: domains?.enabled || false,
enrollmentModes: domains?.enrollment_modes || [],
defaultRole: domains?.default_role || null,
};
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ClerkAPIError } from '@clerk/types';
import type { FormEvent } from 'react';
import { useState } from 'react';

import { useEnvironment } from '../../contexts';
import { Flex } from '../../customizables';
import { Form, FormButtonContainer, TagInput, useCardState } from '../../elements';
import { useFetchRoles } from '../../hooks/useFetchRoles';
Expand Down Expand Up @@ -187,6 +188,8 @@ const AsyncRoleSelect = (field: ReturnType<typeof useFormControl<'role'>>) => {

const { t } = useLocalizations();

const defaultRole = useDefaultRole();

return (
<Form.ControlRow elementId={field.id}>
<Flex
Expand All @@ -195,6 +198,7 @@ const AsyncRoleSelect = (field: ReturnType<typeof useFormControl<'role'>>) => {
>
<RoleSelect
{...field.props}
value={field.props.value || (defaultRole ?? '')}
roles={options}
isDisabled={isLoading}
onChange={value => field.setValue(value)}
Expand All @@ -206,3 +210,20 @@ const AsyncRoleSelect = (field: ReturnType<typeof useFormControl<'role'>>) => {
</Form.ControlRow>
);
};

/**
* Determines default role from the organization settings or fallback to
* the only available role.
*/
const useDefaultRole = () => {
const { options } = useFetchRoles();
const { organizationSettings } = useEnvironment();

let defaultRole = organizationSettings.domains.defaultRole;

if (!defaultRole && options?.length === 1) {
defaultRole = options[0].value;
}

return defaultRole;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { OrganizationInvitationResource } from '@clerk/types';
import { describe } from '@jest/globals';
import { waitFor } from '@testing-library/dom';
import React from 'react';

import { ClerkAPIResponseError } from '../../../../core/resources';
import { render } from '../../../../testUtils';
Expand Down Expand Up @@ -41,7 +42,156 @@ describe('InviteMembersPage', () => {
getByText('Enter or paste one or more email addresses, separated by spaces or commas.');
});

describe('Submitting', () => {
describe('with default role', () => {
it("initializes with the organization's default role", async () => {
const defaultRole = 'mydefaultrole';

const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withOrganizationDomains(undefined, defaultRole);
f.withUser({
email_addresses: ['test@clerk.com'],
organization_memberships: [{ name: 'Org1', role: 'admin' }],
});
});

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

fixtures.clerk.organization?.inviteMembers.mockResolvedValueOnce([{}] as OrganizationInvitationResource[]);
const { getByRole, userEvent, getByTestId } = render(
<Action.Root>
<InviteMembersScreen />
</Action.Root>,
{ wrapper },
);
await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,');
await userEvent.click(getByRole('button', { name: /mydefaultrole/i }));
});

it("initializes if there's only one role available", async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.com'],
organization_memberships: [{ name: 'Org1', role: 'admin' }],
});
});

fixtures.clerk.organization?.getRoles.mockResolvedValue({
total_count: 1,
data: [
{
pathRoot: '',
reload: jest.fn(),
id: 'member',
key: 'member',
name: 'member',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

fixtures.clerk.organization?.inviteMembers.mockResolvedValueOnce([{}] as OrganizationInvitationResource[]);
const { getByRole, userEvent, getByTestId } = render(
<Action.Root>
<InviteMembersScreen />
</Action.Root>,
{ wrapper },
);
await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,');
await waitFor(() => expect(getByRole('button', { name: /member/i })).toBeInTheDocument());
});

it("does not initialize if there's neither a default role nor a unique role", async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.com'],
organization_memberships: [{ name: 'Org1', role: 'admin' }],
});
});

fixtures.clerk.organization?.getRoles.mockResolvedValue({
total_count: 1,
data: [
{
pathRoot: '',
reload: jest.fn(),
id: 'member',
key: 'member',
name: 'member',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

fixtures.clerk.organization?.inviteMembers.mockResolvedValueOnce([{}] as OrganizationInvitationResource[]);
const { getByRole, userEvent, getByTestId } = render(
<Action.Root>
<InviteMembersScreen />
</Action.Root>,
{ wrapper },
);
await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,');
await waitFor(() => expect(getByRole('button', { name: /select role/i })).toBeInTheDocument());
});
});

describe('when submitting', () => {
it('keeps the Send button disabled until a role is selected and one or more email has been entered', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
Expand All @@ -65,6 +215,17 @@ describe('InviteMembersPage', () => {
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

Expand Down Expand Up @@ -108,6 +269,17 @@ describe('InviteMembersPage', () => {
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

Expand Down Expand Up @@ -154,6 +326,17 @@ describe('InviteMembersPage', () => {
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

Expand Down Expand Up @@ -259,6 +442,17 @@ describe('InviteMembersPage', () => {
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

Expand Down Expand Up @@ -318,6 +512,17 @@ describe('InviteMembersPage', () => {
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

Expand Down Expand Up @@ -373,6 +578,17 @@ describe('InviteMembersPage', () => {
createdAt: new Date(),
updatedAt: new Date(),
},
{
pathRoot: '',
reload: jest.fn(),
id: 'admin',
key: 'admin',
name: 'Admin',
description: '',
permissions: [],
createdAt: new Date(),
updatedAt: new Date(),
},
],
});

Expand Down
3 changes: 2 additions & 1 deletion packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,10 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON)
os.max_allowed_memberships = max;
};

const withOrganizationDomains = (modes?: OrganizationEnrollmentMode[]) => {
const withOrganizationDomains = (modes?: OrganizationEnrollmentMode[], defaultRole?: string) => {
os.domains.enabled = true;
os.domains.enrollment_modes = modes || ['automatic_invitation', 'automatic_invitation', 'manual_invitation'];
os.domains.default_role = defaultRole ?? null;
};
return { withOrganizations, withMaxAllowedMemberships, withOrganizationDomains };
};
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/organizationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface OrganizationSettingsJSON extends ClerkResourceJSON {
domains: {
enabled: boolean;
enrollment_modes: OrganizationEnrollmentMode[];
default_role: string | null;
};
}

Expand All @@ -25,5 +26,6 @@ export interface OrganizationSettingsResource extends ClerkResource {
domains: {
enabled: boolean;
enrollmentModes: OrganizationEnrollmentMode[];
defaultRole: string | null;
};
}

0 comments on commit 19d3808

Please sign in to comment.