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
18 changes: 18 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
},
"dependencies": {
"@hono/node-server": "^2.0.2",
"@tanstack/query-async-storage-persister": "^5.101.1",
"@tanstack/react-query": "^5.101.1",
"@tanstack/react-query-devtools": "^5.101.1",
"@tanstack/react-query-persist-client": "^5.101.1",
"hono": "^4.10.7"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions scripts/build-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const isWatch = process.argv.includes("--watch");
const REPO_URL = process.env.REPO_URL ?? "https://github.com/jennings/pulldash";
const define = {
__REPO_URL__: JSON.stringify(REPO_URL),
__DEV__: JSON.stringify(isWatch),
};

async function build() {
Expand Down
41 changes: 23 additions & 18 deletions src/browser/components/home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState, useEffect, useCallback, useMemo } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { queries } from "../lib/queries";
import {
Search,
GitPullRequest,
Expand Down Expand Up @@ -43,8 +45,6 @@ import {
import {
useGitHubStore,
useGitHubReady,
usePRList,
usePRListActions,
getCachedTeams,
type PRSearchResult,
} from "../contexts/github";
Expand Down Expand Up @@ -326,9 +326,7 @@ export function Home() {
const github = useGitHubStore();
const { isAuthenticated } = useAuth();

// Data store
const prList = usePRList();
const { fetchPRList, refreshPRList } = usePRListActions();
const queryClient = useQueryClient();

// Filter config
const [config, setConfig] = useState<FilterConfig>(getFilterConfig);
Expand Down Expand Up @@ -361,21 +359,29 @@ export function Home() {
saveFilterConfig(config);
}, [config]);

useEffect(() => {
if (githubReady) {
fetchPRList(searchQueries, page, perPage);
}
}, [fetchPRList, searchQueries, page, perPage, githubReady]);

// Reset page when config changes
useEffect(() => {
setPage(1);
}, [config.repos, config.state]);

// PR list via React Query
const {
data: prListData,
isFetching: loadingPrs,
isPending: prListPending,
dataUpdatedAt,
} = useQuery({
...queries.prList(searchQueries, page, perPage),
enabled: githubReady,
});

const refreshPRList = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["pr-list"] });
}, [queryClient]);

// Convenience accessors
const prs = prList.items;
const loadingPrs = prList.loading;
const totalCount = prList.totalCount;
const prs = prListData?.items ?? [];
const totalCount = prListData?.totalCount ?? 0;

// Client-side filter for UPDATED PRs
const filteredPrs = useMemo(() => {
Expand Down Expand Up @@ -1045,8 +1051,8 @@ export function Home() {
)}
</span>
<div className="flex items-center gap-2">
{prList.lastFetchedAt && !loadingPrs && (
<RefreshCountdown lastFetchedAt={prList.lastFetchedAt} />
{dataUpdatedAt > 0 && !loadingPrs && (
<RefreshCountdown lastFetchedAt={dataUpdatedAt} />
)}
<button
onClick={refreshPRList}
Expand All @@ -1066,8 +1072,7 @@ export function Home() {

{/* PR List */}
<div className="flex-1 overflow-auto">
{loadingPrs ||
(config.repos.length > 0 && prs.length === 0 && !prList.error) ? (
{loadingPrs || (config.repos.length > 0 && prListPending) ? (
<PRListSkeleton count={8} />
) : prs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
Expand Down
77 changes: 24 additions & 53 deletions src/browser/components/pr-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ import { usePRReviewSelector, usePRReviewStore } from "../contexts/pr-review";
import { getTimeAgo, formatDateTime } from "../lib/dates";
import { parseDiffCached, type ParsedDiff } from "../lib/diff";
import type { ReviewComment } from "@/api/types";
import { useQuery } from "@tanstack/react-query";
import { queries } from "../lib/queries";
import {
useGitHub,
useGitHubReady,
useCurrentUser,
type Review as GitHubReview,
type IssueComment as GitHubIssueComment,
Expand Down Expand Up @@ -204,9 +207,13 @@ export const PROverview = memo(function PROverview() {
const canResolveThread = canWrite && hasWritePermission;

// Reviewers and Assignees state
const [collaborators, setCollaborators] = useState<
Array<{ login: string; avatar_url: string }>
>([]);
const { ready } = useGitHubReady();
const { data: collaboratorsRaw = [], isLoading: loadingCollaborators } =
useQuery({ ...queries.collaborators(owner, repo), enabled: ready });
const collaborators = collaboratorsRaw.map((c) => ({
login: c.login || "",
avatar_url: c.avatar_url || "",
}));
const [showReviewersPicker, setShowReviewersPicker] = useState(false);
const [showAssigneesPicker, setShowAssigneesPicker] = useState(false);
const [reviewersPickerPosition, setReviewersPickerPosition] = useState({
Expand All @@ -217,7 +224,6 @@ export const PROverview = memo(function PROverview() {
top: 0,
left: 0,
});
const [loadingCollaborators, setLoadingCollaborators] = useState(false);
const [reviewerSearchQuery, setReviewerSearchQuery] = useState("");
const [assigneeSearchQuery, setAssigneeSearchQuery] = useState("");
const reviewersButtonRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -366,40 +372,20 @@ export const PROverview = memo(function PROverview() {
await store.updateBranch();
}, [store]);

// Fetch collaborators when picker is opened
const fetchCollaborators = useCallback(async () => {
if (collaborators.length > 0) return;
setLoadingCollaborators(true);
try {
const data = await github.getRepoCollaborators(owner, repo);
setCollaborators(
data.map((c) => ({
login: c.login || "",
avatar_url: c.avatar_url || "",
}))
);
} catch (error) {
console.error("Failed to fetch collaborators:", error);
} finally {
setLoadingCollaborators(false);
}
}, [github, owner, repo, collaborators.length]);

const handleToggleReviewersPicker = useCallback(() => {
if (!showReviewersPicker && reviewersButtonRef.current) {
const rect = reviewersButtonRef.current.getBoundingClientRect();
setReviewersPickerPosition({
top: rect.bottom + 4,
left: Math.min(rect.left, window.innerWidth - 280),
});
fetchCollaborators();
setReviewerSearchQuery("");
// Focus the search input after a short delay to allow the picker to render
setTimeout(() => reviewerSearchInputRef.current?.focus(), 50);
}
setShowReviewersPicker(!showReviewersPicker);
setShowAssigneesPicker(false);
}, [showReviewersPicker, fetchCollaborators]);
}, [showReviewersPicker]);

const handleToggleAssigneesPicker = useCallback(() => {
if (!showAssigneesPicker && assigneesButtonRef.current) {
Expand All @@ -408,14 +394,13 @@ export const PROverview = memo(function PROverview() {
top: rect.bottom + 4,
left: Math.min(rect.left, window.innerWidth - 280),
});
fetchCollaborators();
setAssigneeSearchQuery("");
// Focus the search input after a short delay to allow the picker to render
setTimeout(() => assigneeSearchInputRef.current?.focus(), 50);
}
setShowAssigneesPicker(!showAssigneesPicker);
setShowReviewersPicker(false);
}, [showAssigneesPicker, fetchCollaborators]);
}, [showAssigneesPicker]);

// Helper to refetch PR and update store
const refetchPR = useCallback(async () => {
Expand Down Expand Up @@ -532,7 +517,7 @@ export const PROverview = memo(function PROverview() {
});

// 3. Invalidate cache so future fetches get fresh data
github.invalidateCache(`pr:${owner}/${repo}/${pr.number}`);
github.invalidatePR(owner, repo, pr.number);

// 4. Refetch timeline (reviewer request creates event)
refetchTimeline();
Expand All @@ -558,7 +543,7 @@ export const PROverview = memo(function PROverview() {
});

// 3. Invalidate cache so future fetches get fresh data
github.invalidateCache(`pr:${owner}/${repo}/${pr.number}`);
github.invalidatePR(owner, repo, pr.number);

// 4. Refetch timeline
refetchTimeline();
Expand Down Expand Up @@ -605,7 +590,7 @@ export const PROverview = memo(function PROverview() {
});

// 3. Invalidate cache so future fetches get fresh data
github.invalidateCache(`pr:${owner}/${repo}/${pr.number}`);
github.invalidatePR(owner, repo, pr.number);

// 4. Refetch timeline (assignee change creates event)
refetchTimeline();
Expand All @@ -629,7 +614,7 @@ export const PROverview = memo(function PROverview() {
});

// 3. Invalidate cache so future fetches get fresh data
github.invalidateCache(`pr:${owner}/${repo}/${pr.number}`);
github.invalidatePR(owner, repo, pr.number);

// 4. Refetch timeline
refetchTimeline();
Expand Down Expand Up @@ -676,7 +661,7 @@ export const PROverview = memo(function PROverview() {
});

// 3. Invalidate cache so future fetches get fresh data
github.invalidateCache(`pr:${owner}/${repo}/${pr.number}`);
github.invalidatePR(owner, repo, pr.number);

// 4. Refetch timeline
refetchTimeline();
Expand Down Expand Up @@ -2504,7 +2489,7 @@ export const PROverview = memo(function PROverview() {
store.setPr({ ...pr, labels: newLabels });

// 3. Invalidate cache so future fetches get fresh data
github.invalidateCache(`pr:${owner}/${repo}/${pr.number}`);
github.invalidatePR(owner, repo, pr.number);

// 4. Refetch timeline (label change creates event)
github
Expand Down Expand Up @@ -2647,39 +2632,25 @@ function LabelsSection({
onLabelToggle,
canWrite = true,
}: LabelsSectionProps) {
const github = useGitHub();
const { ready } = useGitHubReady();
const { data: repoLabels = [], isLoading: loadingLabels } = useQuery({
...queries.labels(owner, repo),
enabled: ready,
});
const [showPicker, setShowPicker] = useState(false);
const [repoLabels, setRepoLabels] = useState<
Array<{ name: string; color: string; description?: string | null }>
>([]);
const [loadingLabels, setLoadingLabels] = useState(false);
const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 });
const buttonRef = useRef<HTMLButtonElement>(null);

const fetchLabels = useCallback(async () => {
if (repoLabels.length > 0) return;
setLoadingLabels(true);
try {
const labels = await github.getRepoLabels(owner, repo);
setRepoLabels(labels);
} catch (error) {
console.error("Failed to fetch labels:", error);
} finally {
setLoadingLabels(false);
}
}, [github, owner, repo, repoLabels.length]);

const handleTogglePicker = useCallback(() => {
if (!showPicker && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setPickerPosition({
top: rect.bottom + 4,
left: Math.min(rect.left, window.innerWidth - 280),
});
fetchLabels();
}
setShowPicker(!showPicker);
}, [showPicker, fetchLabels]);
}, [showPicker]);

const handleToggleLabel = useCallback(
async (labelName: string, labelColor: string) => {
Expand Down
11 changes: 2 additions & 9 deletions src/browser/contexts/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,11 @@ function clearAllStorage(): void {
localStorage.removeItem(key);
}

// Remove gh_cache:* keys
// Remove legacy pr-* preference keys
const toRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k?.startsWith("gh_cache:") || k?.startsWith("pr-")) {
if (k?.startsWith("pr-")) {
toRemove.push(k);
}
}
Expand All @@ -206,13 +206,6 @@ function clearAllStorage(): void {
} catch {
// IndexedDB may not be available
}

// Delete Service Worker cache
try {
caches.delete("pulldash-v1");
} catch {
// Cache API may not be available
}
} catch {
// Ignore storage errors
}
Expand Down
Loading
Loading