@yottagraph-app/aether-instructions 1.1.36 → 1.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/rules/cookbook-data.mdc +145 -0
package/package.json
CHANGED
package/rules/cookbook-data.mdc
CHANGED
|
@@ -403,3 +403,148 @@ relationship PID. See the `data` rule for the two-layer architecture.
|
|
|
403
403
|
}
|
|
404
404
|
</script>
|
|
405
405
|
```
|
|
406
|
+
|
|
407
|
+
## 5. Async Entity Search with Live Suggestions
|
|
408
|
+
|
|
409
|
+
Type-ahead search that shows results in a dropdown as the user types. Uses
|
|
410
|
+
`searchEntities()` from the gateway helpers with a debounced watcher.
|
|
411
|
+
|
|
412
|
+
> **Do not use `v-autocomplete` for async search.** Vuetify's `v-autocomplete`
|
|
413
|
+
> with `hide-no-data` + `no-filter` + async item loading has a timing bug:
|
|
414
|
+
> the menu hides while items are empty (during the fetch) and does not reopen
|
|
415
|
+
> when results arrive. Use `v-text-field` with a manual dropdown instead.
|
|
416
|
+
|
|
417
|
+
```vue
|
|
418
|
+
<template>
|
|
419
|
+
<div class="entity-search" style="position: relative">
|
|
420
|
+
<v-text-field
|
|
421
|
+
v-model="searchQuery"
|
|
422
|
+
:label="label"
|
|
423
|
+
:prepend-inner-icon="icon"
|
|
424
|
+
variant="solo-filled"
|
|
425
|
+
rounded="lg"
|
|
426
|
+
clearable
|
|
427
|
+
density="comfortable"
|
|
428
|
+
:loading="searching"
|
|
429
|
+
@focus="showMenu = suggestions.length > 0"
|
|
430
|
+
@click:clear="onClear"
|
|
431
|
+
/>
|
|
432
|
+
|
|
433
|
+
<v-card
|
|
434
|
+
v-if="showMenu && suggestions.length > 0"
|
|
435
|
+
class="search-dropdown"
|
|
436
|
+
elevation="8"
|
|
437
|
+
>
|
|
438
|
+
<v-list density="compact">
|
|
439
|
+
<v-list-item
|
|
440
|
+
v-for="item in suggestions"
|
|
441
|
+
:key="item.neid"
|
|
442
|
+
:title="item.name"
|
|
443
|
+
:subtitle="item.neid"
|
|
444
|
+
@click="onSelect(item)"
|
|
445
|
+
/>
|
|
446
|
+
</v-list>
|
|
447
|
+
</v-card>
|
|
448
|
+
</div>
|
|
449
|
+
</template>
|
|
450
|
+
|
|
451
|
+
<script setup lang="ts">
|
|
452
|
+
import { searchEntities } from '~/utils/elementalHelpers';
|
|
453
|
+
|
|
454
|
+
const props = withDefaults(
|
|
455
|
+
defineProps<{
|
|
456
|
+
label?: string;
|
|
457
|
+
icon?: string;
|
|
458
|
+
flavors?: string[];
|
|
459
|
+
}>(),
|
|
460
|
+
{ label: 'Search', icon: 'mdi-magnify', flavors: undefined },
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const emit = defineEmits<{
|
|
464
|
+
selected: [entity: { neid: string; name: string }];
|
|
465
|
+
}>();
|
|
466
|
+
|
|
467
|
+
const searchQuery = ref('');
|
|
468
|
+
const suggestions = ref<{ neid: string; name: string }[]>([]);
|
|
469
|
+
const searching = ref(false);
|
|
470
|
+
const showMenu = ref(false);
|
|
471
|
+
let selectedName = '';
|
|
472
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
473
|
+
|
|
474
|
+
watch(searchQuery, (val) => {
|
|
475
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
476
|
+
if (val === selectedName) return;
|
|
477
|
+
if (!val || val.length < 2) {
|
|
478
|
+
suggestions.value = [];
|
|
479
|
+
showMenu.value = false;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
debounceTimer = setTimeout(() => doSearch(val), 300);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
async function doSearch(query: string) {
|
|
486
|
+
searching.value = true;
|
|
487
|
+
try {
|
|
488
|
+
suggestions.value = await searchEntities(query, {
|
|
489
|
+
maxResults: 8,
|
|
490
|
+
flavors: props.flavors,
|
|
491
|
+
});
|
|
492
|
+
showMenu.value = suggestions.value.length > 0;
|
|
493
|
+
} catch {
|
|
494
|
+
suggestions.value = [];
|
|
495
|
+
showMenu.value = false;
|
|
496
|
+
} finally {
|
|
497
|
+
searching.value = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function onSelect(item: { neid: string; name: string }) {
|
|
502
|
+
selectedName = item.name;
|
|
503
|
+
searchQuery.value = item.name;
|
|
504
|
+
suggestions.value = [];
|
|
505
|
+
showMenu.value = false;
|
|
506
|
+
emit('selected', item);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function onClear() {
|
|
510
|
+
selectedName = '';
|
|
511
|
+
suggestions.value = [];
|
|
512
|
+
showMenu.value = false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
onMounted(() => {
|
|
516
|
+
document.addEventListener('click', (e) => {
|
|
517
|
+
const el = (e.target as HTMLElement)?.closest('.entity-search');
|
|
518
|
+
if (!el) showMenu.value = false;
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
</script>
|
|
522
|
+
|
|
523
|
+
<style scoped>
|
|
524
|
+
.entity-search {
|
|
525
|
+
max-width: 600px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.search-dropdown {
|
|
529
|
+
position: absolute;
|
|
530
|
+
top: 100%;
|
|
531
|
+
left: 0;
|
|
532
|
+
right: 0;
|
|
533
|
+
z-index: 100;
|
|
534
|
+
max-height: 300px;
|
|
535
|
+
overflow-y: auto;
|
|
536
|
+
margin-top: -8px;
|
|
537
|
+
}
|
|
538
|
+
</style>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Usage:
|
|
542
|
+
|
|
543
|
+
```vue
|
|
544
|
+
<EntitySearch
|
|
545
|
+
label="Search for a company"
|
|
546
|
+
icon="mdi-domain"
|
|
547
|
+
:flavors="['organization']"
|
|
548
|
+
@selected="onCompanySelected"
|
|
549
|
+
/>
|
|
550
|
+
```
|