Skip to content

Commit 92d4a2e

Browse files
fix(web): scope pull request errors to their environment (#6490)
Co-authored-by: Julius Marminge <julius0216@outlook.com>
1 parent 2fab18e commit 92d4a2e

5 files changed

Lines changed: 70 additions & 17 deletions

File tree

apps/web/src/components/pullRequest/PullRequestListFilters.test.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CircleIcon } from "lucide-react";
33
import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
44
import { describe, expect, it, vi } from "vite-plus/test";
55

6-
import { PullRequestFiltersMenu } from "./PullRequestListFilters";
6+
import { PullRequestFiltersMenu, pullRequestProjectKey } from "./PullRequestListFilters";
77

88
function findValueChange(
99
node: ReactNode,
@@ -129,7 +129,7 @@ describe("pull request filters menu", () => {
129129
const radioGroup = findValueChange(view);
130130
expect(radioGroup).toBeDefined();
131131

132-
radioGroup?.props.onValueChange(`${environmentId} ${projectId}`);
132+
radioGroup?.props.onValueChange(pullRequestProjectKey({ id: projectId, environmentId }));
133133
expect(onProject).not.toHaveBeenCalled();
134134

135135
radioGroup?.props.onValueChange("all");
@@ -159,7 +159,23 @@ describe("pull request filters menu", () => {
159159
const radioGroup = findValueChange(view);
160160
expect(radioGroup).toBeDefined();
161161

162-
radioGroup?.props.onValueChange(`env-2 ${projectId}`);
162+
radioGroup?.props.onValueChange(
163+
pullRequestProjectKey({ id: projectId, environmentId: "env-2" as EnvironmentId }),
164+
);
163165
expect(onProject).toHaveBeenCalledWith(projectId, "env-2");
164166
});
167+
168+
it("does not collide when environment and project ids contain spaces", () => {
169+
expect(
170+
pullRequestProjectKey({
171+
environmentId: "a b" as EnvironmentId,
172+
id: "c" as ProjectId,
173+
}),
174+
).not.toBe(
175+
pullRequestProjectKey({
176+
environmentId: "a" as EnvironmentId,
177+
id: "b c" as ProjectId,
178+
}),
179+
);
180+
});
165181
});

apps/web/src/components/pullRequest/PullRequestListFilters.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ const UNFILTERED_VALUE = "all";
122122
* A project's own radio value, carrying the server along with the id: the id alone is only
123123
* unique within its own server, so two rows sharing one would otherwise both read as checked.
124124
*/
125-
const projectMenuValue = (project: {
125+
export const pullRequestProjectKey = (project: {
126126
readonly id: ProjectId;
127127
readonly environmentId: EnvironmentId;
128-
}) => `${project.environmentId} ${project.id}`;
128+
}) => JSON.stringify([project.environmentId, project.id]);
129129

130130
const DRAFT_OPTIONS = [
131131
{ value: UNFILTERED_VALUE, label: "All", Icon: LayersIcon },
@@ -247,7 +247,7 @@ export function PullRequestFiltersMenu({
247247
* the reader is already choosing between projects, rather than as a count above the list
248248
* that says something is missing without saying which.
249249
*/
250-
unavailable: ReadonlyMap<ProjectId, string>;
250+
unavailable: ReadonlyMap<string, string>;
251251
/** The environment comes with the project id, since picking a row picks a specific server's copy of it. */
252252
onProject: (projectId: ProjectId | undefined, environmentId: EnvironmentId | undefined) => void;
253253
}) {
@@ -350,7 +350,7 @@ export function PullRequestFiltersMenu({
350350
value={
351351
projectId === undefined || projectEnvironmentId === undefined
352352
? ALL_PROJECTS_VALUE
353-
: projectMenuValue({ id: projectId, environmentId: projectEnvironmentId })
353+
: pullRequestProjectKey({ id: projectId, environmentId: projectEnvironmentId })
354354
}
355355
onValueChange={(next) => {
356356
if (next === ALL_PROJECTS_VALUE) {
@@ -359,7 +359,7 @@ export function PullRequestFiltersMenu({
359359
}
360360
// The value carries both halves, since the id alone cannot tell two servers' rows
361361
// apart once they share one.
362-
const project = projects.find((candidate) => projectMenuValue(candidate) === next);
362+
const project = projects.find((candidate) => pullRequestProjectKey(candidate) === next);
363363
if (
364364
project !== undefined &&
365365
(project.id !== projectId || project.environmentId !== projectEnvironmentId)
@@ -379,14 +379,16 @@ export function PullRequestFiltersMenu({
379379
as a broken menu rather than as a workspace with three unreadable repositories. */}
380380
{projects
381381
.toSorted(
382-
(left, right) => Number(unavailable.has(left.id)) - Number(unavailable.has(right.id)),
382+
(left, right) =>
383+
Number(unavailable.has(pullRequestProjectKey(left))) -
384+
Number(unavailable.has(pullRequestProjectKey(right))),
383385
)
384386
.map((project) => {
385-
const reason = unavailable.get(project.id);
387+
const reason = unavailable.get(pullRequestProjectKey(project));
386388
return (
387389
<MenuRadioItem
388-
key={projectMenuValue(project)}
389-
value={projectMenuValue(project)}
390+
key={pullRequestProjectKey(project)}
391+
value={pullRequestProjectKey(project)}
390392
disabled={reason !== undefined}
391393
title={reason}
392394
>

apps/web/src/components/pullRequest/pullRequestList.logic.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EnvironmentId, PullRequestListEntry } from "@t3tools/contracts";
1+
import type { EnvironmentId, ProjectId, PullRequestListEntry } from "@t3tools/contracts";
22
import { describe, expect, it } from "vite-plus/test";
33

44
import {
@@ -710,6 +710,20 @@ describe("merging the environments' own listings", () => {
710710
);
711711
});
712712

713+
it("keeps project errors scoped to the environment that reported them", () => {
714+
const error = {
715+
projectId: "project-1" as ProjectId,
716+
projectTitle: "Web",
717+
message: "Not signed in",
718+
} as const;
719+
const merged = mergePullRequestLists([
720+
[ENV_1, answer({ errors: [error] })],
721+
[ENV_2, answer()],
722+
]);
723+
724+
expect(merged?.errors).toEqual([{ ...error, environmentId: ENV_1 }]);
725+
});
726+
713727
it("folds a host reached from two environments into one switcher row", () => {
714728
const merged = mergePullRequestLists([
715729
[ENV_1, answer()],

apps/web/src/components/pullRequest/pullRequestList.logic.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Schema from "effect/Schema";
33
import {
44
EnvironmentId,
55
PullRequestListEntry,
6+
PullRequestListProjectError,
67
PullRequestListResult,
78
resolvePullRequestAuthorFilter,
89
} from "@t3tools/contracts";
@@ -27,6 +28,10 @@ export interface EnvironmentPullRequestStat extends PullRequestDiffStat {
2728
readonly environmentId: EnvironmentId;
2829
}
2930

31+
export interface EnvironmentPullRequestError extends PullRequestListProjectError {
32+
readonly environmentId: EnvironmentId;
33+
}
34+
3035
export type PullRequestGroupKey = "reviewRequested" | "authored" | "others";
3136

3237
export interface PullRequestGroup<Entry extends PullRequestListEntry = PullRequestListEntry> {
@@ -452,7 +457,7 @@ export interface MergedPullRequestList {
452457
readonly viewers: PullRequestViewers;
453458
readonly providers: PullRequestListResult["providers"];
454459
readonly entries: ReadonlyArray<EnvironmentPullRequestEntry>;
455-
readonly errors: PullRequestListResult["errors"];
460+
readonly errors: ReadonlyArray<EnvironmentPullRequestError>;
456461
readonly truncated: boolean;
457462
readonly nextCursors: Readonly<Record<string, PullRequestListCursors>>;
458463
/**
@@ -476,7 +481,7 @@ export function mergePullRequestLists(
476481
const truncatedEnvironments: string[] = [];
477482
const providers = new Map<string, PullRequestListResult["providers"][number]>();
478483
const entries: EnvironmentPullRequestEntry[] = [];
479-
const errors: Array<PullRequestListResult["errors"][number]> = [];
484+
const errors: EnvironmentPullRequestError[] = [];
480485
const nextCursors: Record<string, PullRequestListCursors> = {};
481486
let truncated = false;
482487
for (const [environmentId, answer] of answers) {
@@ -498,7 +503,7 @@ export function mergePullRequestLists(
498503
);
499504
}
500505
entries.push(...answer.entries.map((entry) => ({ ...entry, environmentId })));
501-
errors.push(...answer.errors);
506+
errors.push(...answer.errors.map((error) => ({ ...error, environmentId })));
502507
truncated ||= answer.truncated;
503508
if (answer.truncated) truncatedEnvironments.push(environmentId);
504509
if (Object.keys(answer.nextCursors).length > 0) {
@@ -559,12 +564,18 @@ const EnvironmentPullRequestEntrySchema = Schema.Struct({
559564
environmentId: EnvironmentId,
560565
});
561566

567+
const EnvironmentPullRequestErrorSchema = Schema.Struct({
568+
...PullRequestListProjectError.fields,
569+
environmentId: EnvironmentId,
570+
});
571+
562572
const decodeSnapshot = Schema.decodeUnknownOption(
563573
Schema.Struct({
564574
scope: Schema.String,
565575
data: Schema.Struct({
566576
...PullRequestListResult.fields,
567577
entries: Schema.Array(EnvironmentPullRequestEntrySchema),
578+
errors: Schema.Array(EnvironmentPullRequestErrorSchema),
568579
// Per environment here, unlike the wire shape, which is per repository within one.
569580
nextCursors: Schema.Record(Schema.String, PullRequestListResult.fields.nextCursors),
570581
truncatedEnvironments: Schema.Array(Schema.String),

apps/web/src/routes/_chat.pull-requests.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
PullRequestFiltersMenu,
6161
PullRequestSearchInput,
6262
pullRequestHostLabel,
63+
pullRequestProjectKey,
6364
type PullRequestExpectedHost,
6465
type PullRequestFilterOption,
6566
} from "../components/pullRequest/PullRequestListFilters";
@@ -1250,7 +1251,16 @@ function PullRequestsRouteView() {
12501251

12511252
/** Reported per project rather than as a count, so the reader can see which one it was. */
12521253
const unavailableProjects = useMemo(
1253-
() => new Map(listErrors.map((error) => [error.projectId, error.message] as const)),
1254+
() =>
1255+
new Map(
1256+
listErrors.map(
1257+
(error) =>
1258+
[
1259+
pullRequestProjectKey({ id: error.projectId, environmentId: error.environmentId }),
1260+
error.message,
1261+
] as const,
1262+
),
1263+
),
12541264
[listErrors],
12551265
);
12561266

0 commit comments

Comments
 (0)