@@ -105,53 +105,163 @@ type KeywordSelectorProps = {
105
105
keyword : string ;
106
106
} ;
107
107
108
+ type TokenState =
109
+ | { status : 'none' }
110
+ | { status : 'loading' }
111
+ | { status : 'success' ; token : string }
112
+ | { status : 'error' } ;
113
+
114
+ const dropdownPopperOptions = {
115
+ placement : 'bottom' as const ,
116
+ modifiers : [
117
+ {
118
+ name : 'offset' ,
119
+ options : { offset : [ 0 , 10 ] } ,
120
+ } ,
121
+ { name : 'arrow' } ,
122
+ ] ,
123
+ } ;
124
+
108
125
function OrgAuthTokenCreator ( ) {
109
- const codeContext = useContext ( CodeContext ) ;
126
+ const { codeKeywords } = useContext ( CodeContext ) ;
110
127
111
- const [ tokenState , setTokenState ] = useState < 'none' | 'loading' | 'success' | 'error' > (
112
- 'none'
128
+ const [ tokenState , setTokenState ] = useState < TokenState > ( { status : 'none' } ) ;
129
+ const [ isOpen , setIsOpen ] = useState ( false ) ;
130
+ const [ referenceEl , setReferenceEl ] = useState < HTMLSpanElement > ( null ) ;
131
+ const [ dropdownEl , setDropdownEl ] = useState < HTMLElement > ( null ) ;
132
+ const { styles, state, attributes} = usePopper (
133
+ referenceEl ,
134
+ dropdownEl ,
135
+ dropdownPopperOptions
113
136
) ;
114
- const [ token , setToken ] = useState ( null ) ;
115
- const [ sharedSelection ] = codeContext . sharedKeywordSelection ;
116
- const { codeKeywords} = codeContext ;
117
137
118
- const choices = codeKeywords ?. PROJECT ;
138
+ useOnClickOutside ( {
139
+ ref : { current : referenceEl } ,
140
+ enabled : isOpen ,
141
+ handler : ( ) => setIsOpen ( false ) ,
142
+ } ) ;
143
+
144
+ const createToken = async ( orgSlug : string ) => {
145
+ setTokenState ( { status : 'loading' } ) ;
146
+ const token = await createOrgAuthToken ( {
147
+ orgSlug,
148
+ name : `Generated by Docs on ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` ,
149
+ } ) ;
150
+
151
+ if ( token ) {
152
+ setTokenState ( {
153
+ status : 'success' ,
154
+ token,
155
+ } ) ;
156
+ } else {
157
+ setTokenState ( {
158
+ status : 'error' ,
159
+ } ) ;
160
+ }
161
+ } ;
162
+
163
+ const orgSet = new Set < string > ( ) ;
164
+ codeKeywords ?. PROJECT ?. forEach ( projectKeyword => {
165
+ orgSet . add ( projectKeyword . ORG_SLUG ) ;
166
+ } ) ;
167
+ const orgSlugs = [ ...orgSet ] ;
168
+
169
+ const [ isAnimating , setIsAnimating ] = useState ( false ) ;
119
170
120
- // When not signed in, we just show a placeholder, as the user can't generate a token in this case
121
171
if ( ! codeKeywords . USER ) {
172
+ // User is not logged in - show dummy token
122
173
return < Fragment > sntrys_YOUR_TOKEN_HERE</ Fragment > ;
123
174
}
124
175
125
- const currentSelectionIdx = sharedSelection . PROJECT ?? 0 ;
126
- const currentSelection = choices [ currentSelectionIdx ] ;
176
+ if ( tokenState . status === 'success' ) {
177
+ return < Fragment > { tokenState . token } </ Fragment > ;
178
+ }
127
179
128
- const name = `Generated by Docs for ${ currentSelection . PROJECT_SLUG } on ${ new Date ( )
129
- . toISOString ( )
130
- . slice ( 0 , 10 ) } `;
180
+ if ( tokenState . status === 'error' ) {
181
+ return < Fragment > There was an error while generating your token.</ Fragment > ;
182
+ }
183
+
184
+ if ( tokenState . status === 'loading' ) {
185
+ return < Fragment > Generating token...</ Fragment > ;
186
+ }
131
187
132
- const updateToken = async ( ) => {
133
- if ( tokenState !== 'none' ) {
134
- return ;
188
+ const selector = isOpen && (
189
+ < PositionWrapper style = { styles . popper } ref = { setDropdownEl } { ...attributes . popper } >
190
+ < AnimatedContainer >
191
+ < Dropdown >
192
+ < Arrow
193
+ style = { styles . arrow }
194
+ data-placement = { state ?. placement }
195
+ data-popper-arrow
196
+ />
197
+ < DropdownHeader > Select an organization:</ DropdownHeader >
198
+ < Selections >
199
+ { orgSlugs . map ( org => {
200
+ return (
201
+ < ItemButton
202
+ key = { org }
203
+ isActive = { false }
204
+ onClick = { ( ) => {
205
+ createToken ( org ) ;
206
+ setIsOpen ( false ) ;
207
+ } }
208
+ >
209
+ { org }
210
+ </ ItemButton >
211
+ ) ;
212
+ } ) }
213
+ </ Selections >
214
+ </ Dropdown >
215
+ </ AnimatedContainer >
216
+ </ PositionWrapper >
217
+ ) ;
218
+
219
+ const portal = getPortal ( ) ;
220
+
221
+ const handlePress = ( ) => {
222
+ if ( orgSlugs . length === 1 ) {
223
+ createToken ( orgSlugs [ 0 ] ) ;
224
+ } else {
225
+ setIsOpen ( ! isOpen ) ;
135
226
}
136
- setTokenState ( 'loading' ) ;
137
- const tokenStr = await createOrgAuthToken ( {
138
- orgSlug : currentSelection . ORG_SLUG ,
139
- name,
140
- } ) ;
141
- setTokenState ( token ? 'success' : 'error' ) ;
142
- setToken ( tokenStr ) ;
143
227
} ;
144
228
145
229
return (
146
- < KeywordDropdown onClick = { updateToken } >
147
- { tokenState === 'none'
148
- ? 'Click to generate token'
149
- : tokenState === 'loading'
150
- ? 'Generating...'
151
- : token
152
- ? token
153
- : 'Error generating token' }
154
- </ KeywordDropdown >
230
+ < Fragment >
231
+ < KeywordDropdown
232
+ ref = { setReferenceEl }
233
+ role = "button"
234
+ title = "Click to generate token"
235
+ tabIndex = { 0 }
236
+ onClick = { ( ) => {
237
+ handlePress ( ) ;
238
+ } }
239
+ onKeyDown = { e => {
240
+ if ( [ 'Enter' , 'Space' ] . includes ( e . key ) ) {
241
+ handlePress ( ) ;
242
+ }
243
+ } }
244
+ >
245
+ < span
246
+ style = { {
247
+ // We set inline-grid only when animating the keyword so they
248
+ // correctly overlap during animations, but this must be removed
249
+ // after so copy-paste correctly works.
250
+ display : isAnimating ? 'inline-grid' : undefined ,
251
+ } }
252
+ >
253
+ < AnimatePresence initial = { false } >
254
+ < Keyword
255
+ onAnimationStart = { ( ) => setIsAnimating ( true ) }
256
+ onAnimationComplete = { ( ) => setIsAnimating ( false ) }
257
+ >
258
+ Click to generate token
259
+ </ Keyword >
260
+ </ AnimatePresence >
261
+ </ span >
262
+ </ KeywordDropdown >
263
+ { portal && createPortal ( < AnimatePresence > { selector } </ AnimatePresence > , portal ) }
264
+ </ Fragment >
155
265
) ;
156
266
}
157
267
@@ -162,16 +272,11 @@ function KeywordSelector({keyword, group, index}: KeywordSelectorProps) {
162
272
const [ referenceEl , setReferenceEl ] = useState < HTMLSpanElement > ( null ) ;
163
273
const [ dropdownEl , setDropdownEl ] = useState < HTMLElement > ( null ) ;
164
274
165
- const { styles, state, attributes} = usePopper ( referenceEl , dropdownEl , {
166
- placement : 'bottom' ,
167
- modifiers : [
168
- {
169
- name : 'offset' ,
170
- options : { offset : [ 0 , 10 ] } ,
171
- } ,
172
- { name : 'arrow' } ,
173
- ] ,
174
- } ) ;
275
+ const { styles, state, attributes} = usePopper (
276
+ referenceEl ,
277
+ dropdownEl ,
278
+ dropdownPopperOptions
279
+ ) ;
175
280
176
281
useOnClickOutside ( {
177
282
ref : { current : referenceEl } ,
@@ -195,26 +300,32 @@ function KeywordSelector({keyword, group, index}: KeywordSelectorProps) {
195
300
const selector = isOpen && (
196
301
< PositionWrapper style = { styles . popper } ref = { setDropdownEl } { ...attributes . popper } >
197
302
< AnimatedContainer >
198
- < Arrow style = { styles . arrow } data-placement = { state ?. placement } data-popper-arrow />
199
- < Selections >
200
- { choices . map ( ( item , idx ) => {
201
- const isActive = idx === currentSelectionIdx ;
202
- return (
203
- < ItemButton
204
- key = { idx }
205
- isActive = { isActive }
206
- onClick = { ( ) => {
207
- const newSharedSelection = { ...sharedSelection } ;
208
- newSharedSelection [ group ] = idx ;
209
- setSharedSelection ( newSharedSelection ) ;
210
- setIsOpen ( false ) ;
211
- } }
212
- >
213
- { item . title }
214
- </ ItemButton >
215
- ) ;
216
- } ) }
217
- </ Selections >
303
+ < Dropdown >
304
+ < Arrow
305
+ style = { styles . arrow }
306
+ data-placement = { state ?. placement }
307
+ data-popper-arrow
308
+ />
309
+ < Selections >
310
+ { choices . map ( ( item , idx ) => {
311
+ const isActive = idx === currentSelectionIdx ;
312
+ return (
313
+ < ItemButton
314
+ key = { idx }
315
+ isActive = { isActive }
316
+ onClick = { ( ) => {
317
+ const newSharedSelection = { ...sharedSelection } ;
318
+ newSharedSelection [ group ] = idx ;
319
+ setSharedSelection ( newSharedSelection ) ;
320
+ setIsOpen ( false ) ;
321
+ } }
322
+ >
323
+ { item . title }
324
+ </ ItemButton >
325
+ ) ;
326
+ } ) }
327
+ </ Selections >
328
+ </ Dropdown >
218
329
</ AnimatedContainer >
219
330
</ PositionWrapper >
220
331
) ;
@@ -342,16 +453,19 @@ const Arrow = styled('div')`
342
453
}
343
454
` ;
344
455
456
+ const Dropdown = styled ( 'div' ) `
457
+ overflow: hidden;
458
+ border-radius: 3px;
459
+ background: #fff;
460
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
461
+ ` ;
462
+
345
463
const Selections = styled ( 'div' ) `
346
464
padding: 4px 0;
347
- margin-top: -2px;
348
- background: #fff;
349
- border-radius: 3px;
350
465
overflow: scroll;
351
466
overscroll-behavior: contain;
352
467
max-height: 210px;
353
468
min-width: 300px;
354
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
355
469
` ;
356
470
357
471
const AnimatedContainer = styled ( motion . div ) `` ;
@@ -367,6 +481,13 @@ AnimatedContainer.defaultProps = {
367
481
} ,
368
482
} ;
369
483
484
+ const DropdownHeader = styled ( 'div' ) `
485
+ padding: 4px 8px;
486
+ color: #80708f;
487
+ background-color: #fff;
488
+ border-bottom: 1px solid #dbd6e1;
489
+ ` ;
490
+
370
491
const ItemButton = styled ( 'button' ) < { isActive : boolean } > `
371
492
font-family: 'Rubik', -apple-system, BlinkMacSystemFont, 'Segoe UI';
372
493
font-size: 0.85rem;
0 commit comments