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
3 changes: 1 addition & 2 deletions apps/web/src/components/pullRequest/PullRequestsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,12 @@ import { INVOLVEMENT_TABS, SORT_OPTIONS, STATE_TABS } from "./pullRequestListOpt
import { PullRequestsColumn } from "./PullRequestsColumn";
import { PullRequestListGroups } from "./PullRequestListGroups";
import {
pullRequestGroups,
pullRequestListEntryId,
usePullRequestDisplayGroups,
usePullRequestQueryFilters,
usePullRequestScope,
type PullRequestsSearch,
} from "./usePullRequestListView";
import { pullRequestGroups, pullRequestListEntryId } from "./pullRequestListGroups.logic";
import { PullRequestListEmptyState } from "./PullRequestListEmptyState";
import { PullRequestListGhost } from "./PullRequestGhosts";
import { type PullRequestRowTarget } from "./PullRequestRow";
Expand Down
127 changes: 127 additions & 0 deletions apps/web/src/components/pullRequest/pullRequestListGroups.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type {
PullRequestInvolvement,
PullRequestListFilters,
PullRequestListResult,
} from "@t3tools/contracts";

import type { PullRequestTabStatusSeed } from "~/components/RightPanelTabs";
import { pullRequestSurfaceId } from "~/rightPanelStore";
import {
filterPullRequestsByInvolvement,
groupPullRequestsByInvolvement,
matchesPullRequestFilters,
matchesPullRequestQuery,
partitionPullRequestsWithPriority,
pullRequestEntryKey,
pullRequestEntryViewer,
type EnvironmentPullRequestEntry,
type PullRequestPartitionsSnapshot,
} from "./pullRequestList.logic";

export const pullRequestListEntryId = (target: Parameters<typeof pullRequestSurfaceId>[0]) =>
pullRequestSurfaceId({ ...target, repository: target.repository.toLowerCase() });

/** The held rows narrowed to what the reader asked for, in the groups the list shows. */
export function pullRequestGroups({
known,
providers,
viewers,
involvement,
localFilters,
hasLocalFilters,
text,
answeredLocally,
baselineEmpty,
held,
authored,
reviewing,
partitionsWanted,
}: {
known: ReadonlyArray<EnvironmentPullRequestEntry>;
providers: PullRequestListResult["providers"];
viewers: PullRequestListResult["viewers"];
involvement: PullRequestInvolvement;
localFilters: PullRequestListFilters;
hasLocalFilters: boolean;
text: string;
answeredLocally: boolean;
baselineEmpty: boolean;
held: PullRequestPartitionsSnapshot | undefined;
authored: ReadonlyArray<EnvironmentPullRequestEntry> | undefined;
reviewing: ReadonlyArray<EnvironmentPullRequestEntry> | undefined;
partitionsWanted: boolean;
}) {
const searchingHosts = new Set(
providers.flatMap((provider) => (provider.searchesOnHost ? [provider.host] : [])),
);

const entries = (() => {
const involvementEntries = filterPullRequestsByInvolvement(known, viewers, involvement);
// The hosts search more than the row shows — a body, a review, a commit message — so once
// their answer is in, narrowing it again here would throw away matches the reader asked for.
// The local pass stands in for the answer that has not arrived yet, and for the hosts that
// answered without searching at all: Azure DevOps has no text filter, so its rows arrive
// whole and would otherwise sit under a search that never touched them.
const narrowedEntries = hasLocalFilters
? involvementEntries.filter((entry) =>
matchesPullRequestFilters(entry, localFilters, pullRequestEntryViewer(entry, viewers)),
)
: involvementEntries;
if (text.length === 0) {
return narrowedEntries.toSorted((left, right) =>
right.updatedAt.localeCompare(left.updatedAt),
);
}
return narrowedEntries.filter(
(entry) =>
(answeredLocally && searchingHosts.has(entry.host)) || matchesPullRequestQuery(entry, text),
);
})();

const listedPullRequestTabStatuses: Record<string, PullRequestTabStatusSeed> = Object.fromEntries(
entries.map((entry) => [
pullRequestSurfaceId(entry),
{
state: entry.state,
isDraft: entry.isDraft,
},
]),
);

const groups = (() => {
if (involvement !== "all") return [{ key: "others" as const, label: "", entries }];
if (baselineEmpty) {
return groupPullRequestsByInvolvement(entries, viewers);
}
const narrow = (rows: ReadonlyArray<EnvironmentPullRequestEntry> | undefined) =>
rows === undefined || !hasLocalFilters
? rows
: rows.filter((entry) =>
matchesPullRequestFilters(entry, localFilters, pullRequestEntryViewer(entry, viewers)),
);
const narrowedAuthored = narrow(partitionsWanted ? (authored ?? held?.authored) : undefined);
const narrowedReviewing = narrow(partitionsWanted ? (reviewing ?? held?.reviewing) : undefined);
if (narrowedAuthored === undefined || narrowedReviewing === undefined) {
return groupPullRequestsByInvolvement(entries, viewers);
}
return partitionPullRequestsWithPriority(entries, narrowedAuthored, narrowedReviewing);
})();

const entriesByStatsKey: ReadonlyMap<string, EnvironmentPullRequestEntry> = new Map(
groups.flatMap((group) =>
group.entries.map((entry) => [pullRequestEntryKey(entry), entry] as const),
),
);
const heldPullRequestsBySurface = new Map(
groups.flatMap((group) =>
group.entries.map((entry) => [pullRequestListEntryId(entry), entry] as const),
),
);
return {
entries,
listedPullRequestTabStatuses,
groups,
entriesByStatsKey,
heldPullRequestsBySurface,
};
}
116 changes: 1 addition & 115 deletions apps/web/src/components/pullRequest/usePullRequestListView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,14 @@ import {
} from "@t3tools/contracts";
import { useState } from "react";

import type { PullRequestTabStatusSeed } from "~/components/RightPanelTabs";
import { pullRequestSurfaceId } from "~/rightPanelStore";
import { useAllEnvironmentShellsBootstrapped, useProjects } from "~/state/entities";
import { useEnvironments } from "~/state/environments";
import { useDebouncedValue } from "~/state/queries";
import {
applyPullRequestOverrides,
filterPullRequestsByInvolvement,
findProjectForRepository,
findScopedProject,
groupPullRequestsByInvolvement,
matchesPullRequestFilters,
matchesPullRequestQuery,
partitionPullRequestsWithPriority,
pullRequestDiffStatKey,
pullRequestEntryKey,
pullRequestEntryViewer,
Expand All @@ -40,12 +34,12 @@ import {
type PullRequestDiffStats,
type PullRequestGroup,
type PullRequestListOverride,
type PullRequestPartitionsSnapshot,
} from "./pullRequestList.logic";
import type { PullRequestExpectedHost } from "./PullRequestListFilters";
import type { PullRequestListPreferences, PullRequestListSort } from "./pullRequestListPreferences";
import { assignProjectsToEnvironments } from "./pullRequestProjectAssignment.logic";
import { pullRequestFilterProjects } from "./pullRequestProjectFilter.logic";
import { pullRequestListEntryId } from "./pullRequestListGroups.logic";

export interface PullRequestsSearch extends PullRequestListPreferences {
readonly environmentId?: EnvironmentId;
Expand All @@ -60,9 +54,6 @@ export interface PullRequestsSearch extends PullRequestListPreferences {

const SEARCH_DEBOUNCE_MS = 250;

export const pullRequestListEntryId = (target: Parameters<typeof pullRequestSurfaceId>[0]) =>
pullRequestSurfaceId({ ...target, repository: target.repository.toLowerCase() });

/** Which servers and projects the page reads, from the URL and what this client knows. */
export function usePullRequestScope(search: PullRequestsSearch) {
const { environments } = useEnvironments();
Expand Down Expand Up @@ -225,111 +216,6 @@ export function usePullRequestQueryFilters(search: PullRequestsSearch, typedQuer
};
}

/** The held rows narrowed to what the reader asked for, in the groups the list shows. */
export function pullRequestGroups({
known,
providers,
viewers,
involvement,
localFilters,
hasLocalFilters,
text,
answeredLocally,
baselineEmpty,
held,
authored,
reviewing,
partitionsWanted,
}: {
known: ReadonlyArray<EnvironmentPullRequestEntry>;
providers: PullRequestListResult["providers"];
viewers: PullRequestListResult["viewers"];
involvement: PullRequestInvolvement;
localFilters: PullRequestListFilters;
hasLocalFilters: boolean;
text: string;
answeredLocally: boolean;
baselineEmpty: boolean;
held: PullRequestPartitionsSnapshot | undefined;
authored: ReadonlyArray<EnvironmentPullRequestEntry> | undefined;
reviewing: ReadonlyArray<EnvironmentPullRequestEntry> | undefined;
partitionsWanted: boolean;
}) {
const searchingHosts = new Set(
providers.flatMap((provider) => (provider.searchesOnHost ? [provider.host] : [])),
);

const entries = (() => {
const involvementEntries = filterPullRequestsByInvolvement(known, viewers, involvement);
// The hosts search more than the row shows — a body, a review, a commit message — so once
// their answer is in, narrowing it again here would throw away matches the reader asked for.
// The local pass stands in for the answer that has not arrived yet, and for the hosts that
// answered without searching at all: Azure DevOps has no text filter, so its rows arrive
// whole and would otherwise sit under a search that never touched them.
const narrowedEntries = hasLocalFilters
? involvementEntries.filter((entry) =>
matchesPullRequestFilters(entry, localFilters, pullRequestEntryViewer(entry, viewers)),
)
: involvementEntries;
if (text.length === 0) {
return narrowedEntries.toSorted((left, right) =>
right.updatedAt.localeCompare(left.updatedAt),
);
}
return narrowedEntries.filter(
(entry) =>
(answeredLocally && searchingHosts.has(entry.host)) || matchesPullRequestQuery(entry, text),
);
})();

const listedPullRequestTabStatuses: Record<string, PullRequestTabStatusSeed> = Object.fromEntries(
entries.map((entry) => [
pullRequestSurfaceId(entry),
{
state: entry.state,
isDraft: entry.isDraft,
},
]),
);

const groups = (() => {
if (involvement !== "all") return [{ key: "others" as const, label: "", entries }];
if (baselineEmpty) {
return groupPullRequestsByInvolvement(entries, viewers);
}
const narrow = (rows: ReadonlyArray<EnvironmentPullRequestEntry> | undefined) =>
rows === undefined || !hasLocalFilters
? rows
: rows.filter((entry) =>
matchesPullRequestFilters(entry, localFilters, pullRequestEntryViewer(entry, viewers)),
);
const narrowedAuthored = narrow(partitionsWanted ? (authored ?? held?.authored) : undefined);
const narrowedReviewing = narrow(partitionsWanted ? (reviewing ?? held?.reviewing) : undefined);
if (narrowedAuthored === undefined || narrowedReviewing === undefined) {
return groupPullRequestsByInvolvement(entries, viewers);
}
return partitionPullRequestsWithPriority(entries, narrowedAuthored, narrowedReviewing);
})();

const entriesByStatsKey: ReadonlyMap<string, EnvironmentPullRequestEntry> = new Map(
groups.flatMap((group) =>
group.entries.map((entry) => [pullRequestEntryKey(entry), entry] as const),
),
);
const heldPullRequestsBySurface = new Map(
groups.flatMap((group) =>
group.entries.map((entry) => [pullRequestListEntryId(entry), entry] as const),
),
);
return {
entries,
listedPullRequestTabStatuses,
groups,
entriesByStatsKey,
heldPullRequestsBySurface,
};
}

/**
* The hosts the list has answered for (kept while a host filter narrows the answer), and the
* groups as shown with measured sizes, optimistic actions, and the reader's sort.
Expand Down
Loading