chore(web): Add count to members / requests / invites tabs in settings (#621)
Some checks failed
Update Roadmap Released / update (push) Has been cancelled
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Has been cancelled
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Has been cancelled
Publish to ghcr / merge (push) Has been cancelled

This commit is contained in:
Brendan Kellam 2025-11-18 10:41:40 -08:00 committed by GitHub
parent 831197980c
commit 97dd54d48f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 39 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Added counts to members, requets, and invites tabs in the members settings. [#621](https://github.com/sourcebot-dev/sourcebot/pull/621)
### Fixed ### Fixed
- Fixed spurious infinite loads with explore panel, file tree, and file search command. [#617](https://github.com/sourcebot-dev/sourcebot/pull/617) - Fixed spurious infinite loads with explore panel, file tree, and file search command. [#617](https://github.com/sourcebot-dev/sourcebot/pull/617)
- Wipe search context on init if entitlement no longer exists [#618](https://github.com/sourcebot-dev/sourcebot/pull/618) - Wipe search context on init if entitlement no longer exists [#618](https://github.com/sourcebot-dev/sourcebot/pull/618)

View file

@ -1,4 +1,4 @@
import { getConnectionStats, getRepos, getReposStats } from "@/actions"; import { getConnectionStats, getCurrentUserRole, getOrgAccountRequests, getRepos, getReposStats } from "@/actions";
import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { SourcebotLogo } from "@/app/components/sourcebotLogo";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -10,7 +10,7 @@ import { env } from "@sourcebot/shared";
import { ServiceErrorException } from "@/lib/serviceError"; import { ServiceErrorException } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils"; import { isServiceError } from "@/lib/utils";
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import { RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; import { OrgRole, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { OrgSelector } from "../orgSelector"; import { OrgSelector } from "../orgSelector";
@ -39,11 +39,32 @@ export const NavigationMenu = async ({
throw new ServiceErrorException(repoStats); throw new ServiceErrorException(repoStats);
} }
const connectionStats = isAuthenticated ? await getConnectionStats() : null; const role = isAuthenticated ? await getCurrentUserRole(domain) : null;
if (isServiceError(role)) {
throw new ServiceErrorException(role);
}
const stats = await (async () => {
if (!isAuthenticated || role !== OrgRole.OWNER) {
return null;
}
const joinRequests = await getOrgAccountRequests(domain);
if (isServiceError(joinRequests)) {
throw new ServiceErrorException(joinRequests);
}
const connectionStats = await getConnectionStats();
if (isServiceError(connectionStats)) { if (isServiceError(connectionStats)) {
throw new ServiceErrorException(connectionStats); throw new ServiceErrorException(connectionStats);
} }
return {
numJoinRequests: joinRequests.length,
connectionStats,
};
})();
const sampleRepos = await getRepos({ const sampleRepos = await getRepos({
where: { where: {
jobs: { jobs: {
@ -100,9 +121,10 @@ export const NavigationMenu = async ({
numberOfRepos={numberOfRepos} numberOfRepos={numberOfRepos}
isReposButtonNotificationDotVisible={numberOfReposWithFirstTimeIndexingJobsInProgress > 0} isReposButtonNotificationDotVisible={numberOfReposWithFirstTimeIndexingJobsInProgress > 0}
isSettingsButtonNotificationDotVisible={ isSettingsButtonNotificationDotVisible={
connectionStats ? stats ? (
connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0 : stats.connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0 ||
false stats.numJoinRequests > 0
) : false
} }
isAuthenticated={isAuthenticated} isAuthenticated={isAuthenticated}
/> />

View file

@ -69,7 +69,7 @@ export default async function SettingsLayout(
throw new ServiceErrorException(connectionStats); throw new ServiceErrorException(connectionStats);
} }
const hasPermissionSyncingEntitlement = await hasEntitlement("permission-syncing"); const hasPermissionSyncingEntitlement = hasEntitlement("permission-syncing");
const sidebarNavItems: SidebarNavItem[] = [ const sidebarNavItems: SidebarNavItem[] = [
{ {
@ -89,16 +89,8 @@ export default async function SettingsLayout(
} }
] : []), ] : []),
...(userRoleInOrg === OrgRole.OWNER ? [{ ...(userRoleInOrg === OrgRole.OWNER ? [{
title: ( title:"Members",
<div className="flex items-center gap-2"> isNotificationDotVisible: numJoinRequests !== undefined && numJoinRequests > 0,
Members
{numJoinRequests !== undefined && numJoinRequests > 0 && (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
{numJoinRequests}
</span>
)}
</div>
),
href: `/${domain}/settings/members`, href: `/${domain}/settings/members`,
}] : []), }] : []),
...(userRoleInOrg === OrgRole.OWNER ? [ ...(userRoleInOrg === OrgRole.OWNER ? [

View file

@ -13,6 +13,8 @@ import { getSeats, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared";
import { RequestsList } from "./components/requestsList"; import { RequestsList } from "./components/requestsList";
import { OrgRole } from "@prisma/client"; import { OrgRole } from "@prisma/client";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { NotificationDot } from "../../components/notificationDot";
import { Badge } from "@/components/ui/badge";
interface MembersSettingsPageProps { interface MembersSettingsPageProps {
params: Promise<{ params: Promise<{
@ -106,16 +108,29 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp
<TabSwitcher <TabSwitcher
className="h-auto p-0 bg-transparent" className="h-auto p-0 bg-transparent"
tabs={[ tabs={[
{ label: "Team Members", value: "members" }, {
label: (
<div className="flex items-center gap-2">
Team Members
<Badge variant="secondary" className="px-1.5 relative">
{members.length}
</Badge>
</div>
),
value: "members"
},
...(userRoleInOrg === OrgRole.OWNER ? [ ...(userRoleInOrg === OrgRole.OWNER ? [
{ {
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{requests.length > 0 && (
<NotificationDot />
)}
Pending Requests Pending Requests
{requests.length > 0 && ( {requests.length > 0 && (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground"> <Badge variant="secondary" className="px-1.5 relative">
{requests.length} {requests.length}
</span> </Badge>
)} )}
</div> </div>
), ),
@ -126,9 +141,9 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
Pending Invites Pending Invites
{invites.length > 0 && ( {invites.length > 0 && (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground"> <Badge variant="secondary" className="px-1.5 relative">
{invites.length} {invites.length}
</span> </Badge>
)} )}
</div> </div>
), ),