@@ -105,53 +105,163 @@ type KeywordSelectorProps = {
105105 keyword : string ;
106106} ;
107107
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+
108125function OrgAuthTokenCreator ( ) {
109- const codeContext = useContext ( CodeContext ) ;
126+ const { codeKeywords } = useContext ( CodeContext ) ;
110127
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
113136 ) ;
114- const [ token , setToken ] = useState ( null ) ;
115- const [ sharedSelection ] = codeContext . sharedKeywordSelection ;
116- const { codeKeywords} = codeContext ;
117137
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 ) ;
119170
120- // When not signed in, we just show a placeholder, as the user can't generate a token in this case
121171 if ( ! codeKeywords . USER ) {
172+ // User is not logged in - show dummy token
122173 return < Fragment > sntrys_YOUR_TOKEN_HERE</ Fragment > ;
123174 }
124175
125- const currentSelectionIdx = sharedSelection . PROJECT ?? 0 ;
126- const currentSelection = choices [ currentSelectionIdx ] ;
176+ if ( tokenState . status === 'success' ) {
177+ return < Fragment > { tokenState . token } </ Fragment > ;
178+ }
127179
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+ }
131187
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 ) ;
135226 }
136- setTokenState ( 'loading' ) ;
137- const tokenStr = await createOrgAuthToken ( {
138- orgSlug : currentSelection . ORG_SLUG ,
139- name,
140- } ) ;
141- setTokenState ( token ? 'success' : 'error' ) ;
142- setToken ( tokenStr ) ;
143227 } ;
144228
145229 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 >
155265 ) ;
156266}
157267
@@ -162,16 +272,11 @@ function KeywordSelector({keyword, group, index}: KeywordSelectorProps) {
162272 const [ referenceEl , setReferenceEl ] = useState < HTMLSpanElement > ( null ) ;
163273 const [ dropdownEl , setDropdownEl ] = useState < HTMLElement > ( null ) ;
164274
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+ ) ;
175280
176281 useOnClickOutside ( {
177282 ref : { current : referenceEl } ,
@@ -195,26 +300,32 @@ function KeywordSelector({keyword, group, index}: KeywordSelectorProps) {
195300 const selector = isOpen && (
196301 < PositionWrapper style = { styles . popper } ref = { setDropdownEl } { ...attributes . popper } >
197302 < 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 >
218329 </ AnimatedContainer >
219330 </ PositionWrapper >
220331 ) ;
@@ -342,16 +453,19 @@ const Arrow = styled('div')`
342453 }
343454` ;
344455
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+
345463const Selections = styled ( 'div' ) `
346464 padding: 4px 0;
347- margin-top: -2px;
348- background: #fff;
349- border-radius: 3px;
350465 overflow: scroll;
351466 overscroll-behavior: contain;
352467 max-height: 210px;
353468 min-width: 300px;
354- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
355469` ;
356470
357471const AnimatedContainer = styled ( motion . div ) `` ;
@@ -367,6 +481,13 @@ AnimatedContainer.defaultProps = {
367481 } ,
368482} ;
369483
484+ const DropdownHeader = styled ( 'div' ) `
485+ padding: 4px 8px;
486+ color: #80708f;
487+ background-color: #fff;
488+ border-bottom: 1px solid #dbd6e1;
489+ ` ;
490+
370491const ItemButton = styled ( 'button' ) < { isActive : boolean } > `
371492 font-family: 'Rubik', -apple-system, BlinkMacSystemFont, 'Segoe UI';
372493 font-size: 0.85rem;
0 commit comments