1
- import { useUser } from '@clerk/shared/react' ;
1
+ import { useClerk , useOrganizationList , useUser } from '@clerk/shared/react' ;
2
+ import type { CreateOrganizationParams } from '@clerk/types' ;
3
+ import React , { useState } from 'react' ;
2
4
3
5
import { withCoreSessionSwitchGuard } from '@/ui/contexts' ;
4
- import { Flow , localizationKeys } from '@/ui/customizables' ;
6
+ import { useSessionTasksContext } from '@/ui/contexts/components/SessionTasks' ;
7
+ import { Col , descriptors , Flex , Flow , Icon , localizationKeys , Spinner } from '@/ui/customizables' ;
5
8
import { Card } from '@/ui/elements/Card' ;
6
- import { withCardStateProvider } from '@/ui/elements/contexts' ;
7
- import { FullHeightLoader } from '@/ui/elements/FullHeightLoader' ;
9
+ import { useCardState , withCardStateProvider } from '@/ui/elements/contexts' ;
10
+ import { Form } from '@/ui/elements/Form' ;
11
+ import { FormButtonContainer } from '@/ui/elements/FormButtons' ;
12
+ import { FormContainer } from '@/ui/elements/FormContainer' ;
8
13
import { Header } from '@/ui/elements/Header' ;
14
+ import { IconButton } from '@/ui/elements/IconButton' ;
15
+ import { Organization } from '@/ui/icons' ;
16
+ import { createSlug } from '@/ui/utils/createSlug' ;
17
+ import { handleError } from '@/ui/utils/errorHandler' ;
18
+ import { useFormControl } from '@/ui/utils/useFormControl' ;
9
19
import { getIdentifier } from '@/utils/user' ;
10
20
21
+ import { useOrganizationListInView } from '../../OrganizationList/OrganizationListPage' ;
22
+ import { OrganizationProfileAvatarUploader } from '../../OrganizationProfile/OrganizationProfileAvatarUploader' ;
23
+ import { organizationListParams } from '../../OrganizationSwitcher/utils' ;
11
24
import { withTaskGuard } from './withTaskGuard' ;
12
25
13
26
const TaskSelectOrganizationInternal = ( ) => {
14
27
const { user } = useUser ( ) ;
28
+ const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView ( ) ;
15
29
16
- // TODO -> Improve loading UI to keep consistent height with SignIn/SignUp
17
- const isLoading = false ;
30
+ const isLoading = userMemberships ?. isLoading || userInvitations ?. isLoading || userSuggestions ?. isLoading ;
31
+ const hasExistingResources = ! ! ( userMemberships ?. count || userInvitations ?. count || userSuggestions ?. count ) ;
18
32
19
33
return (
20
34
< Flow . Root flow = 'taskSelectOrganization' >
21
35
< Card . Root >
22
- { isLoading ? (
23
- < FullHeightLoader />
24
- ) : (
36
+ { ! isLoading && user ? (
25
37
< >
26
38
< Card . Content >
27
39
< Header . Root showLogo >
28
40
< Header . Title localizationKey = { localizationKeys ( 'taskSelectOrganization.title' ) } />
29
41
< Header . Subtitle localizationKey = { localizationKeys ( 'taskSelectOrganization.subtitle' ) } />
30
42
</ Header . Root >
43
+
44
+ < TaskSelectOrganizationFlows initialFlow = { hasExistingResources ? 'create' : 'select' } />
31
45
</ Card . Content >
32
46
< Card . Footer >
33
47
< Card . Action elementId = 'signOut' >
@@ -36,19 +50,213 @@ const TaskSelectOrganizationInternal = () => {
36
50
// TODO -> Change this key name to identifier
37
51
// TODO -> what happens if the user does not email address? only username or phonenumber
38
52
// Signed in as +55482323232
39
- emailAddress : user . primaryEmailAddress . emailAddress || getIdentifier ( user ) ,
53
+ emailAddress : user . primaryEmailAddress ? .emailAddress || getIdentifier ( user ) ,
40
54
} ) }
41
55
/>
42
56
< Card . ActionLink localizationKey = { localizationKeys ( 'taskSelectOrganization.signOut.actionLink' ) } />
43
57
</ Card . Action >
44
58
</ Card . Footer >
45
59
</ >
60
+ ) : (
61
+ // TODO -> Improve loading UI to keep consistent height with SignIn/SignUp
62
+ < Flex
63
+ direction = { 'row' }
64
+ align = { 'center' }
65
+ justify = { 'center' }
66
+ sx = { t => ( {
67
+ height : '100%' ,
68
+ minHeight : t . sizes . $100 ,
69
+ } ) }
70
+ >
71
+ < Spinner
72
+ size = { 'lg' }
73
+ colorScheme = { 'primary' }
74
+ elementDescriptor = { descriptors . spinner }
75
+ />
76
+ </ Flex >
46
77
) }
47
78
</ Card . Root >
48
79
</ Flow . Root >
49
80
) ;
50
81
} ;
51
82
83
+ type TaskSelectOrganizationFlowsProps = {
84
+ initialFlow : 'create' | 'select' ;
85
+ } ;
86
+
87
+ const TaskSelectOrganizationFlows = withCardStateProvider ( ( props : TaskSelectOrganizationFlowsProps ) => {
88
+ const [ currentFlow , setCurrentFlow ] = useState ( props . initialFlow ) ;
89
+
90
+ if ( currentFlow === 'create' ) {
91
+ return (
92
+ < CreateOrganizationScreen
93
+ onCancel = { props . initialFlow === 'select' ? ( ) => setCurrentFlow ( 'select' ) : undefined }
94
+ />
95
+ ) ;
96
+ }
97
+
98
+ return < > </ > ;
99
+ } ) ;
100
+
101
+ type CreateOrganizationScreenProps = {
102
+ onCancel ?: ( ) => void ;
103
+ hideSlug ?: boolean ;
104
+ } ;
105
+
106
+ const CreateOrganizationScreen = withCardStateProvider ( ( props : CreateOrganizationScreenProps ) => {
107
+ const card = useCardState ( ) ;
108
+ const { __internal_navigateToTaskIfAvailable } = useClerk ( ) ;
109
+ const { redirectUrlComplete } = useSessionTasksContext ( ) ;
110
+ const { createOrganization, isLoaded, setActive } = useOrganizationList ( {
111
+ userMemberships : organizationListParams . userMemberships ,
112
+ } ) ;
113
+
114
+ const [ file , setFile ] = useState < File | null > ( ) ;
115
+ const nameField = useFormControl ( 'name' , '' , {
116
+ type : 'text' ,
117
+ label : localizationKeys ( 'formFieldLabel__organizationName' ) ,
118
+ placeholder : localizationKeys ( 'formFieldInputPlaceholder__organizationName' ) ,
119
+ } ) ;
120
+ const slugField = useFormControl ( 'slug' , '' , {
121
+ type : 'text' ,
122
+ label : localizationKeys ( 'formFieldLabel__organizationSlug' ) ,
123
+ placeholder : localizationKeys ( 'formFieldInputPlaceholder__organizationSlug' ) ,
124
+ } ) ;
125
+
126
+ const onSubmit = async ( e : React . FormEvent ) => {
127
+ e . preventDefault ( ) ;
128
+
129
+ if ( ! isLoaded ) {
130
+ return ;
131
+ }
132
+
133
+ try {
134
+ const createOrgParams : CreateOrganizationParams = { name : nameField . value } ;
135
+
136
+ if ( ! props . hideSlug ) {
137
+ // TODO -> Should we always show the slug
138
+ // should it be exposed as a prop on TaskSelectOrganization
139
+ createOrgParams . slug = slugField . value ;
140
+ }
141
+
142
+ const organization = await createOrganization ( createOrgParams ) ;
143
+
144
+ if ( file ) {
145
+ await organization . setLogo ( { file } ) ;
146
+ }
147
+
148
+ await setActive ( { organization } ) ;
149
+
150
+ await __internal_navigateToTaskIfAvailable ( { redirectUrlComplete } ) ;
151
+ } catch ( err ) {
152
+ handleError ( err , [ nameField , slugField ] , card . setError ) ;
153
+ }
154
+ } ;
155
+
156
+ const onAvatarRemove = ( ) => {
157
+ card . setIdle ( ) ;
158
+ return setFile ( null ) ;
159
+ } ;
160
+
161
+ const onChangeName = ( event : React . ChangeEvent < HTMLInputElement > ) => {
162
+ nameField . setValue ( event . target . value ) ;
163
+ updateSlugField ( createSlug ( event . target . value ) ) ;
164
+ } ;
165
+
166
+ const onChangeSlug = ( event : React . ChangeEvent < HTMLInputElement > ) => {
167
+ updateSlugField ( event . target . value ) ;
168
+ } ;
169
+
170
+ const updateSlugField = ( val : string ) => {
171
+ slugField . setValue ( val ) ;
172
+ } ;
173
+
174
+ const isSubmitButtonDisabled = ! nameField . value || ! isLoaded ;
175
+
176
+ return (
177
+ < FormContainer sx = { t => ( { minHeight : t . sizes . $60 , gap : t . space . $6 , textAlign : 'left' } ) } >
178
+ < Form . Root onSubmit = { onSubmit } >
179
+ < Col >
180
+ < OrganizationProfileAvatarUploader
181
+ organization = { { name : nameField . value } }
182
+ // TODO - Fix type of `onAvatarChange`
183
+ onAvatarChange = { async file => setFile ( file ) }
184
+ onAvatarRemove = { file ? onAvatarRemove : null }
185
+ avatarPreviewPlaceholder = {
186
+ < IconButton
187
+ variant = 'ghost'
188
+ aria-label = 'Upload organization logo'
189
+ // TODO -> Update to icon from Figma
190
+ icon = {
191
+ < Icon
192
+ size = 'md'
193
+ icon = { Organization }
194
+ sx = { t => ( {
195
+ color : t . colors . $colorMutedForeground ,
196
+ transitionDuration : t . transitionDuration . $controls ,
197
+ } ) }
198
+ />
199
+ }
200
+ sx = { t => ( {
201
+ width : t . sizes . $16 ,
202
+ height : t . sizes . $16 ,
203
+ borderRadius : t . radii . $md ,
204
+ borderWidth : t . borderWidths . $normal ,
205
+ borderStyle : t . borderStyles . $dashed ,
206
+ borderColor : t . colors . $borderAlpha200 ,
207
+ backgroundColor : t . colors . $neutralAlpha50 ,
208
+ ':hover' : {
209
+ backgroundColor : t . colors . $neutralAlpha50 ,
210
+ svg : {
211
+ transform : 'scale(1.2)' ,
212
+ } ,
213
+ } ,
214
+ } ) }
215
+ />
216
+ }
217
+ />
218
+ </ Col >
219
+ < Form . ControlRow elementId = { nameField . id } >
220
+ < Form . PlainInput
221
+ { ...nameField . props }
222
+ onChange = { onChangeName }
223
+ isRequired
224
+ // TODO -> Remove auto focus?
225
+ autoFocus
226
+ ignorePasswordManager
227
+ />
228
+ </ Form . ControlRow >
229
+ { ! props . hideSlug && (
230
+ < Form . ControlRow elementId = { slugField . id } >
231
+ < Form . PlainInput
232
+ { ...slugField . props }
233
+ onChange = { onChangeSlug }
234
+ isRequired
235
+ pattern = '^(?=.*[a-z0-9])[a-z0-9\-]+$'
236
+ ignorePasswordManager
237
+ />
238
+ </ Form . ControlRow >
239
+ ) }
240
+ < FormButtonContainer sx = { t => ( { marginTop : t . space . $none , flexDirection : 'column' } ) } >
241
+ < Form . SubmitButton
242
+ block = { false }
243
+ sx = { ( ) => ( { width : '100%' } ) }
244
+ isDisabled = { isSubmitButtonDisabled }
245
+ localizationKey = { localizationKeys ( 'taskSelectOrganization.createOrganizationScreen.formButtonSubmit' ) }
246
+ />
247
+ { props . onCancel && (
248
+ < Form . ResetButton
249
+ localizationKey = { localizationKeys ( 'taskSelectOrganization.createOrganizationScreen.formButtonReset' ) }
250
+ block = { false }
251
+ onClick = { props . onCancel }
252
+ />
253
+ ) }
254
+ </ FormButtonContainer >
255
+ </ Form . Root >
256
+ </ FormContainer >
257
+ ) ;
258
+ } ) ;
259
+
52
260
export const TaskSelectOrganization = withCoreSessionSwitchGuard (
53
261
withTaskGuard ( withCardStateProvider ( TaskSelectOrganizationInternal ) ) ,
54
262
) ;
0 commit comments