mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
Complete filtered user search pages
Fetch another user-search page when selected or archived results leave the visible result viewport underfilled. Cover fully filtered and non-scrollable first pages, and capture duplicate-name UI evidence. Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
@@ -142,6 +142,24 @@ export function CreateAgentRespondToField({
|
||||
),
|
||||
[allowlistSet, isArchivedDiscovery, userSearchResults],
|
||||
);
|
||||
const searchViewportRef = React.useRef<HTMLDivElement>(null);
|
||||
React.useEffect(() => {
|
||||
const viewport = searchViewportRef.current;
|
||||
if (
|
||||
!viewport ||
|
||||
!userSearchQuery.hasNextPage ||
|
||||
userSearchQuery.isFetchingNextPage ||
|
||||
viewport.scrollHeight > viewport.clientHeight
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
void userSearchQuery.fetchNextPage();
|
||||
}, [
|
||||
userSearchQuery.fetchNextPage,
|
||||
userSearchQuery.hasNextPage,
|
||||
userSearchQuery.isFetchingNextPage,
|
||||
]);
|
||||
const handleSearchScroll = useUserSearchFetchMoreOnScroll(userSearchQuery);
|
||||
|
||||
const pasteParsed = React.useMemo(
|
||||
@@ -261,6 +279,7 @@ export function CreateAgentRespondToField({
|
||||
onAddRawPubkey={handleAddRawPubkey}
|
||||
onAddSearchResult={handleAddSearchResult}
|
||||
onSearchScroll={handleSearchScroll}
|
||||
searchViewportRef={searchViewportRef}
|
||||
onPasteTextChange={setPasteText}
|
||||
onQueryChange={setQuery}
|
||||
onRemove={handleRemove}
|
||||
@@ -296,6 +315,7 @@ function AllowlistPicker({
|
||||
onAddRawPubkey,
|
||||
onAddSearchResult,
|
||||
onSearchScroll,
|
||||
searchViewportRef,
|
||||
onPasteTextChange,
|
||||
onQueryChange,
|
||||
onRemove,
|
||||
@@ -318,6 +338,7 @@ function AllowlistPicker({
|
||||
onAddRawPubkey: (pubkey: string) => void;
|
||||
onAddSearchResult: (user: UserSearchResult) => void;
|
||||
onSearchScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
||||
searchViewportRef: React.RefObject<HTMLDivElement | null>;
|
||||
onPasteTextChange: (value: string) => void;
|
||||
onQueryChange: (value: string) => void;
|
||||
onRemove: (pubkey: string) => void;
|
||||
@@ -414,66 +435,69 @@ function AllowlistPicker({
|
||||
<p className="px-2 py-1 text-sm text-muted-foreground">
|
||||
Searching…
|
||||
</p>
|
||||
) : searchResults.length > 0 ? (
|
||||
) : (
|
||||
<div
|
||||
className="max-h-44 space-y-1 overflow-y-auto"
|
||||
onScroll={onSearchScroll}
|
||||
ref={searchViewportRef}
|
||||
>
|
||||
{searchResults.map((result) => (
|
||||
{searchResults.length > 0 ? (
|
||||
searchResults.map((result) => (
|
||||
<button
|
||||
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
data-testid={`agent-respond-to-result-${result.pubkey}`}
|
||||
key={result.pubkey}
|
||||
onClick={() => onAddSearchResult(result)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<UserAvatar
|
||||
avatarUrl={result.avatarUrl}
|
||||
displayName={formatSearchUserName(result)}
|
||||
size="xs"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium leading-5">
|
||||
{formatSearchUserName(result)}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{formatSearchUserSecondary(result)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Add</span>
|
||||
</button>
|
||||
))
|
||||
) : queryIsHexPubkey ? (
|
||||
<button
|
||||
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
data-testid={`agent-respond-to-result-${result.pubkey}`}
|
||||
key={result.pubkey}
|
||||
onClick={() => onAddSearchResult(result)}
|
||||
data-testid="agent-respond-to-add-raw-pubkey"
|
||||
onClick={() => onAddRawPubkey(deferredQuery.toLowerCase())}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<UserAvatar
|
||||
avatarUrl={result.avatarUrl}
|
||||
displayName={formatSearchUserName(result)}
|
||||
avatarUrl={null}
|
||||
displayName={truncatePubkey(deferredQuery)}
|
||||
size="xs"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium leading-5">
|
||||
{formatSearchUserName(result)}
|
||||
{truncatePubkey(deferredQuery)}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{formatSearchUserSecondary(result)}
|
||||
Add pubkey directly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Add</span>
|
||||
</button>
|
||||
))}
|
||||
) : (
|
||||
<p className="px-2 py-1 text-sm text-muted-foreground">
|
||||
No matching users.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : queryIsHexPubkey ? (
|
||||
<button
|
||||
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
data-testid="agent-respond-to-add-raw-pubkey"
|
||||
onClick={() => onAddRawPubkey(deferredQuery.toLowerCase())}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<UserAvatar
|
||||
avatarUrl={null}
|
||||
displayName={truncatePubkey(deferredQuery)}
|
||||
size="xs"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium leading-5">
|
||||
{truncatePubkey(deferredQuery)}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
Add pubkey directly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Add</span>
|
||||
</button>
|
||||
) : (
|
||||
<p className="px-2 py-1 text-sm text-muted-foreground">
|
||||
No matching users.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -165,6 +165,9 @@ test("selected-people search keeps same-name people independently selectable", a
|
||||
await expect(firstWill).toContainText("11111111…1111");
|
||||
await expect(secondWill).toContainText("Will");
|
||||
await expect(secondWill).toContainText("22222222…2222");
|
||||
await page
|
||||
.getByTestId("agent-respond-to-allowlist")
|
||||
.screenshot({ path: `${SHOTS}/duplicate-will-selected-people.png` });
|
||||
|
||||
await secondWill.click();
|
||||
await expect(
|
||||
@@ -173,6 +176,56 @@ test("selected-people search keeps same-name people independently selectable", a
|
||||
await expect(firstWill).toHaveCount(0);
|
||||
});
|
||||
|
||||
for (const selectedFirstPageCount of [50, 49]) {
|
||||
test(`selected-people search fetches past a ${
|
||||
selectedFirstPageCount === 50 ? "fully filtered" : "non-scrollable"
|
||||
} first page`, async ({ page }) => {
|
||||
const firstPageProfiles = Array.from({ length: 50 }, (_, index) => ({
|
||||
pubkey: (index + 1).toString(16).padStart(64, "0"),
|
||||
displayName: `Will ${String(index).padStart(2, "0")}`,
|
||||
}));
|
||||
const targetPubkey = "f".repeat(64);
|
||||
const agent = TEST_IDENTITIES.charlie;
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [
|
||||
{
|
||||
pubkey: agent.pubkey,
|
||||
name: "Hack Day Helper",
|
||||
status: "running",
|
||||
channelNames: ["general"],
|
||||
respondTo: "allowlist",
|
||||
respondToAllowlist: firstPageProfiles
|
||||
.slice(0, selectedFirstPageCount)
|
||||
.map((profile) => profile.pubkey),
|
||||
},
|
||||
],
|
||||
searchProfiles: [
|
||||
...firstPageProfiles,
|
||||
{ pubkey: targetPubkey, displayName: "Will Target" },
|
||||
],
|
||||
});
|
||||
await page.goto("/");
|
||||
await openAgentAccessDialog(page, agent.pubkey);
|
||||
|
||||
await page.getByTestId("agent-respond-to-search").fill("Will");
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(() =>
|
||||
(window.__BUZZ_E2E_COMMAND_LOG__ ?? [])
|
||||
.filter((entry) => entry.command === "search_users")
|
||||
.map((entry) => entry.payload),
|
||||
),
|
||||
{ message: "search should request the second result page" },
|
||||
)
|
||||
.toContainEqual(expect.objectContaining({ cursor: "2" }));
|
||||
await expect(
|
||||
page.getByTestId(`agent-respond-to-result-${targetPubkey}`),
|
||||
).toContainText("Will Target");
|
||||
});
|
||||
}
|
||||
|
||||
test("full agent editor tightens the exact sidebar agent instance", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
const MOCK_VIEWER_PUBKEY = "deadbeef".repeat(8);
|
||||
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
||||
const SHOTS = "test-results/mentions";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
@@ -715,6 +716,11 @@ for (const selectedWill of ["first", "second"] as const) {
|
||||
await expect(firstWill).toBeVisible();
|
||||
await expect(secondWill).toBeVisible();
|
||||
await expect(dropdown.getByTestId("mention-collision-npub")).toHaveCount(2);
|
||||
if (selectedWill === "first") {
|
||||
await dropdown.screenshot({
|
||||
path: `${SHOTS}/duplicate-will-autocomplete.png`,
|
||||
});
|
||||
}
|
||||
|
||||
await dropdown.getByTestId(`mention-suggestion-${selectedPubkey}`).click();
|
||||
await page.keyboard.type(`hello ${selectedWill}`);
|
||||
|
||||
Reference in New Issue
Block a user