Skip to content

Commit c5fcb39

Browse files
committed
Prefill with org defaults
1 parent 52941a9 commit c5fcb39

File tree

8 files changed

+65
-49
lines changed

8 files changed

+65
-49
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { APIKeySettings } from './APIKeySettings';
1616
import { AuthConfig, BaseResource, CommerceSettings, DisplayConfig, ProtectConfig, UserSettings } from './internal';
1717
import { OrganizationSettings } from './OrganizationSettings';
1818

19+
// TODO -> Update with new flag for default orgs
20+
// Use it to conditionally trigger query
1921
export class Environment extends BaseResource implements EnvironmentResource {
2022
private static instance: Environment;
2123

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
UserOrganizationInvitation,
5454
Web3Wallet,
5555
} from './internal';
56+
import { OrganizationCreationDefaults } from './OrganizationCreationDefaults';
5657

5758
export class User extends BaseResource implements UserResource {
5859
pathRoot = '/me';
@@ -275,6 +276,8 @@ export class User extends BaseResource implements UserResource {
275276
getOrganizationMemberships: GetOrganizationMemberships = retrieveMembership =>
276277
OrganizationMembership.retrieve(retrieveMembership);
277278

279+
getOrganizationCreationDefaults = () => OrganizationCreationDefaults.retrieve();
280+
278281
leaveOrganization = async (organizationId: string): Promise<DeletedObjectResource> => {
279282
const json = (
280283
await BaseResource._fetch<DeletedObjectJSON>({

packages/shared/src/types/user.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ExternalAccountResource } from './externalAccount';
77
import type { ImageResource } from './image';
88
import type { UserJSON } from './json';
99
import type { OAuthScope } from './oauth';
10+
import type { OrganizationCreationDefaultsResource } from './organizationCreationDefaults';
1011
import type { OrganizationInvitationStatus } from './organizationInvitation';
1112
import type { OrganizationMembershipResource } from './organizationMembership';
1213
import type { OrganizationSuggestionResource, OrganizationSuggestionStatus } from './organizationSuggestion';
@@ -115,6 +116,7 @@ export interface UserResource extends ClerkResource, BillingPayerMethods {
115116
getOrganizationSuggestions: (
116117
params?: GetUserOrganizationSuggestionsParams,
117118
) => Promise<ClerkPaginatedResponse<OrganizationSuggestionResource>>;
119+
getOrganizationCreationDefaults: () => Promise<OrganizationCreationDefaultsResource>;
118120
leaveOrganization: (organizationId: string) => Promise<DeletedObjectResource>;
119121
createTOTP: () => Promise<TOTPResource>;
120122
verifyTOTP: (params: VerifyTOTPParams) => Promise<TOTPResource>;

packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useOrganizationList } from '@clerk/shared/react';
2-
import type { CreateOrganizationParams } from '@clerk/shared/types';
2+
import type { CreateOrganizationParams, OrganizationCreationDefaultsResource } from '@clerk/shared/types';
33
import { useState } from 'react';
44

55
import { useEnvironment } from '@/ui/contexts';
@@ -20,24 +20,9 @@ import { OrganizationProfileAvatarUploader } from '../../../OrganizationProfile/
2020
import { organizationListParams } from '../../../OrganizationSwitcher/utils';
2121
import { OrganizationCreationDefaultsAlert } from './OrganizationCreationDefaultsAlert';
2222

23-
// TODO: Replace with actual API call to OrganizationCreationDefaults.retrieve()
24-
// TODO - Only replace if .organization_settings.organization_creation_defaults.enabled
25-
const organizationCreationDefaults = {
26-
advisory: {
27-
type: 'existing_org_with_domain' as const,
28-
severity: 'warning' as const,
29-
},
30-
form: {
31-
name: '',
32-
slug: '',
33-
logo: null,
34-
},
35-
pathRoot: '',
36-
reload: () => Promise.resolve({} as any),
37-
};
38-
3923
type CreateOrganizationScreenProps = {
4024
onCancel?: () => void;
25+
organizationCreationDefaults?: OrganizationCreationDefaultsResource;
4126
};
4227

4328
export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => {
@@ -50,12 +35,12 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
5035
const { organizationSettings } = useEnvironment();
5136
const [file, setFile] = useState<File | null>();
5237

53-
const nameField = useFormControl('name', '', {
38+
const nameField = useFormControl('name', props.organizationCreationDefaults?.form.name ?? '', {
5439
type: 'text',
5540
label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__name'),
5641
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__name'),
5742
});
58-
const slugField = useFormControl('slug', '', {
43+
const slugField = useFormControl('slug', props.organizationCreationDefaults?.form.slug ?? '', {
5944
type: 'text',
6045
label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__slug'),
6146
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__slug'),
@@ -81,6 +66,11 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
8166

8267
if (file) {
8368
await organization.setLogo({ file });
69+
} else if (defaultLogoUrl) {
70+
const response = await fetch(defaultLogoUrl);
71+
const blob = await response.blob();
72+
const logoFile = new File([blob], 'logo', { type: blob.type });
73+
await organization.setLogo({ file: logoFile });
8474
}
8575

8676
await setActive({
@@ -109,6 +99,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
10999
};
110100

111101
const isSubmitButtonDisabled = !nameField.value || !isLoaded;
102+
const defaultLogoUrl = file === undefined ? props.organizationCreationDefaults?.form.logo : undefined;
112103

113104
return (
114105
<>
@@ -122,11 +113,11 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
122113

123114
<FormContainer sx={t => ({ padding: `${t.space.$none} ${t.space.$10} ${t.space.$8}` })}>
124115
<Form.Root onSubmit={onSubmit}>
125-
<OrganizationCreationDefaultsAlert organizationCreationDefaults={organizationCreationDefaults} />
116+
<OrganizationCreationDefaultsAlert organizationCreationDefaults={props.organizationCreationDefaults} />
126117
<OrganizationProfileAvatarUploader
127-
organization={{ name: nameField.value }}
118+
organization={{ name: nameField.value, imageUrl: defaultLogoUrl ?? undefined }}
128119
onAvatarChange={async file => await setFile(file)}
129-
onAvatarRemove={file ? onAvatarRemove : null}
120+
onAvatarRemove={file || defaultLogoUrl ? onAvatarRemove : null}
130121
avatarPreviewPlaceholder={
131122
<IconButton
132123
variant='ghost'
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import type { OrganizationCreationAdvisoryType, OrganizationCreationDefaultsResource } from '@clerk/shared/types';
22

3+
import { Alert, Text } from '@/customizables';
34
import { type LocalizationKey, localizationKeys } from '@/localization';
4-
import { Alert } from '@/ui/elements/Alert';
55

66
export function OrganizationCreationDefaultsAlert({
77
organizationCreationDefaults,
88
}: {
9-
organizationCreationDefaults: OrganizationCreationDefaultsResource;
9+
organizationCreationDefaults?: OrganizationCreationDefaultsResource;
1010
}) {
11-
if (!organizationCreationDefaults.advisory) {
11+
if (!organizationCreationDefaults?.advisory) {
1212
return null;
1313
}
1414

1515
return (
16-
<Alert
17-
variant='warning'
18-
title={advisoryTypeToLocalizationKey[organizationCreationDefaults.advisory.type]}
19-
/>
16+
<Alert colorScheme='warning'>
17+
<Text
18+
colorScheme='warning'
19+
localizationKey={advisoryTypeToLocalizationKey[organizationCreationDefaults.advisory.type]}
20+
variant='caption'
21+
/>
22+
</Alert>
2023
);
2124
}
2225

26+
// TODO -> Update with latest advisory where meta is returned
27+
// TODO -> Include email domain in message, eg: {{ meta }}
2328
const advisoryTypeToLocalizationKey: Record<OrganizationCreationAdvisoryType, LocalizationKey> = {
2429
existing_org_with_domain: localizationKeys('taskChooseOrganization.alerts.existingOrgWithDomain'),
2530
};

packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,14 @@ describe('TaskChooseOrganization', () => {
299299
});
300300

301301
describe('with organization creation defaults', () => {
302-
it.todo('displays warning when organization already exists for user email domain');
302+
describe('when enabled on environment', () => {
303+
it.todo('displays warning when organization already exists for user email domain');
303304

304-
it.todo('prefills create organization form with defaults');
305+
it.todo('prefills create organization form with defaults');
306+
});
307+
308+
describe('when disabled on environment', () => {
309+
it.todo('does not fetch for creation defaults');
310+
});
305311
});
306312
});

packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useClerk, useSession, useUser } from '@clerk/shared/react';
2-
import { useState, type ComponentType } from 'react';
2+
import type { OrganizationCreationDefaultsResource } from '@clerk/shared/types';
3+
import { type ComponentType, useState } from 'react';
34

4-
import { useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts';
5+
import { useFetch } from '@/hooks';
6+
import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts';
57
import { descriptors, Flex, Flow, localizationKeys, Spinner } from '@/ui/customizables';
68
import { Card } from '@/ui/elements/Card';
79
import { withCardStateProvider } from '@/ui/elements/contexts';
@@ -14,24 +16,23 @@ import { ChooseOrganizationScreen } from './ChooseOrganizationScreen';
1416
import { CreateOrganizationScreen } from './CreateOrganizationScreen';
1517

1618
const TaskChooseOrganizationInternal = () => {
17-
const { signOut } = useClerk();
1819
const { user } = useUser();
19-
const { session } = useSession();
20+
const { environment } = useEnvironment();
2021
const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView();
21-
const { otherSessions } = useMultipleSessions({ user });
22-
const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext();
23-
24-
const handleSignOut = () => {
25-
if (otherSessions.length === 0) {
26-
return signOut(navigateAfterSignOut);
27-
}
28-
29-
return signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: session?.id });
30-
};
22+
const organizationCreationDefaults = useFetch(
23+
environment.organizationSettings.organizationCreationDefaults.enabled
24+
? user?.getOrganizationCreationDefaults
25+
: undefined,
26+
'organization-creation-defaults',
27+
{ staleTime: Infinity },
28+
);
3129

32-
const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading;
30+
const isLoading =
31+
userMemberships?.isLoading ||
32+
userInvitations?.isLoading ||
33+
userSuggestions?.isLoading ||
34+
organizationCreationDefaults.isLoading;
3335
const hasExistingResources = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count);
34-
const identifier = user?.primaryEmailAddress?.emailAddress ?? user?.username;
3536

3637
return (
3738
<Flow.Root flow='taskChooseOrganization'>
@@ -55,7 +56,10 @@ const TaskChooseOrganizationInternal = () => {
5556
/>
5657
</Flex>
5758
) : (
58-
<TaskChooseOrganizationFlows initialFlow={hasExistingResources ? 'choose' : 'create'} />
59+
<TaskChooseOrganizationFlows
60+
initialFlow={hasExistingResources ? 'choose' : 'create'}
61+
organizationCreationDefaults={organizationCreationDefaults.data}
62+
/>
5963
)}
6064
</Card.Content>
6165

@@ -111,6 +115,7 @@ const TaskChooseOrganizationCardFooter = () => {
111115

112116
type TaskChooseOrganizationFlowsProps = {
113117
initialFlow: 'create' | 'choose';
118+
organizationCreationDefaults?: OrganizationCreationDefaultsResource;
114119
};
115120

116121
const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrganizationFlowsProps) => {
@@ -120,6 +125,7 @@ const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrga
120125
return (
121126
<CreateOrganizationScreen
122127
onCancel={props.initialFlow === 'choose' ? () => setCurrentFlow('choose') : undefined}
128+
organizationCreationDefaults={props.organizationCreationDefaults}
123129
/>
124130
);
125131
}

packages/ui/src/elements/AvatarUploader.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ export const AvatarUploader = (props: AvatarUploaderProps) => {
9090
await handleFileDrop(f);
9191
};
9292

93+
const hasExistingImage = !!(avatarPreview.props as { imageUrl?: string })?.imageUrl;
9394
const previewElement = objectUrl
9495
? React.cloneElement(avatarPreview, { imageUrl: objectUrl })
95-
: avatarPreviewPlaceholder
96+
: avatarPreviewPlaceholder && !hasExistingImage
9697
? React.cloneElement(avatarPreviewPlaceholder, { onClick: openDialog })
9798
: avatarPreview;
9899

0 commit comments

Comments
 (0)