Skip to content

Commit 4bfe010

Browse files
authored
add local-first github sync catch-up
* add local-first github sync catch-up * fix(dashboard): unblock cache test suite * improve dashboard live responsiveness
1 parent 505d2a6 commit 4bfe010

15 files changed

Lines changed: 1188 additions & 322 deletions

apps/dashboard/src/components/details/detail-activity.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
githubViewerQueryOptions,
3636
} from "#/lib/github.query";
3737
import type { GitHubActor } from "#/lib/github.types";
38+
import { removePullFromOpenViews } from "#/lib/github-query-updates";
3839
import { checkPermissionWarning } from "#/lib/warning-store";
3940

4041
export function DetailActivityHeader({
@@ -180,6 +181,13 @@ export function DetailCommentBox({
180181
},
181182
});
182183
if (result.ok) {
184+
if (newState === "closed") {
185+
removePullFromOpenViews(queryClient, scope, {
186+
owner,
187+
repo,
188+
pullNumber: issueNumber,
189+
});
190+
}
183191
void queryClient.invalidateQueries({
184192
queryKey: githubQueryKeys.all,
185193
});

apps/dashboard/src/components/inbox/inbox-page.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import type {
4545
NotificationParticipant,
4646
NotificationsResult,
4747
} from "#/lib/github.types";
48+
import { githubRevalidationSignalKeys } from "#/lib/github-revalidation";
49+
import { useGitHubSignalStream } from "#/lib/use-github-signal-stream";
4850
import { useHasMounted } from "#/lib/use-has-mounted";
4951

5052
const routeApi = getRouteApi("/_protected/inbox");
@@ -120,11 +122,25 @@ export function InboxPage() {
120122
const [selectedId, setSelectedId] = useState<string | null>(null);
121123
const [filter, setFilter] = useState<InboxFilter>("unread");
122124

123-
const queryInput = { all: filter === "all" };
125+
const queryInput = useMemo(() => ({ all: filter === "all" }), [filter]);
126+
const queryKey = useMemo(
127+
() => githubQueryKeys.notifications.list(scope, queryInput),
128+
[scope, queryInput],
129+
);
130+
const webhookRefreshTargets = useMemo(
131+
() => [
132+
{
133+
queryKey,
134+
signalKeys: [githubRevalidationSignalKeys.notifications],
135+
},
136+
],
137+
[queryKey],
138+
);
124139
const query = useQuery({
125140
...githubNotificationsQueryOptions(scope, queryInput),
126141
enabled: hasMounted,
127142
});
143+
useGitHubSignalStream(webhookRefreshTargets);
128144

129145
const queryClient = useQueryClient();
130146
const notifications = query.data?.notifications ?? [];
@@ -208,6 +224,7 @@ const InboxSidebar = memo(function InboxSidebar({
208224
const prev = queryClient.getQueryData<NotificationsResult>(queryKey);
209225
if (prev) {
210226
queryClient.setQueryData<NotificationsResult>(queryKey, {
227+
...prev,
211228
notifications: prev.notifications.map((n) => ({
212229
...n,
213230
unread: false,
@@ -234,6 +251,7 @@ const InboxSidebar = memo(function InboxSidebar({
234251
const prev = queryClient.getQueryData<NotificationsResult>(queryKey);
235252
if (prev) {
236253
queryClient.setQueryData<NotificationsResult>(queryKey, {
254+
...prev,
237255
notifications: prev.notifications.filter((n) => n.unread),
238256
});
239257
}
@@ -436,6 +454,7 @@ const InboxRow = memo(function InboxRow({
436454
const prev = queryClient.getQueryData<NotificationsResult>(queryKey);
437455
if (prev) {
438456
queryClient.setQueryData<NotificationsResult>(queryKey, {
457+
...prev,
439458
notifications: prev.notifications.filter(
440459
(n) => n.id !== notification.id,
441460
),
@@ -455,6 +474,7 @@ const InboxRow = memo(function InboxRow({
455474
const prev = queryClient.getQueryData<NotificationsResult>(queryKey);
456475
if (prev) {
457476
queryClient.setQueryData<NotificationsResult>(queryKey, {
477+
...prev,
458478
notifications: prev.notifications.map((n) =>
459479
n.id === notification.id ? { ...n, unread: false } : n,
460480
),

apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ import {
5151
lazy,
5252
Suspense,
5353
useCallback,
54-
useEffect,
5554
useMemo,
5655
useRef,
5756
useState,
@@ -113,10 +112,13 @@ import type {
113112
PullWorkflowApproval,
114113
TimelineEvent,
115114
} from "#/lib/github.types";
115+
import { removePullFromOpenViews } from "#/lib/github-query-updates";
116+
import { githubRevalidationSignalKeys } from "#/lib/github-revalidation";
116117
import {
117118
mergeIssueStateIntoCloseEvent,
118119
parseCloseReason,
119120
} from "#/lib/timeline-close-reason";
121+
import { useGitHubSignalStream } from "#/lib/use-github-signal-stream";
120122
import { usePrefersNoHover } from "#/lib/use-prefers-no-hover";
121123
import { checkPermissionWarning } from "#/lib/warning-store";
122124

@@ -334,16 +336,53 @@ function MergeStatusSection({
334336
prTitle: string;
335337
firstCommitMessage?: string;
336338
}) {
339+
const input = useMemo(
340+
() => ({ owner, repo, pullNumber }),
341+
[owner, repo, pullNumber],
342+
);
343+
const statusQueryKey = useMemo(
344+
() => githubQueryKeys.pulls.status(scope, input),
345+
[scope, input],
346+
);
337347
const statusQuery = useQuery({
338-
...githubPullStatusQueryOptions(scope, { owner, repo, pullNumber }),
348+
...githubPullStatusQueryOptions(scope, input),
339349
});
350+
const workflowStatusRefreshTargets = useMemo(
351+
() => [
352+
{
353+
queryKey: statusQueryKey,
354+
signalKeys: [
355+
githubRevalidationSignalKeys.pullEntity(input),
356+
githubRevalidationSignalKeys.repoProtection({
357+
owner: input.owner,
358+
repo: input.repo,
359+
}),
360+
githubRevalidationSignalKeys.repoStatuses({
361+
owner: input.owner,
362+
repo: input.repo,
363+
}),
364+
...(statusQuery.data?.pendingWorkflowApprovals ?? []).map(
365+
(approval) =>
366+
githubRevalidationSignalKeys.workflowRunEntity({
367+
owner: input.owner,
368+
repo: input.repo,
369+
runId: approval.workflowRunId,
370+
}),
371+
),
372+
],
373+
},
374+
],
375+
[input, statusQuery.data?.pendingWorkflowApprovals, statusQueryKey],
376+
);
377+
useGitHubSignalStream(workflowStatusRefreshTargets);
340378

341379
const status = statusQuery.data ?? null;
342380

343381
if (!status) return <MergeStatusSkeleton />;
344382

345383
return (
346384
<MergeStatusCard
385+
scope={scope}
347386
status={status}
348387
owner={owner}
349388
repo={repo}
@@ -440,13 +479,15 @@ function MergedBranchBanner({
440479
}
441480

442481
function MergeStatusCard({
482+
scope,
443483
status,
444484
owner,
445485
repo,
446486
pullNumber,
447487
prTitle,
448488
firstCommitMessage,
449489
}: {
490+
scope: GitHubQueryScope;
450491
status: PullStatus;
451492
owner: string;
452493
repo: string;
@@ -506,6 +547,7 @@ function MergeStatusCard({
506547
{/* Checks section */}
507548
{(checks.total > 0 || pendingWorkflowApprovals.length > 0) && (
508549
<ChecksSection
550+
scope={scope}
509551
checks={checks}
510552
checkRuns={checkRuns}
511553
pendingWorkflowApprovals={pendingWorkflowApprovals}
@@ -545,6 +587,7 @@ function MergeStatusCard({
545587

546588
{/* Merge action footer */}
547589
<MergeFooter
590+
scope={scope}
548591
isMergeBlocked={isMergeBlocked}
549592
canMerge={canMerge}
550593
bypass={bypass}
@@ -754,6 +797,7 @@ function ReviewsSection({
754797
// ── Checks section ──────────────────────────────────────────────────
755798

756799
function ChecksSection({
800+
scope,
757801
checks,
758802
checkRuns,
759803
pendingWorkflowApprovals,
@@ -763,6 +807,7 @@ function ChecksSection({
763807
repo,
764808
pullNumber,
765809
}: {
810+
scope: GitHubQueryScope;
766811
checks: PullStatus["checks"];
767812
checkRuns: PullCheckRun[];
768813
pendingWorkflowApprovals: PullWorkflowApproval[];
@@ -776,6 +821,10 @@ function ChecksSection({
776821
const [isRerunning, setIsRerunning] = useState(false);
777822
const [isApproving, setIsApproving] = useState(false);
778823
const queryClient = useQueryClient();
824+
const statusQueryKey = useMemo(
825+
() => githubQueryKeys.pulls.status(scope, { owner, repo, pullNumber }),
826+
[scope, owner, repo, pullNumber],
827+
);
779828

780829
const pendingTotal = checks.pending + checks.expected;
781830
const approvalCount = pendingWorkflowApprovals.length;
@@ -894,33 +943,27 @@ function ChecksSection({
894943
toast.warning(
895944
`Approved ${approved} workflow${approved !== 1 ? "s" : ""}, but ${failed} failed`,
896945
);
946+
} else {
947+
toast.success(
948+
`Approved ${pendingWorkflowApprovals.length} workflow${pendingWorkflowApprovals.length !== 1 ? "s" : ""}`,
949+
);
897950
}
898-
// Keep the button in loading state; the effect below resets it once the
899-
// workflow_run webhook invalidates the cache and the pending list drains.
900-
await queryClient.invalidateQueries({ queryKey: ["github"] });
951+
await queryClient.invalidateQueries({
952+
queryKey: statusQueryKey,
953+
exact: true,
954+
refetchType: "active",
955+
});
901956
} else {
902957
toast.error(result.error);
903958
checkPermissionWarning(result, `${owner}/${repo}`);
904-
setIsApproving(false);
905959
}
906960
} catch {
907961
toast.error("Failed to approve workflows");
962+
} finally {
908963
setIsApproving(false);
909964
}
910965
};
911966

912-
// Reset the approving state when the pending list drains (webhook arrived) or
913-
// after a safety timeout to avoid a permanently-stuck spinner.
914-
useEffect(() => {
915-
if (!isApproving) return;
916-
if (pendingWorkflowApprovals.length === 0) {
917-
setIsApproving(false);
918-
return;
919-
}
920-
const timer = setTimeout(() => setIsApproving(false), 30_000);
921-
return () => clearTimeout(timer);
922-
}, [isApproving, pendingWorkflowApprovals.length]);
923-
924967
return (
925968
<Collapsible open={open} onOpenChange={setOpen}>
926969
<CollapsibleTrigger asChild>
@@ -1296,6 +1339,7 @@ const MERGE_STRATEGIES = [
12961339
];
12971340

12981341
function MergeFooter({
1342+
scope,
12991343
isMergeBlocked,
13001344
canMerge,
13011345
bypass,
@@ -1305,6 +1349,7 @@ function MergeFooter({
13051349
prTitle,
13061350
firstCommitMessage,
13071351
}: {
1352+
scope: GitHubQueryScope;
13081353
isMergeBlocked: boolean;
13091354
canMerge: boolean;
13101355
bypass: ReturnType<typeof useMergeBypass>;
@@ -1357,6 +1402,11 @@ function MergeFooter({
13571402
},
13581403
});
13591404
if (result.ok) {
1405+
removePullFromOpenViews(queryClient, scope, {
1406+
owner,
1407+
repo,
1408+
pullNumber,
1409+
});
13601410
await queryClient.invalidateQueries({ queryKey: ["github"] });
13611411
} else {
13621412
toast.error(result.error);

apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export function PullDetailContent({
7676
queryKey: githubQueryKeys.pulls.status(scope, input),
7777
signalKeys: [
7878
githubRevalidationSignalKeys.pullEntity(input),
79+
githubRevalidationSignalKeys.repoProtection({
80+
owner: input.owner,
81+
repo: input.repo,
82+
}),
7983
githubRevalidationSignalKeys.repoStatuses({
8084
owner: input.owner,
8185
repo: input.repo,

0 commit comments

Comments
 (0)