1- import React , { useState , useEffect , useCallback } from 'react' ;
1+ import React , { useState , useEffect , useCallback , useMemo } from 'react' ;
22import { useApi } from '@backstage/core-plugin-api' ;
33import Chip from '@material-ui/core/Chip'
44import { stackoverflowteamsApiRef } from '../../api' ;
@@ -30,7 +30,8 @@ import PersonIcon from '@mui/icons-material/Person';
3030import { useStackOverflowStyles } from './hooks' ;
3131import { TiptapEditor } from './TiptapEditor' ;
3232import type { Tag } from '../../types'
33- import { CircularProgress } from '@mui/material' ;
33+ import CircularProgress from '@mui/material/CircularProgress' ;
34+ import { debounce } from '@material-ui/core' ;
3435
3536// Utility function to detect Mac
3637const isMac = ( ) => {
@@ -74,7 +75,12 @@ export const StackOverflowPostQuestionModal = () => {
7475 const [ loadingTags , setLoadingTags ] = useState ( false )
7576 const [ tagError , setTagError ] = useState < string | null > ( null )
7677
77- const fetchPopularTags = useCallback ( async function ( ) {
78+ // Autopopulate tags
79+ const [ tagSearchResults , setTagSearchResults ] = useState < Tag [ ] > ( [ ] ) ;
80+ const [ searchingTags , setSearchingTags ] = useState ( false ) ;
81+ const [ showCreateTagOption , setShowCreateTagOption ] = useState ( false ) ;
82+
83+ const fetchPopularTags = useCallback ( async function fetchPopularTags ( ) {
7884 if ( ! isAuthenticated ) return ;
7985
8086 setLoadingTags ( true ) ;
@@ -91,6 +97,29 @@ export const StackOverflowPostQuestionModal = () => {
9197 setLoadingTags ( false ) ;
9298 }
9399 } , [ stackOverflowApi , isAuthenticated ] )
100+
101+ const searchTags = useMemo (
102+ ( ) => debounce ( async ( searchTerm : string ) => {
103+ if ( ! searchTerm . trim ( ) || ! isAuthenticated ) {
104+ setTagSearchResults ( [ ] ) ;
105+ setShowCreateTagOption ( false ) ;
106+ return ;
107+ }
108+ setSearchingTags ( true ) ;
109+ try {
110+ const response = await stackOverflowApi . getTags ( searchTerm . trim ( ) ) ;
111+ const results = response . items || [ ] ;
112+ setTagSearchResults ( results ) ;
113+ setShowCreateTagOption ( results . length === 0 ) ; // Show create option only when no results found!
114+ } catch ( err ) {
115+ setTagSearchResults ( [ ] ) ;
116+ setShowCreateTagOption ( true ) ; // Show create option if search fails, will keep this for now, since is highly unlikely that this fails without all other things being broken
117+ } finally {
118+ setSearchingTags ( false ) ;
119+ }
120+ } , 500 ) ,
121+ [ stackOverflowApi , isAuthenticated ]
122+ ) ;
94123
95124 // Get modifier key info
96125 const modifierKey = getModifierKey ( ) ;
@@ -198,6 +227,7 @@ export const StackOverflowPostQuestionModal = () => {
198227 if ( ! tagsStarted ) setTagsStarted ( true ) ;
199228 }
200229 setTagInput ( '' ) ;
230+ setTagSearchResults ( [ ] ) ;
201231 } ;
202232
203233 // This can be uncommented in future once mentioning users over API v3 is supported
@@ -551,43 +581,126 @@ export const StackOverflowPostQuestionModal = () => {
551581 </ Typography >
552582 ) }
553583 </ Box >
554- < Box sx = { { mb : 3 } } >
555- < Typography variant = "subtitle1" fontWeight = "bold" gutterBottom >
556- Tags
557- </ Typography >
558- < Typography variant = "body2" color = "text.secondary" sx = { { mb : 1 } } >
559- Add a minimum of one tag
560- </ Typography >
561- < TextField
562- fullWidth
563- variant = "outlined"
564- value = { tagInput }
565- onChange = { e => {
566- setTagInput ( e . target . value ) ;
567- if ( e . target . value . includes ( ',' ) || e . target . value . includes ( ' ' ) ) {
568- handleTagAdd ( ) ;
569- }
570- } }
571- onFocus = { ( ) => setFocusedField ( 'tags' ) }
572- onKeyDown = { e => e . key === 'Enter' && handleTagAdd ( ) }
573- placeholder = "e.g., react, javascript, authentication"
574- error = { ! ! tagsValidation }
575- />
576-
577- { tags . length > 0 && (
578- < Box sx = { { display : 'flex' , flexWrap : 'wrap' , gap : 1 , mt : 1 } } >
579- { tags . map ( ( tag , index ) => (
580- < Chip
581- key = { index }
582- label = { tag }
583- onDelete = { ( ) => setTags ( tags . filter ( t => t !== tag ) ) }
584- size = "medium"
585- variant = "outlined"
586- color = "primary"
584+ < Box sx = { { mb : 3 , position : 'relative' } } >
585+ < Typography variant = "subtitle1" fontWeight = "bold" gutterBottom >
586+ Tags
587+ </ Typography >
588+ < Typography variant = "body2" color = "text.secondary" sx = { { mb : 1 } } >
589+ Add a minimum of one tag
590+ </ Typography >
591+ < TextField
592+ fullWidth
593+ variant = "outlined"
594+ value = { tagInput }
595+ onChange = { e => {
596+ const value = e . target . value ;
597+ setTagInput ( value ) ;
598+ setShowCreateTagOption ( false ) ;
599+
600+ // Search for tags as user types
601+ const lastTag = value . split ( / [ \s , ] / ) . pop ( ) ?. trim ( ) || '' ;
602+ if ( lastTag . length >= 2 ) {
603+ searchTags ( lastTag ) ;
604+ } else {
605+ setTagSearchResults ( [ ] ) ;
606+ }
607+
608+ if ( value . includes ( ',' ) || value . includes ( ' ' ) ) {
609+ handleTagAdd ( ) ;
610+ }
611+ } }
612+ onFocus = { ( ) => setFocusedField ( 'tags' ) }
613+ onKeyDown = { e => e . key === 'Enter' && handleTagAdd ( ) }
614+ placeholder = "e.g., react, javascript, authentication"
615+ error = { ! ! tagsValidation }
616+ />
617+ { ( tagSearchResults . length > 0 || searchingTags || ( tagInput . trim ( ) && tagSearchResults . length === 0 && ! searchingTags ) ) && (
618+ < Paper
619+ elevation = { 3 }
620+ sx = { {
621+ position : 'absolute' ,
622+ zIndex : 1000 ,
623+ width : '100%' ,
624+ maxHeight : 200 ,
625+ overflow : 'auto' ,
626+ mt : 1
627+ } }
628+ >
629+ { searchingTags && (
630+ < Box sx = { { p : 2 , display : 'flex' , alignItems : 'center' , gap : 1 } } >
631+ < CircularProgress size = { 16 } />
632+ < Typography variant = "body2" > Searching tags...</ Typography >
633+ </ Box >
634+ ) }
635+ { tagSearchResults . map ( tag => (
636+ < ListItem
637+ key = { tag . name }
638+ onClick = { ( ) => {
639+ if ( ! tags . includes ( tag . name ) && tags . length < 5 ) {
640+ setTags ( [ ...tags , tag . name ] ) ;
641+ if ( ! tagsStarted ) setTagsStarted ( true ) ;
642+ }
643+ setTagInput ( '' ) ;
644+ setTagSearchResults ( [ ] ) ;
645+ } }
646+ sx = { {
647+ cursor : 'pointer' ,
648+ '&:hover' : { backgroundColor : 'action.hover' }
649+ } }
650+ >
651+ < ListItemText
652+ primary = { tag . name }
653+ // secondary={`${tag.count} questions`}
587654 />
588- ) ) }
589- </ Box >
590- ) }
655+ </ ListItem >
656+ ) ) }
657+ { tagInput . trim ( ) && showCreateTagOption && (
658+ < ListItem
659+ onClick = { ( ) => {
660+ const trimmedTag = tagInput . trim ( ) ;
661+ if ( trimmedTag && ! tags . includes ( trimmedTag ) && tags . length < 5 ) {
662+ setTags ( [ ...tags , trimmedTag ] ) ;
663+ if ( ! tagsStarted ) setTagsStarted ( true ) ;
664+ setShowCreateTagOption ( false ) ;
665+ }
666+ setTagInput ( '' ) ;
667+ setTagSearchResults ( [ ] ) ;
668+ } }
669+ sx = { {
670+ cursor : 'pointer' ,
671+ '&:hover' : { backgroundColor : 'action.hover' } ,
672+ borderTop : tagSearchResults . length > 0 ? '1px solid' : 'none' ,
673+ borderColor : 'divider'
674+ } }
675+ >
676+ < ListItemText
677+ primary = {
678+ < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } } >
679+ < Typography > Create "{ tagInput . trim ( ) } "</ Typography >
680+ < Chip size = "small" label = "New" color = "primary" variant = "outlined" />
681+ </ Box >
682+ }
683+ secondary = "This will create a new tag"
684+ />
685+ </ ListItem >
686+ ) }
687+ </ Paper >
688+ ) }
689+
690+ { tags . length > 0 && (
691+ < Box sx = { { display : 'flex' , flexWrap : 'wrap' , gap : 1 , mt : 1 } } >
692+ { tags . map ( ( tag , index ) => (
693+ < Chip
694+ key = { index }
695+ label = { tag }
696+ onDelete = { ( ) => setTags ( tags . filter ( t => t !== tag ) ) }
697+ size = "medium"
698+ variant = "outlined"
699+ color = "primary"
700+ />
701+ ) ) }
702+ </ Box >
703+ ) }
591704 </ Box >
592705 { /* This is the UI for mentioning users (not yet supported on v3) */ }
593706 { /* <Box sx={{ mb: 3 }}>
@@ -679,7 +792,7 @@ export const StackOverflowPostQuestionModal = () => {
679792 ) ;
680793 }
681794 return (
682- < Grid container spacing = { 4 } sx = { { height : '100% ' } } >
795+ < Grid container spacing = { 4 } sx = { { height : '80vh ' } } >
683796 < Grid item xs = { 12 } md = { 8 } >
684797 { renderQuestionForm ( ) }
685798 </ Grid >
@@ -698,7 +811,7 @@ export const StackOverflowPostQuestionModal = () => {
698811 top : '50%' ,
699812 left : '50%' ,
700813 transform : 'translate(-50%, -50%)' ,
701- width : { xs : '95vw' , sm : '90vw' , md : '80vw' , lg : '65vw ' } ,
814+ width : { xs : '95vw' , sm : '90vw' , md : '80vw' , lg : '70vw ' } ,
702815 maxHeight : '90vh' ,
703816 bgcolor : 'background.paper' ,
704817 boxShadow : 24 ,
0 commit comments