Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/src/components/pullRequest/PullRequestsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,8 @@ export function PullRequestsPage() {
heldPullRequestsBySurface,
} = pullRequestGroups({
known: ordered?.key === filterKey ? ordered.entries : (listData?.entries ?? []),
// The baseline was read without typed qualifiers, and rows cannot be checked for `checks:`.
baseline: typedParsed.filters.checks === undefined ? baselineQuery.data?.entries : undefined,
providers: baselineQuery.data?.providers ?? listData?.providers ?? [],
viewers,
involvement: search.involvement,
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/components/pullRequest/pullRequestList.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
reusePullRequestEntries,
settlePullRequestOverrides,
} from "./pullRequestList.logic";
import { pullRequestGroups } from "./pullRequestListGroups.logic";
import {
readPullRequestListPreferences,
writePullRequestListPreferences,
Expand Down Expand Up @@ -177,6 +178,43 @@ describe("ranking what a search found", () => {
rankPullRequestMatches([mentioned, titled], "welcome wizard").map((entry) => entry.number),
).toEqual([1, 2]);
});

it("keeps loaded partial-word matches the host's whole-word search missed, after its answer", () => {
const bilal = { login: "bilal", name: null, avatarUrl: null };
const hosted = row({ number: 1, title: "Notifications match the body text", author: bilal });
const toast = row({ number: 2, title: "Threads show a completed toast", author: bilal });
const toaster = row({ number: 3, title: "Toaster fixtures cover retries", author: bilal });
const theirs = row({ number: 4, title: "Toasted sidebar" });
const unrelated = row({ number: 5, title: "Cursor no longer adds attribution", author: bilal });

const { entries } = pullRequestGroups({
known: [hosted, toast],
baseline: [theirs, unrelated, toaster, toast],
providers: [
{
host: "github.com",
kind: SourceControlProviderKind.make("github"),
searchesOnHost: true,
projectCount: 1,
configured: true,
detail: null,
},
],
viewers: VIEWERS,
involvement: "all",
localFilters: { author: "bilal" },
hasLocalFilters: true,
text: "toa",
answeredLocally: true,
baselineEmpty: false,
held: undefined,
authored: undefined,
reviewing: undefined,
partitionsWanted: false,
});

expect(entries.map((entry) => entry.number)).toEqual([1, 2, 3]);
});
});

describe("default merge-readiness ranking", () => {
Expand Down
27 changes: 21 additions & 6 deletions apps/web/src/components/pullRequest/pullRequestListGroups.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
partitionPullRequestsWithPriority,
pullRequestEntryKey,
pullRequestEntryViewer,
rankPullRequestMatches,
type EnvironmentPullRequestEntry,
type PullRequestPartitionsSnapshot,
} from "./pullRequestList.logic";
Expand All @@ -25,6 +26,7 @@ export const pullRequestListEntryId = (target: Parameters<typeof pullRequestSurf
/** The held rows narrowed to what the reader asked for, in the groups the list shows. */
export function pullRequestGroups({
known,
baseline,
providers,
viewers,
involvement,
Expand All @@ -39,6 +41,8 @@ export function pullRequestGroups({
partitionsWanted,
}: {
known: ReadonlyArray<EnvironmentPullRequestEntry>;
/** The text-free rows already loaded, or undefined when the typed filters cannot vet them. */
baseline: ReadonlyArray<EnvironmentPullRequestEntry> | undefined;
providers: PullRequestListResult["providers"];
viewers: PullRequestListResult["viewers"];
involvement: PullRequestInvolvement;
Expand All @@ -56,22 +60,33 @@ export function pullRequestGroups({
providers.flatMap((provider) => (provider.searchesOnHost ? [provider.host] : [])),
);

const entries = (() => {
const involvementEntries = filterPullRequestsByInvolvement(known, viewers, involvement);
// Host text searches cover fields absent from rows, so do not narrow their matches locally.
// Filter text locally while results are pending or when the host has no text search.
const narrowedEntries = hasLocalFilters
const narrowRows = (rows: ReadonlyArray<EnvironmentPullRequestEntry>) => {
const involvementEntries = filterPullRequestsByInvolvement(rows, viewers, involvement);
return hasLocalFilters
? involvementEntries.filter((entry) =>
matchesPullRequestFilters(entry, localFilters, pullRequestEntryViewer(entry, viewers)),
)
: involvementEntries;
};

const entries = (() => {
const narrowedEntries = narrowRows(known);
if (text.length === 0) {
return narrowedEntries.toSorted(comparePullRequestsByRecency);
}
return narrowedEntries.filter(
// Host text searches cover fields absent from rows, so do not narrow their matches locally.
// Filter text locally while results are pending or when the host has no text search.
const matched = narrowedEntries.filter(
(entry) =>
(answeredLocally && searchingHosts.has(entry.host)) || matchesPullRequestQuery(entry, text),
);
// Host search matches whole words, so it can miss loaded rows a partial word matches here.
// Those follow the host's ranked answer rather than reshuffling it.
const listed = new Set(matched.map(pullRequestEntryKey));
const missed = narrowRows(baseline ?? []).filter(
(entry) => !listed.has(pullRequestEntryKey(entry)) && matchesPullRequestQuery(entry, text),
);
return missed.length === 0 ? matched : [...matched, ...rankPullRequestMatches(missed, text)];
})();

const listedPullRequestTabStatuses: Record<string, PullRequestTabStatusSeed> = Object.fromEntries(
Expand Down
Loading