Skip to content

Commit 7d2d4bb

Browse files
brendan-kellamclaudegithub-actions[bot]
authored
feat(web): seed permission sync banner with server-side initial state (#942)
* feat(web): seed permission sync banner with server-side initial state Refactors getPermissionSyncStatus into a reusable server action and passes the result as initialData to the banner query, eliminating the loading flash on first render. Also fixes hasPendingFirstSync to treat accounts with no sync jobs yet as pending. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update changelog for #942 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update CHANGELOG with permission sync banner fixes Consolidate permission sync banner fixes for clarity. * refactor(web): move getPermissionSyncStatus to api.ts to fix invalid route export Next.js does not allow exporting arbitrary functions from route.ts files. Extracts the getPermissionSyncStatus helper into a sibling api.ts module and updates route.ts and layout.tsx imports accordingly. Co-authored-by: Brendan Kellam <brendan-kellam@users.noreply.github.com> * fix(web): update PermissionSyncStatusResponse import to use api.ts Co-authored-by: Brendan Kellam <brendan-kellam@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Brendan Kellam <brendan-kellam@users.noreply.github.com>
1 parent 808dc94 commit 7d2d4bb

File tree

6 files changed

+81
-49
lines changed

6 files changed

+81
-49
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323

2424
### Fixed
2525
- Fixed text inside angle brackets (e.g., `<id>`) being hidden in chat prompt display due to HTML parsing. [#929](https://github.com/sourcebot-dev/sourcebot/pull/929) [#932](https://github.com/sourcebot-dev/sourcebot/pull/932)
26+
- Fixed permission sync banner flashing on initial page load. [#942](https://github.com/sourcebot-dev/sourcebot/pull/942)
27+
- Fixed issue where the permission sync banner would sometimes not appear until the page was refreshed. [#942](https://github.com/sourcebot-dev/sourcebot/pull/942)
2628

2729
## [4.11.7] - 2026-02-23
2830

packages/web/src/app/[domain]/components/permissionSyncBanner.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { usePrevious } from "@uidotdev/usehooks";
1111

1212
const POLL_INTERVAL_MS = 5000;
1313

14-
export function PermissionSyncBanner() {
14+
interface PermissionSyncBannerProps {
15+
initialHasPendingFirstSync: boolean;
16+
}
17+
18+
export function PermissionSyncBanner({ initialHasPendingFirstSync }: PermissionSyncBannerProps) {
1519
const router = useRouter();
1620

1721
const { data: hasPendingFirstSync, isError, isPending } = useQuery({
@@ -25,6 +29,9 @@ export function PermissionSyncBanner() {
2529
// Keep polling while sync is in progress, stop when done
2630
return hasPendingFirstSync ? POLL_INTERVAL_MS : false;
2731
},
32+
initialData: {
33+
hasPendingFirstSync: initialHasPendingFirstSync,
34+
}
2835
});
2936

3037
const previousHasPendingFirstSync = usePrevious(hasPendingFirstSync);

packages/web/src/app/[domain]/layout.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { UpgradeToast } from "./components/upgradeToast";
2626
import { getLinkedAccountProviderStates } from "@/ee/features/permissionSyncing/actions";
2727
import { LinkAccounts } from "@/ee/features/permissionSyncing/components/linkAccounts";
2828
import { PermissionSyncBanner } from "./components/permissionSyncBanner";
29+
import { getPermissionSyncStatus } from "../api/(server)/ee/permissionSyncStatus/api";
2930

3031
interface LayoutProps {
3132
children: React.ReactNode,
@@ -190,12 +191,18 @@ export default async function Layout(props: LayoutProps) {
190191
)
191192
}
192193
const isPermissionSyncBannerVisible = session && hasEntitlement("permission-syncing");
194+
const hasPendingFirstSync = isPermissionSyncBannerVisible ? (await getPermissionSyncStatus()) : null;
193195

194196
return (
195197
<SyntaxGuideProvider>
196198
{
197199
isPermissionSyncBannerVisible ? (
198-
<PermissionSyncBanner />
200+
<PermissionSyncBanner
201+
initialHasPendingFirstSync={(isServiceError(hasPendingFirstSync) || hasPendingFirstSync === null) ?
202+
false :
203+
hasPendingFirstSync.hasPendingFirstSync
204+
}
205+
/>
199206
) : null
200207
}
201208
{children}

packages/web/src/app/api/(client)/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
FileSourceRequest,
2020
FileSourceResponse,
2121
} from "@/features/git";
22-
import { PermissionSyncStatusResponse } from "../(server)/ee/permissionSyncStatus/route";
22+
import { PermissionSyncStatusResponse } from "../(server)/ee/permissionSyncStatus/api";
2323
import {
2424
SearchChatShareableMembersQueryParams,
2525
SearchChatShareableMembersResponse,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use server';
2+
3+
import { ServiceError } from "@/lib/serviceError";
4+
import { withAuthV2 } from "@/withAuthV2";
5+
import { env, getEntitlements } from "@sourcebot/shared";
6+
import { AccountPermissionSyncJobStatus } from "@sourcebot/db";
7+
import { StatusCodes } from "http-status-codes";
8+
import { ErrorCode } from "@/lib/errorCodes";
9+
import { sew } from "@/actions";
10+
11+
export interface PermissionSyncStatusResponse {
12+
hasPendingFirstSync: boolean;
13+
}
14+
15+
/**
16+
* Returns whether a user has a account that has it's permissions
17+
* synced for the first time.
18+
*/
19+
export const getPermissionSyncStatus = async (): Promise<PermissionSyncStatusResponse | ServiceError> => sew(async () =>
20+
withAuthV2(async ({ prisma, user }) => {
21+
const entitlements = getEntitlements();
22+
if (!entitlements.includes('permission-syncing')) {
23+
return {
24+
statusCode: StatusCodes.FORBIDDEN,
25+
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
26+
message: "Permission syncing is not enabled for your license",
27+
} satisfies ServiceError;
28+
}
29+
30+
31+
const accounts = await prisma.account.findMany({
32+
where: {
33+
userId: user.id,
34+
provider: { in: ['github', 'gitlab', 'bitbucket-cloud', 'bitbucket-server'] }
35+
},
36+
include: {
37+
permissionSyncJobs: {
38+
orderBy: { createdAt: 'desc' },
39+
take: 1,
40+
}
41+
}
42+
});
43+
44+
const activeStatuses: AccountPermissionSyncJobStatus[] = [
45+
AccountPermissionSyncJobStatus.PENDING,
46+
AccountPermissionSyncJobStatus.IN_PROGRESS
47+
];
48+
49+
const hasPendingFirstSync = env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' &&
50+
accounts.some(account =>
51+
account.permissionSyncedAt === null &&
52+
// @note: to handle the case where the permission sync job
53+
// has not yet been scheduled for a new account, we consider
54+
// accounts with no permission sync jobs as having a pending first sync.
55+
(account.permissionSyncJobs.length === 0 || (account.permissionSyncJobs.length > 0 && activeStatuses.includes(account.permissionSyncJobs[0].status)))
56+
)
57+
58+
return { hasPendingFirstSync } satisfies PermissionSyncStatusResponse;
59+
})
60+
)

packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,15 @@
1-
'use server';
2-
31
import { apiHandler } from "@/lib/apiHandler";
42
import { serviceErrorResponse } from "@/lib/serviceError";
53
import { isServiceError } from "@/lib/utils";
6-
import { withAuthV2 } from "@/withAuthV2";
7-
import { getEntitlements } from "@sourcebot/shared";
8-
import { AccountPermissionSyncJobStatus } from "@sourcebot/db";
94
import { StatusCodes } from "http-status-codes";
10-
import { ErrorCode } from "@/lib/errorCodes";
11-
12-
export interface PermissionSyncStatusResponse {
13-
hasPendingFirstSync: boolean;
14-
}
5+
import { getPermissionSyncStatus } from "./api";
156

167
/**
178
* Returns whether a user has a account that has it's permissions
189
* synced for the first time.
1910
*/
2011
export const GET = apiHandler(async () => {
21-
const entitlements = getEntitlements();
22-
if (!entitlements.includes('permission-syncing')) {
23-
return serviceErrorResponse({
24-
statusCode: StatusCodes.FORBIDDEN,
25-
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
26-
message: "Permission syncing is not enabled for your license",
27-
});
28-
}
29-
30-
const result = await withAuthV2(async ({ prisma, user }) => {
31-
const accounts = await prisma.account.findMany({
32-
where: {
33-
userId: user.id,
34-
provider: { in: ['github', 'gitlab', 'bitbucket-cloud', 'bitbucket-server'] }
35-
},
36-
include: {
37-
permissionSyncJobs: {
38-
orderBy: { createdAt: 'desc' },
39-
take: 1,
40-
}
41-
}
42-
});
43-
44-
const activeStatuses: AccountPermissionSyncJobStatus[] = [
45-
AccountPermissionSyncJobStatus.PENDING,
46-
AccountPermissionSyncJobStatus.IN_PROGRESS
47-
];
48-
49-
const hasPendingFirstSync = accounts.some(account =>
50-
account.permissionSyncedAt === null &&
51-
account.permissionSyncJobs.length > 0 &&
52-
activeStatuses.includes(account.permissionSyncJobs[0].status)
53-
);
54-
55-
return { hasPendingFirstSync } satisfies PermissionSyncStatusResponse;
56-
});
12+
const result = await getPermissionSyncStatus();
5713

5814
if (isServiceError(result)) {
5915
return serviceErrorResponse(result);

0 commit comments

Comments
 (0)