Skip to content

Commit a8acd93

Browse files
committed
fix: tweaks to the orgs pages
1 parent aa8e4c8 commit a8acd93

File tree

10 files changed

+1232
-844
lines changed

10 files changed

+1232
-844
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { getServerSession } from 'next-auth'
3+
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
4+
import db from '@codebuff/common/db'
5+
import * as schema from '@codebuff/common/db/schema'
6+
import { eq } from 'drizzle-orm'
7+
import { checkOrganizationPermission } from '@/lib/organization-permissions'
8+
import { logger } from '@/util/logger'
9+
import type { PublisherProfileResponse } from '@codebuff/common/types/publisher'
10+
11+
interface RouteParams {
12+
params: { orgId: string }
13+
}
14+
15+
// Get all publishers for organization
16+
export async function GET(
17+
request: NextRequest,
18+
{ params }: RouteParams
19+
): Promise<NextResponse> {
20+
try {
21+
const session = await getServerSession(authOptions)
22+
if (!session?.user?.id) {
23+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
24+
}
25+
26+
const { orgId } = params
27+
28+
// Check if user has access to this organization
29+
const orgPermission = await checkOrganizationPermission(orgId, ['owner', 'admin', 'member'])
30+
if (!orgPermission.success) {
31+
return NextResponse.json(
32+
{ error: orgPermission.error },
33+
{ status: orgPermission.status || 500 }
34+
)
35+
}
36+
37+
// Find all publishers for this organization
38+
const publishers = await db
39+
.select({
40+
id: schema.publisher.id,
41+
name: schema.publisher.name,
42+
verified: schema.publisher.verified,
43+
bio: schema.publisher.bio,
44+
avatar_url: schema.publisher.avatar_url,
45+
created_at: schema.publisher.created_at,
46+
user_id: schema.publisher.user_id,
47+
org_id: schema.publisher.org_id,
48+
created_by: schema.publisher.created_by,
49+
updated_at: schema.publisher.updated_at,
50+
email: schema.publisher.email,
51+
})
52+
.from(schema.publisher)
53+
.where(eq(schema.publisher.org_id, orgId))
54+
55+
// Get agent count for each publisher
56+
const response: PublisherProfileResponse[] = await Promise.all(
57+
publishers.map(async (publisher) => {
58+
const agentCount = await db
59+
.select({ count: schema.agentConfig.id })
60+
.from(schema.agentConfig)
61+
.where(eq(schema.agentConfig.publisher_id, publisher.id))
62+
.then((result) => result.length)
63+
64+
return {
65+
...publisher,
66+
agentCount,
67+
ownershipType: 'organization' as const,
68+
}
69+
})
70+
)
71+
72+
return NextResponse.json({ publishers: response })
73+
} catch (error) {
74+
logger.error({ error, orgId: params.orgId }, 'Error fetching organization publishers')
75+
return NextResponse.json(
76+
{ error: 'Internal server error' },
77+
{ status: 500 }
78+
)
79+
}
80+
}

web/src/app/api/publishers/validate/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import * as schema from '@codebuff/common/db/schema'
44
import { eq } from 'drizzle-orm'
55
import { validatePublisherId } from '@/lib/validators/publisher'
66

7+
// Force dynamic rendering for this route
8+
export const dynamic = 'force-dynamic'
9+
710
export async function GET(request: NextRequest) {
811
try {
912
const { searchParams } = new URL(request.url)
@@ -39,10 +42,7 @@ export async function GET(request: NextRequest) {
3942
)
4043
}
4144

42-
return NextResponse.json(
43-
{ valid: true, error: null },
44-
{ status: 200 }
45-
)
45+
return NextResponse.json({ valid: true, error: null }, { status: 200 })
4646
} catch (error) {
4747
console.error('Error validating publisher ID:', error)
4848
return NextResponse.json(

web/src/app/orgs/[slug]/settings/page.tsx

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Skeleton } from '@/components/ui/skeleton'
2323
import { Textarea } from '@/components/ui/textarea'
2424
import { toast } from '@/components/ui/use-toast'
2525
import { useOrganizationData } from '@/hooks/use-organization-data'
26+
import type { PublisherProfileResponse } from '@codebuff/common/types/publisher'
2627

2728
export default function OrganizationSettingsPage() {
2829
const { data: session, status } = useSession()
@@ -38,10 +39,34 @@ export default function OrganizationSettingsPage() {
3839
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
3940
const [deleteConfirmSlug, setDeleteConfirmSlug] = useState('')
4041
const [deleting, setDeleting] = useState(false)
42+
const [publishers, setPublishers] = useState<PublisherProfileResponse[]>([])
43+
const [publishersLoading, setPublishersLoading] = useState(true)
4144

4245
// Use the custom hook for organization data
4346
const { organization, isLoading, error } = useOrganizationData(orgSlug)
4447

48+
// Fetch publishers data for this organization
49+
useEffect(() => {
50+
const fetchPublishers = async () => {
51+
if (!organization?.id) return
52+
53+
setPublishersLoading(true)
54+
try {
55+
const response = await fetch(`/api/orgs/${organization.id}/publishers`)
56+
if (response.ok) {
57+
const data = await response.json()
58+
setPublishers(data.publishers || [])
59+
}
60+
} catch (error) {
61+
console.error('Error fetching publishers:', error)
62+
} finally {
63+
setPublishersLoading(false)
64+
}
65+
}
66+
67+
fetchPublishers()
68+
}, [organization?.id])
69+
4570
// Initialize form when organization data loads
4671
useEffect(() => {
4772
if (organization) {
@@ -278,26 +303,104 @@ export default function OrganizationSettingsPage() {
278303
{canManageOrg && (
279304
<Card>
280305
<CardHeader>
281-
<CardTitle>Publisher Profile</CardTitle>
306+
<CardTitle className="flex items-center">
307+
<User className="mr-2 h-5 w-5" />
308+
Publisher Profiles
309+
</CardTitle>
310+
<p className="text-sm text-muted-foreground">
311+
Manage publisher profiles for this organization to publish and
312+
distribute agents
313+
</p>
282314
</CardHeader>
283315
<CardContent className="space-y-4">
284-
<div>
285-
<h4 className="font-medium mb-2">
286-
Create Organization Publisher
287-
</h4>
288-
<p className="text-sm text-muted-foreground mb-4">
289-
Create a publisher profile for this organization to publish
290-
agents.
291-
</p>
292-
<Link
293-
href={`/publishers?org=${organization.id}&type=organization`}
294-
>
295-
<Button className="flex items-center">
296-
<User className="mr-2 h-4 w-4" />
297-
Create Publisher Profile
298-
</Button>
299-
</Link>
300-
</div>
316+
{publishersLoading ? (
317+
<div className="space-y-2">
318+
<Skeleton className="h-4 w-48" />
319+
<Skeleton className="h-4 w-64" />
320+
<Skeleton className="h-10 w-40" />
321+
</div>
322+
) : (
323+
<div>
324+
<div className="flex items-center justify-between mb-4">
325+
<h4 className="font-medium">
326+
Organization Publishers ({publishers.length})
327+
</h4>
328+
<Link
329+
href={`/publishers/new?org=${organization.id}&type=organization`}
330+
>
331+
<Button className="flex items-center">
332+
<User className="mr-2 h-4 w-4" />
333+
Create Publisher Profile
334+
</Button>
335+
</Link>
336+
</div>
337+
338+
{publishers.length === 0 ? (
339+
<div className="text-center py-8 bg-muted/50 rounded-lg">
340+
<p className="text-sm text-muted-foreground mb-4">
341+
No publisher profiles created yet.
342+
</p>
343+
<p className="text-xs text-muted-foreground">
344+
Create a publisher profile to start publishing agents
345+
for this organization.
346+
</p>
347+
</div>
348+
) : (
349+
<div className="space-y-3">
350+
{publishers.map((publisher) => (
351+
<div
352+
key={publisher.id}
353+
className="bg-muted/50 rounded-lg p-4"
354+
>
355+
<div className="flex items-start justify-between">
356+
<div className="flex-1">
357+
<div className="flex items-center space-x-2 mb-2">
358+
<h5 className="font-medium">
359+
{publisher.name}
360+
</h5>
361+
{publisher.verified && (
362+
<span className="text-green-600 text-sm">
363+
✓ Verified
364+
</span>
365+
)}
366+
</div>
367+
<p className="text-sm text-muted-foreground mb-2">
368+
@{publisher.id}
369+
</p>
370+
{publisher.bio && (
371+
<p className="text-sm mb-2">
372+
{publisher.bio}
373+
</p>
374+
)}
375+
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
376+
<span>
377+
{publisher.agentCount || 0} agents published
378+
</span>
379+
<span>
380+
Created{' '}
381+
{new Date(
382+
publisher.created_at
383+
).toLocaleDateString()}
384+
</span>
385+
</div>
386+
</div>
387+
<Link href={`/publishers/${publisher.id}`}>
388+
<Button
389+
variant="outline"
390+
size="sm"
391+
className="flex items-center"
392+
>
393+
<User className="mr-2 h-4 w-4" />
394+
View Profile
395+
</Button>
396+
</Link>
397+
</div>
398+
</div>
399+
))}
400+
</div>
401+
)}
402+
</div>
403+
)}
301404
</CardContent>
302405
</Card>
303406
)}

0 commit comments

Comments
 (0)