Detailed changes
@@ -13,7 +13,8 @@
"typescript/no-unsafe-member-access": "warn",
"typescript/no-unsafe-return": "warn",
"typescript/await-thenable": "error",
- "typescript/no-unnecessary-type-assertion": "warn"
+ "typescript/no-unnecessary-type-assertion": "warn",
+ "react/react-in-jsx-scope": "off"
},
"options": {
"typeAware": true,
@@ -81,7 +81,7 @@ export function IssueFilters({
const validLabels = useMemo(
() =>
- [...(labelsData?.repository?.validLabels.nodes ?? [])].sort((a, b) =>
+ (labelsData?.repository?.validLabels.nodes ?? []).toSorted((a, b) =>
a.name.localeCompare(b.name),
),
[labelsData],
@@ -89,7 +89,7 @@ export function IssueFilters({
const allIdentities = useMemo(
() =>
- [...(authorsData?.repository?.allIdentities.nodes ?? [])].sort((a, b) =>
+ (authorsData?.repository?.allIdentities.nodes ?? []).toSorted((a, b) =>
a.displayName.localeCompare(b.displayName),
),
[authorsData],
@@ -177,7 +177,7 @@ function CommitListSkeleton() {
<div key={g}>
<Skeleton className="mb-2 h-3 w-32" />
<div className="divide-border border-border divide-y overflow-hidden rounded-md border">
- {Array.from({ length: 4 }).map((_, i) => (
+ {Array.from({ length: 4 }).map((_c, i) => (
<div key={i} className="flex items-center gap-3 px-4 py-3">
<Skeleton className="size-4 rounded-sm" />
<div className="flex-1 space-y-1.5">
@@ -2,7 +2,7 @@ import { Link } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
import { Folder, File } from "lucide-react";
-import type { GitTreeEntry } from "@/__generated__/graphql";
+import { GitObjectType, type GitTreeEntry } from "@/__generated__/graphql";
import { Skeleton } from "@/components/ui/skeleton";
import { useRepo } from "@/lib/repo";
@@ -27,8 +27,8 @@ interface FileTreeProps {
// name, last-commit message (linked to commit detail), and relative date.
export function FileTree({ entries, path, loading, onNavigate, onNavigateUp }: FileTreeProps) {
// Directories first, then files — each group alphabetical
- const sorted = [...entries].sort((a, b) => {
- if (a.type !== b.type) return a.type === "TREE" ? -1 : 1;
+ const sorted = entries.toSorted((a, b) => {
+ if (a.type !== b.type) return a.type === GitObjectType.Tree ? -1 : 1;
return a.name.localeCompare(b.name);
});
@@ -64,7 +64,7 @@ function FileTreeRow({
entry: TreeEntryWithCommit;
onNavigate: (entry: TreeEntryWithCommit) => void;
}) {
- const isDir = entry.type === "TREE";
+ const isDir = entry.type === GitObjectType.Tree;
const repo = useRepo();
return (
@@ -1,7 +1,7 @@
import { GitBranch, Tag, Check, ChevronsUpDown } from "lucide-react";
import { useState } from "react";
-import type { GitRef } from "@/__generated__/graphql";
+import { GitRefType, type GitRef } from "@/__generated__/graphql";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -20,8 +20,8 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
const [filter, setFilter] = useState("");
const filtered = refs.filter((r) => r.shortName.toLowerCase().includes(filter.toLowerCase()));
- const branches = filtered.filter((r) => r.type === "BRANCH");
- const tags = filtered.filter((r) => r.type === "TAG");
+ const branches = filtered.filter((r) => r.type === GitRefType.Branch);
+ const tags = filtered.filter((r) => r.type === GitRefType.Tag);
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -102,7 +102,7 @@ function RefItem({
active && "font-medium",
)}
>
- {ref_.type === "BRANCH" ? (
+ {ref_.type === GitRefType.Branch ? (
<GitBranch className="text-muted-foreground size-3 shrink-0" />
) : (
<Tag className="text-muted-foreground size-3 shrink-0" />
@@ -17,12 +17,13 @@ import { useTheme } from "@/lib/theme";
// SignOutButton sends a POST to /auth/logout and reloads the page.
// A full reload is the simplest way to reset all Apollo cache + React state.
+function handleSignOut() {
+ void fetch("/auth/logout", { method: "POST", credentials: "include" }).finally(() =>
+ window.location.assign("/"),
+ );
+}
+
function SignOutButton() {
- function handleSignOut() {
- void fetch("/auth/logout", { method: "POST", credentials: "include" }).finally(() =>
- window.location.assign("/"),
- );
- }
return (
<Button variant="ghost" size="sm" onClick={handleSignOut} title="Sign out">
<LogOut className="size-4" />
@@ -105,10 +105,12 @@ function ExternalAuthProvider({
useEffect(() => {
void fetch("/auth/user", { credentials: "include" })
- .then((res) => {
+ .then(async (res) => {
if (res.status === 401) return null;
if (!res.ok) throw new Error(`/auth/user returned ${res.status}`);
- return res.json() as Promise<AuthUser>;
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment
+ const data: AuthUser = await res.json();
+ return data;
})
.then((u) => setUser(u))
.catch(() => setUser(null))
@@ -2,6 +2,7 @@ import { ApolloProvider } from "@apollo/client/react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+// eslint-disable-next-line import/no-unassigned-import
import "./index.css";
import { client } from "@/lib/apollo";
import { AuthProvider } from "@/lib/auth";
@@ -7,7 +7,13 @@ import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router"
import { AlertCircle, GitCommit } from "lucide-react";
import { useEffect } from "react";
-import type { GitRef, GitTreeEntry, GitBlob, GitLastCommit } from "@/__generated__/graphql";
+import {
+ GitObjectType,
+ type GitRef,
+ type GitTreeEntry,
+ type GitBlob,
+ type GitLastCommit,
+} from "@/__generated__/graphql";
import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb";
import { CommitList } from "@/components/code/CommitList";
import { FileTree } from "@/components/code/FileTree";
@@ -116,13 +122,17 @@ type ViewMode = CodePageSearch["type"];
export const Route = createFileRoute("/$repo/")({
component: RouteComponent,
pendingComponent: CodePageSkeleton,
- validateSearch: (search: Record<string, unknown>): CodePageSearch => ({
- ref: (search.ref as string) ?? "",
- path: (search.path as string) ?? "",
- type: ["tree", "blob", "commits"].includes(search.type as string)
- ? (search.type as CodePageSearch["type"])
- : "tree",
- }),
+ validateSearch: (search: Record<string, unknown>): CodePageSearch => {
+ const ref = typeof search.ref === "string" ? search.ref : "";
+ const path = typeof search.path === "string" ? search.path : "";
+ const typeStr = typeof search.type === "string" ? search.type : "";
+ const validTypes: ViewMode[] = ["tree", "blob", "commits"];
+ return {
+ ref,
+ path,
+ type: validTypes.find((t) => t === typeStr) ?? "tree",
+ };
+ },
loader: ({ params: { repo } }) => ({
refsRef: preloadQuery<RefsQueryData>(REFS_QUERY, {
variables: { repo: repo === "_" ? null : repo },
@@ -180,7 +190,8 @@ function RouteComponent() {
const blob: GitBlob | null = blobData?.repository?.blob ?? null;
const readmeEntry = entries.find(
- (e: GitTreeEntry) => e.type === "BLOB" && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
+ (e: GitTreeEntry) =>
+ e.type === GitObjectType.Blob && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
);
const readmePath = readmeEntry
? currentPath
@@ -201,7 +212,7 @@ function RouteComponent() {
function handleEntryClick(entry: TreeEntryWithCommit) {
const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
- navigateTo(newPath, entry.type === "BLOB" ? "blob" : "tree");
+ navigateTo(newPath, entry.type === GitObjectType.Blob ? "blob" : "tree");
}
function handleNavigateUp() {
@@ -21,8 +21,8 @@ type IssuesSearch = {
export const Route = createFileRoute("/$repo/issues/")({
component: RouteComponent,
validateSearch: (search: Record<string, unknown>): IssuesSearch => ({
- q: (search.q as string) ?? "status:open",
- after: (search.after as string) ?? "",
+ q: typeof search.q === "string" ? search.q : "status:open",
+ after: typeof search.after === "string" ? search.after : "",
}),
});
@@ -312,7 +312,11 @@ function tokenizeQuery(input: string): string[] {
}
// Parse a query string back into structured filter state.
-const VALID_SORTS = new Set<SortValue>(["creation-desc", "creation-asc", "edit-desc", "edit-asc"]);
+const VALID_SORTS = new Set<string>(["creation-desc", "creation-asc", "edit-desc", "edit-asc"]);
+
+function isValidSort(v: string): v is SortValue {
+ return VALID_SORTS.has(v);
+}
function parseQueryString(input: string): {
status: StatusFilter;
@@ -333,8 +337,8 @@ function parseQueryString(input: string): {
else if (token.startsWith("label:")) labels.push(token.slice(6));
else if (token.startsWith("author:")) author = token.slice(7).replace(/^"|"$/g, "");
else if (token.startsWith("sort:")) {
- const v = token.slice(5) as SortValue;
- if (VALID_SORTS.has(v)) sort = v;
+ const v = token.slice(5);
+ if (isValidSort(v)) sort = v;
} else free.push(token);
}
@@ -36,6 +36,7 @@ function RouteComponent() {
try {
const res = await fetch("/auth/identities", { credentials: "include" });
if (!res.ok) throw new Error(`unexpected status ${res.status}`);
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment
const data: IdentityItem[] = await res.json();
setIdentities(data);
} catch (e) {
@@ -1,6 +1,7 @@
{
"extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/vite-react/tsconfig.json"],
"compilerOptions": {
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
"exactOptionalPropertyTypes": false,
"noPropertyAccessFromIndexSignature": false,
"paths": {