From 54d5a6cea9dc26bf211d4ab4738af51e828cd0b0 Mon Sep 17 00:00:00 2001 From: Mahad Kalam Date: Tue, 1 Sep 2026 02:43:49 +0000 Subject: [PATCH 1/3] Consolidate user search combobox behaviour Amp-Thread-ID: https://ampcode.com/threads/T-01a05ac9-3763-74fe-8ebd-b4cd88c93554 Co-authored-by: Amp --- app/javascript/components/UserPicker.svelte | 233 ++++++---------- .../components/UserSearchCombobox.svelte | 182 +++++++++++++ app/javascript/pages/Admin/Timeline.svelte | 248 +++++++++--------- test/system/admin/timeline_test.rb | 56 ++++ 4 files changed, 440 insertions(+), 279 deletions(-) create mode 100644 app/javascript/components/UserSearchCombobox.svelte diff --git a/app/javascript/components/UserPicker.svelte b/app/javascript/components/UserPicker.svelte index 9986a9b73..c17eb406c 100644 --- a/app/javascript/components/UserPicker.svelte +++ b/app/javascript/components/UserPicker.svelte @@ -12,8 +12,8 @@ {#snippet avatar(user: UserPickerResult, className: string)} @@ -151,65 +58,93 @@ {/if} {/snippet} -
-
-
-
- - -
+ (selected = user)} +> + {#snippet children({ + query, + results, + open, + highlight, + searchError, + listboxId, + activeDescendant, + setQuery, + select, + handleKeydown, + handleOptionKeydown, + })} +
+
+
+
+ + setQuery(event.currentTarget.value)} + onkeydown={handleKeydown} + autocomplete="off" + role="combobox" + aria-autocomplete="list" + aria-controls={listboxId} + aria-expanded={open && !searchError} + aria-activedescendant={activeDescendant} + class="w-full rounded-lg border border-surface-200 bg-input py-2 pl-10 pr-3 text-sm text-surface-content placeholder-gray-500 focus:border-primary focus:outline-none" + /> +
- {#if open} -
- {#if results.length} - {#each results as user, i} -
selectUser(user)} - onkeydown={(e) => handleOptionKeydown(e, user)} - > - {@render avatar(user, "h-8 w-8 rounded-full")} -
-
- {user.display_name} -
-
- ID: {user.id}{user.created_at - ? ` · Created: ${formatUtcDate(user.created_at) ?? user.created_at}` - : ""} + {#if open && !searchError} +
+ {#if results.length} + {#each results as user, i} +
select(user)} + onkeydown={(event) => handleOptionKeydown(event, user)} + > + {@render avatar(user, "h-8 w-8 rounded-full")} +
+
+ {user.display_name} +
+
+ ID: {user.id}{user.created_at + ? ` · Created: ${formatUtcDate(user.created_at) ?? user.created_at}` + : ""} +
+
-
-
- {/each} - {:else} - No users found + {/each} + {:else} + No users found + {/if} +
{/if}
- {/if} -
+ {/snippet} +
{#if selected} diff --git a/app/javascript/components/UserSearchCombobox.svelte b/app/javascript/components/UserSearchCombobox.svelte new file mode 100644 index 000000000..2b714e9bb --- /dev/null +++ b/app/javascript/components/UserSearchCombobox.svelte @@ -0,0 +1,182 @@ + + + + +{@render children({ + query, + results, + open, + highlight, + searching, + searchError, + listboxId, + activeDescendant, + setQuery, + select, + handleKeydown, + handleOptionKeydown, + openResults, + closeResults, +})} diff --git a/app/javascript/pages/Admin/Timeline.svelte b/app/javascript/pages/Admin/Timeline.svelte index a4e22ca32..fd92e4e51 100644 --- a/app/javascript/pages/Admin/Timeline.svelte +++ b/app/javascript/pages/Admin/Timeline.svelte @@ -4,6 +4,7 @@ import AdminUserMention from "../../components/AdminUserMention.svelte"; import Button from "../../components/Button.svelte"; import TextInput from "../../components/TextInput.svelte"; + import UserSearchCombobox from "../../components/UserSearchCombobox.svelte"; import { adminTimeline, users } from "../../api"; type UserSummary = { @@ -101,12 +102,6 @@ }); let selected = $state(untrack(() => selected_users)); - let query = $state(""); - let results = $state([]); - let searching = $state(false); - let searchError = $state(false); - let showResults = $state(false); - let searchSequence = 0; const canMutate = $derived( ["admin", "superadmin", "ultraadmin"].includes(current_user.admin_level), ); @@ -188,46 +183,13 @@ $effect(() => { selected = selected_users; }); - $effect(() => { - const value = query.trim(); - const sequence = ++searchSequence; - if (value.length < 2 && !/^\d+$/.test(value)) { - results = []; - showResults = false; - searching = false; - return; - } - const timer = setTimeout(async () => { - searching = true; - searchError = false; - try { - const response = await fetch( - adminTimeline.searchUsers.path({ query: { query: value } }), - ); - if (!response.ok) throw new Error(); - if (sequence === searchSequence) { - results = await response.json(); - showResults = true; - } - } catch { - if (sequence === searchSequence) { - searchError = true; - showResults = true; - } - } finally { - if (sequence === searchSequence) searching = false; - } - }, 300); - return () => clearTimeout(timer); - }); function selectUser(user: UserSummary) { if (!selected.some(({ id }) => id === user.id)) selected = [...selected, user]; - query = ""; - results = []; - showResults = false; } + const canSearchUsers = (query: string) => + query.length >= 2 || /^\d+$/.test(query); async function applyPreset(period: string) { const response = await fetch( adminTimeline.leaderboardUsers.path({ query: { period } }), @@ -236,26 +198,6 @@ return alert("Could not load preset users. Please try again."); selected = (await response.json()).users; } - async function handleSearchKeydown(event: KeyboardEvent) { - if (event.key === "Escape") { - query = ""; - showResults = false; - } - if (event.key === "Enter") { - event.preventDefault(); - if (results[0]) return selectUser(results[0]); - const value = query.trim(); - if (value.length < 2 && !/^\d+$/.test(value)) return; - searching = true; - const response = await fetch( - adminTimeline.searchUsers.path({ query: { query: value, limit: 1 } }), - ); - searching = false; - if (!response.ok) return; - const [user] = await response.json(); - if (user) selectUser(user); - } - } async function setTrust(column: Column) { if (!canMutate) return alert("you dont have human rights to do that"); let options = @@ -312,77 +254,123 @@ value={date} />
-
-
- - {#if searching}{:else}{/if} - - query.length >= 2 && (showResults = true)} - onblur={() => setTimeout(() => (showResults = false), 200)} - autocomplete="off" - placeholder="Add user by name/email/id..." - class="w-full rounded-md bg-darker py-2 pl-10 pr-3 text-sm text-surface-content placeholder-gray-300 focus:border-transparent focus:outline-none" - /> -
- {#if showResults}
- {#if searchError}
- Error searching users -
{:else if results.length === 0} + {#if searchError}
+ Error searching users +
{:else if results.length === 0}
+ No users found +
{/if} + {#each results as user, index}{/each} +
{/if} +
+ {/snippet} +