Skip to content

Commit f2a82c4

Browse files
committed
feat(github): implement API route for fetching repositories by installation ID and refactor ProjectNewPage components
- Added a new API route to fetch GitHub repositories based on the installation ID. - Updated ProjectNewPage to require organizationId as a mandatory prop. - Refactored InstallationSelector to utilize a new RepositoriesPanel component for displaying repositories. - Removed redundant repository fetching logic from InstallationSelector and centralized it in RepositoriesPanel. - Updated RepositoryItem to handle form submissions for adding projects.
1 parent c9f6401 commit f2a82c4

File tree

5 files changed

+140
-96
lines changed

5 files changed

+140
-96
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createClient } from '@/libs/db/server'
2+
import { getRepositoriesByInstallationId } from '@liam-hq/github'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
5+
export async function GET(req: NextRequest) {
6+
const { searchParams } = new URL(req.url)
7+
const installationId = Number(searchParams.get('installationId'))
8+
9+
if (!installationId) {
10+
return NextResponse.json(
11+
{ error: 'Installation ID is required' },
12+
{ status: 400 },
13+
)
14+
}
15+
16+
const supabase = await createClient()
17+
const { data } = await supabase.auth.getSession()
18+
const session = data.session
19+
20+
if (!session) {
21+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
22+
}
23+
24+
try {
25+
const res = await getRepositoriesByInstallationId(session, installationId)
26+
return NextResponse.json(res.repositories)
27+
} catch (error) {
28+
console.error('Error fetching repositories:', error)
29+
return NextResponse.json(
30+
{ error: 'Failed to fetch repositories' },
31+
{ status: 500 },
32+
)
33+
}
34+
}

frontend/apps/app/features/projects/pages/ProjectNewPage/InstallationSelector/InstallationSelector.tsx

+11-88
Original file line numberDiff line numberDiff line change
@@ -7,94 +7,30 @@ import {
77
DropdownMenuRoot,
88
DropdownMenuTrigger,
99
} from '@/components'
10-
import { addProject } from '@/features/projects/actions'
11-
import { createClient } from '@/libs/db/client'
12-
import { getRepositoriesByInstallationId } from '@liam-hq/github'
13-
import type { Installation, Repository } from '@liam-hq/github'
14-
import { type FC, useCallback, useEffect, useState } from 'react'
10+
import type { Installation } from '@liam-hq/github'
11+
import { type FC, useState } from 'react'
1512
import { P, match } from 'ts-pattern'
16-
import { RepositoryItem } from '../RepositoryItem'
1713
import styles from './InstallationSelector.module.css'
14+
import { RepositoriesPanel } from './RepositoriesPanel'
1815

1916
type Props = {
2017
installations: Installation[]
21-
organizationId?: string
18+
organizationId: string
2219
}
2320

21+
const githubAppUrl = process.env.NEXT_PUBLIC_GITHUB_APP_URL
22+
2423
export const InstallationSelector: FC<Props> = ({
2524
installations,
2625
organizationId,
2726
}) => {
2827
const [selectedInstallation, setSelectedInstallation] =
2928
useState<Installation | null>(null)
30-
const [repositories, setRepositories] = useState<Repository[]>([])
31-
const [loading, setLoading] = useState(false)
32-
const [isAddingProject, setIsAddingProject] = useState(false)
33-
34-
const githubAppUrl = process.env.NEXT_PUBLIC_GITHUB_APP_URL
35-
36-
useEffect(() => {
37-
if (selectedInstallation) {
38-
fetchRepositories(selectedInstallation.id)
39-
}
40-
}, [selectedInstallation])
41-
42-
const fetchRepositories = async (installationId: number) => {
43-
setLoading(true)
44-
try {
45-
const supabase = await createClient()
46-
const { data } = await supabase.auth.getSession()
47-
const session = data.session
48-
49-
if (session === null) {
50-
throw new Error('')
51-
}
52-
53-
const res = await getRepositoriesByInstallationId(
54-
data.session,
55-
installationId,
56-
)
57-
setRepositories(res.repositories)
58-
} catch (error) {
59-
console.error('Error fetching repositories:', error)
60-
} finally {
61-
setLoading(false)
62-
}
63-
}
6429

6530
const handleSelectInstallation = (installation: Installation) => {
6631
setSelectedInstallation(installation)
6732
}
6833

69-
const handleClick = useCallback(
70-
async (repository: Repository) => {
71-
try {
72-
setIsAddingProject(true)
73-
74-
const formData = new FormData()
75-
formData.set('projectName', repository.name)
76-
formData.set('repositoryName', repository.name)
77-
formData.set('repositoryOwner', repository.owner.login)
78-
formData.set('repositoryId', repository.id.toString())
79-
formData.set(
80-
'installationId',
81-
selectedInstallation?.id.toString() || '',
82-
)
83-
84-
if (organizationId) {
85-
formData.set('organizationId', organizationId.toString())
86-
}
87-
88-
await addProject(formData)
89-
// This point is not reached because a redirect occurs on success
90-
} catch (error) {
91-
console.error('Error adding project:', error)
92-
setIsAddingProject(false)
93-
}
94-
},
95-
[selectedInstallation, organizationId],
96-
)
97-
9834
return (
9935
<>
10036
<div className={styles.installationSelector}>
@@ -136,24 +72,11 @@ export const InstallationSelector: FC<Props> = ({
13672
</DropdownMenuRoot>
13773
</div>
13874

139-
{loading && <div>Loading repositories...</div>}
140-
141-
{!loading && repositories.length > 0 && (
142-
<div className={styles.repositoriesList}>
143-
<h3>Repositories</h3>
144-
{repositories.map((repo) => (
145-
<RepositoryItem
146-
key={repo.id}
147-
name={repo.name}
148-
onClick={() => handleClick(repo)}
149-
isLoading={isAddingProject}
150-
/>
151-
))}
152-
</div>
153-
)}
154-
155-
{!loading && selectedInstallation && repositories.length === 0 && (
156-
<div>No repositories found</div>
75+
{selectedInstallation && (
76+
<RepositoriesPanel
77+
installationId={selectedInstallation.id}
78+
organizationId={organizationId}
79+
/>
15780
)}
15881
</>
15982
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client'
2+
3+
import { addProject } from '@/features/projects/actions'
4+
import type { Repository } from '@liam-hq/github'
5+
import { useEffect, useState } from 'react'
6+
import { RepositoryItem } from '../RepositoryItem'
7+
import styles from './InstallationSelector.module.css'
8+
9+
type Props = {
10+
installationId: number
11+
organizationId: string
12+
}
13+
14+
export function RepositoriesPanel({ installationId, organizationId }: Props) {
15+
const [repositories, setRepositories] = useState<Repository[]>([])
16+
const [isLoading, setIsLoading] = useState(false)
17+
const [error, setError] = useState<string | null>(null)
18+
19+
useEffect(() => {
20+
const fetchRepositories = async () => {
21+
setIsLoading(true)
22+
setError(null)
23+
24+
try {
25+
const response = await fetch(
26+
`/api/github/repositories?installationId=${installationId}`,
27+
{ cache: 'no-store' },
28+
)
29+
30+
if (!response.ok) {
31+
const errorData = await response.json()
32+
throw new Error(errorData.error || 'Failed to fetch repositories')
33+
}
34+
35+
const data = await response.json()
36+
setRepositories(data)
37+
} catch (error) {
38+
console.error('Error fetching repositories:', error)
39+
setError(
40+
error instanceof Error
41+
? error.message
42+
: 'Failed to load repositories',
43+
)
44+
} finally {
45+
setIsLoading(false)
46+
}
47+
}
48+
49+
fetchRepositories()
50+
}, [installationId])
51+
52+
// Show loading state
53+
if (isLoading) {
54+
return <div>Loading repositories...</div>
55+
}
56+
57+
// Show error state
58+
if (error) {
59+
return <div>Error: {error}</div>
60+
}
61+
62+
// Show empty state
63+
if (repositories.length === 0) {
64+
return <div>No repositories found</div>
65+
}
66+
67+
// Render repositories with form actions
68+
return (
69+
<div className={styles.repositoriesList}>
70+
<h3>Repositories</h3>
71+
{repositories.map((repo) => (
72+
<form key={repo.id} action={addProject}>
73+
<input type="hidden" name="projectName" value={repo.name} />
74+
<input type="hidden" name="repositoryName" value={repo.name} />
75+
<input
76+
type="hidden"
77+
name="repositoryOwner"
78+
value={repo.owner.login}
79+
/>
80+
<input type="hidden" name="repositoryId" value={repo.id.toString()} />
81+
<input
82+
type="hidden"
83+
name="installationId"
84+
value={installationId.toString()}
85+
/>
86+
<input type="hidden" name="organizationId" value={organizationId} />
87+
<RepositoryItem name={repo.name} isLoading={false} />
88+
</form>
89+
))}
90+
</div>
91+
)
92+
}

frontend/apps/app/features/projects/pages/ProjectNewPage/ProjectNewPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import styles from './ProjectNewPage.module.css'
55

66
type Props = {
77
installations: Installation[]
8-
organizationId?: string
8+
organizationId: string
99
}
1010

1111
export const ProjectNewPage: FC<Props> = ({

frontend/apps/app/features/projects/pages/ProjectNewPage/RepositoryItem/RepositoryItem.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@ import styles from './RepositoryItem.module.css'
44

55
type Props = {
66
name: string
7-
onClick: () => void
87
isLoading?: boolean
98
}
109

11-
export const RepositoryItem: FC<Props> = ({
12-
name,
13-
onClick,
14-
isLoading = false,
15-
}) => {
10+
export const RepositoryItem: FC<Props> = ({ name, isLoading = false }) => {
1611
return (
1712
<div className={styles.wrapper}>
1813
<span>{name}</span>
1914
<Button
2015
size="sm"
2116
variant="solid-primary"
22-
onClick={onClick}
2317
disabled={isLoading}
18+
type="submit"
2419
>
2520
{isLoading ? 'Importing...' : 'Import'}
2621
</Button>

0 commit comments

Comments
 (0)