@ -1,7 +1,40 @@
/ * * * E n t i t y G r i d . v u e - U n i f i e d e n t i t y g r i d l a y o u t c o m p o n e n t * * E x t r a c t e d f r o m
GiftedDialog . vue to provide a reusable grid layout * for displaying people ,
projects , and special entities with selection . * * @ author Matthew Raymer * /
/ * *
* EntityGrid . vue - Unified entity grid layout component
*
* Extracted from GiftedDialog . vue to provide a reusable grid layout
* for displaying people , projects , and special entities with selection .
*
* @ author Matthew Raymer
* /
< template >
<!-- Quick Search -- >
< div
id = "QuickSearch"
class = "mb-4 flex items-center text-sm"
>
< input
v - model = "searchTerm"
type = "text"
placeholder = "Search…"
class = "block w-full rounded-l border border-r-0 border-slate-400 px-3 py-1.5 placeholder:italic placeholder:text-slate-400 focus:outline-none"
@ input = "handleSearchInput"
@ keydown . enter = "performSearch"
/ >
< div
v - show = "isSearching"
class = "border-y border-slate-400 ps-2 py-1.5 text-center text-slate-400"
>
< font -awesome icon = "spinner" class = "fa-spin-pulse leading-[1.1]" > < / f o n t - a w e s o m e >
< / div >
< button
: disabled = "!searchTerm"
class = "px-2 py-1.5 rounded-r bg-white border border-l-0 border-slate-400 text-slate-400 disabled:cursor-not-allowed"
@ click = "clearSearch"
>
< font -awesome : icon = "searchTerm ? 'times' : 'magnifying-glass'" class = "fa-fw" > < / f o n t - a w e s o m e >
< / button >
< / div >
< ul class = "border-t border-slate-300 mb-4 max-h-[60vh] overflow-y-auto" >
<!-- Special entities ( You , Unnamed ) for people grids -- >
< template v-if ="entityType === 'people'" >
@ -101,6 +134,12 @@ export default class EntityGrid extends Vue {
@ Prop ( { required : true } )
entityType ! : "people" | "projects" ;
/ / S e a r c h s t a t e
searchTerm = "" ;
isSearching = false ;
searchTimeout : NodeJS . Timeout | null = null ;
filteredEntities : Contact [ ] | PlanData [ ] = [ ] ;
/** Array of entities to display */
@ Prop ( { required : true } )
entities ! : Contact [ ] | PlanData [ ] ;
@ -184,8 +223,15 @@ export default class EntityGrid extends Vue {
/ * *
* Computed entities to display - uses function prop if provided , otherwise defaults
* When searching , returns filtered results instead of original logic
* /
get displayedEntities ( ) : Contact [ ] | PlanData [ ] {
/ / I f s e a r c h i n g , r e t u r n f i l t e r e d r e s u l t s
if ( this . searchTerm . trim ( ) ) {
return this . filteredEntities ;
}
/ / O r i g i n a l l o g i c w h e n n o t s e a r c h i n g
if ( this . displayEntitiesFunction ) {
return this . displayEntitiesFunction (
this . entities ,
@ -283,6 +329,74 @@ export default class EntityGrid extends Vue {
} ) ;
}
/ * *
* Handle search input with debouncing
* /
handleSearchInput ( ) : void {
/ / S h o w s p i n n e r i m m e d i a t e l y w h e n u s e r t y p e s
this . isSearching = true ;
/ / C l e a r e x i s t i n g t i m e o u t
if ( this . searchTimeout ) {
clearTimeout ( this . searchTimeout ) ;
}
/ / S e t n e w t i m e o u t f o r 5 0 0 m s d e l a y
this . searchTimeout = setTimeout ( ( ) => {
this . performSearch ( ) ;
} , 500 ) ;
}
/ * *
* Perform the actual search
* /
async performSearch ( ) : Promise < void > {
if ( ! this . searchTerm . trim ( ) ) {
this . filteredEntities = [ ] ;
return ;
}
this . isSearching = true ;
try {
/ / S i m u l a t e a s y n c s e a r c h ( i n c a s e w e n e e d t o a d d A P I c a l l s l a t e r )
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
const searchLower = this . searchTerm . toLowerCase ( ) . trim ( ) ;
if ( this . entityType === "people" ) {
this . filteredEntities = ( this . entities as Contact [ ] ) . filter ( ( contact : Contact ) => {
const name = contact . name ? . toLowerCase ( ) || "" ;
const did = contact . did . toLowerCase ( ) ;
return name . includes ( searchLower ) || did . includes ( searchLower ) ;
} ) ;
} else {
this . filteredEntities = ( this . entities as PlanData [ ] ) . filter ( ( project : PlanData ) => {
const name = project . name ? . toLowerCase ( ) || "" ;
const handleId = project . handleId . toLowerCase ( ) ;
return name . includes ( searchLower ) || handleId . includes ( searchLower ) ;
} ) ;
}
} finally {
this . isSearching = false ;
}
}
/ * *
* Clear the search
* /
clearSearch ( ) : void {
this . searchTerm = "" ;
this . filteredEntities = [ ] ;
this . isSearching = false ;
/ / C l e a r a n y p e n d i n g t i m e o u t
if ( this . searchTimeout ) {
clearTimeout ( this . searchTimeout ) ;
this . searchTimeout = null ;
}
}
/ / E m i t m e t h o d s u s i n g @ E m i t d e c o r a t o r
@ Emit ( "entity-selected" )
@ -295,6 +409,15 @@ export default class EntityGrid extends Vue {
} {
return data ;
}
/ * *
* Cleanup timeouts when component is destroyed
* /
beforeUnmount ( ) : void {
if ( this . searchTimeout ) {
clearTimeout ( this . searchTimeout ) ;
}
}
}
< / script >