1import type { EnvironmentId, ProjectId } from "@t3tools/contracts";
2import { createFileRoute } from "@tanstack/react-router";
3
4import { type PullRequestListSort } from "../components/pullRequest/pullRequestListPreferences";
5import { SORT_OPTIONS } from "../components/pullRequest/pullRequestListOptions";
6import { type PullRequestsSearch } from "../components/pullRequest/usePullRequestListView";
7import { PullRequestsPage } from "../components/pullRequest/PullRequestsPage";
8
9const MAX_SEARCH_LABEL_CANDIDATES = 100;
10
11function pullRequestSearchLabels(raw: unknown): PullRequestsSearch["labels"] {
12 const values = (Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []).slice(
13 0,
14 MAX_SEARCH_LABEL_CANDIDATES,
15 );
16 const labels: Array<string> = [];
17 const seen = new Set<string>();
18 for (const rawValue of values) {
19 if (typeof rawValue !== "string") continue;
20 const value = rawValue.trim().slice(0, 200);
21 const key = value.toLowerCase();
22 if (value.length === 0 || seen.has(key)) continue;
23 labels.push(value);
24 seen.add(key);
25 if (labels.length === 10) break;
26 }
27 return labels.length === 0 ? undefined : labels;
28}
29
30export const Route = createFileRoute("/_chat/pull-requests")({
31 validateSearch: (raw: Record<string, unknown>): PullRequestsSearch => ({
32 ...(typeof raw.selectedUrl === "string" ? { selectedUrl: raw.selectedUrl.slice(0, 4096) } : {}),
33 involvement:
34 raw.involvement === "reviewing" || raw.involvement === "authored" ? raw.involvement : "all",
35 state:
36 raw.state === "closed" || raw.state === "merged" || raw.state === "all" ? raw.state : "open",
37 ...(SORT_OPTIONS.some((option) => option.value === raw.sort)
38 ? { sort: raw.sort as PullRequestListSort }
39 : {}),
40 ...(typeof raw.repository === "string" && raw.repository
41 ? { repository: raw.repository.slice(0, 200) }
42 : {}),
43 ...(typeof raw.number === "number" && Number.isInteger(raw.number) && raw.number > 0
44 ? { number: raw.number }
45 : {}),
46 ...(typeof raw.projectId === "string" && raw.projectId
47 ? { projectId: raw.projectId as ProjectId }
48 : {}),
49 ...(typeof raw.environmentId === "string" && raw.environmentId
50 ? { environmentId: raw.environmentId as EnvironmentId }
51 : {}),
52 ...(typeof raw.host === "string" && raw.host ? { host: raw.host.slice(0, 200) } : {}),
53 ...(typeof raw.selectedProjectId === "string" && raw.selectedProjectId
54 ? { selectedProjectId: raw.selectedProjectId as ProjectId }
55 : {}),
56 ...(typeof raw.selectedHost === "string" && raw.selectedHost
57 ? { selectedHost: raw.selectedHost.slice(0, 200) }
58 : {}),
59 ...(typeof raw.selectedEnvironmentId === "string" && raw.selectedEnvironmentId
60 ? { selectedEnvironmentId: raw.selectedEnvironmentId as EnvironmentId }
61 : {}),
62 // Always include free-text keys to override numeric values in the parent route's unvalidated search.
63 q: typeof raw.q === "string" && raw.q ? raw.q.slice(0, 200) : undefined,
64 ...(raw.draft === "only" || raw.draft === "hide" ? { draft: raw.draft } : {}),
65 ...(raw.review === "approved" ||
66 raw.review === "changes-requested" ||
67 raw.review === "review-required" ||
68 raw.review === "none"
69 ? { review: raw.review }
70 : {}),
71 ...(raw.checks === "passing" || raw.checks === "failing" ? { checks: raw.checks } : {}),
72 author:
73 typeof raw.author === "string" && raw.author.trim()
74 ? raw.author.trim().slice(0, 200)
75 : undefined,
76 labels: pullRequestSearchLabels(raw.labels),
77 }),
78 component: PullRequestsPage,
79});