Browse Source

feat: Add quick search to EntityGrid with date-based contact sorting

- Add search-as-you-type functionality with 500ms debounce
- Implement search across contact names and DIDs, project names and handleIds
- Add loading spinner and dynamic clear button
- Add $contactsByDateAdded() method to PlatformServiceMixin for newest-first sorting
- Update GiftedDialog to use date-based contact ordering
- Maintain backward compatibility with existing $contacts() alphabetical sorting
- Add proper cleanup for search timeouts on component unmount

The search feature provides real-time filtering with visual feedback,
while the new sorting ensures recently added contacts appear first.
pull/216/head
Jose Olarte III 4 days ago
parent
commit
a804877a08
  1. 129
      src/components/EntityGrid.vue
  2. 2
      src/components/GiftedDialog.vue
  3. 15
      src/utils/PlatformServiceMixin.ts

129
src/components/EntityGrid.vue

@ -1,7 +1,40 @@
/** * 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 */
/**
* 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]"></font-awesome>
</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"></font-awesome>
</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";
// Search state
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[] {
// If searching, return filtered results
if (this.searchTerm.trim()) {
return this.filteredEntities;
}
// Original logic when not searching
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 {
// Show spinner immediately when user types
this.isSearching = true;
// Clear existing timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Set new timeout for 500ms delay
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 {
// Simulate async search (in case we need to add API calls later)
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;
// Clear any pending timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
this.searchTimeout = null;
}
}
// Emit methods using @Emit decorator
@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>

2
src/components/GiftedDialog.vue

@ -231,7 +231,7 @@ export default class GiftedDialog extends Vue {
apiServer: this.apiServer,
});
this.allContacts = await this.$contacts();
this.allContacts = await this.$contactsByDateAdded();
this.allMyDids = await retrieveAccountDids();

15
src/utils/PlatformServiceMixin.ts

@ -970,6 +970,20 @@ export const PlatformServiceMixin = {
return this.$normalizeContacts(rawContacts);
},
/**
* Load all contacts sorted by when they were added (by ID)
* Always fetches fresh data from database for consistency
* Handles JSON string/object duality for contactMethods field
* @returns Promise<Contact[]> Array of normalized contact objects sorted by addition date (newest first)
*/
async $contactsByDateAdded(): Promise<Contact[]> {
const rawContacts = (await this.$query(
"SELECT * FROM contacts ORDER BY id DESC",
)) as ContactMaybeWithJsonStrings[];
return this.$normalizeContacts(rawContacts);
},
/**
* Ultra-concise shortcut for getting number of contacts
* @returns Promise<number> Total number of contacts
@ -2057,6 +2071,7 @@ declare module "@vue/runtime-core" {
// Specialized shortcuts - contacts cached, settings fresh
$contacts(): Promise<Contact[]>;
$contactsByDateAdded(): Promise<Contact[]>;
$contactCount(): Promise<number>;
$settings(defaults?: Settings): Promise<Settings>;
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;

Loading…
Cancel
Save