Skip to content

Commit dff0434

Browse files
authored
Merge pull request #13 from Multiplier-Labs/fix/remove-local-scripts
Fix repo list in workflows and add shared RepoList component
2 parents 49d8336 + c379af2 commit dff0434

File tree

4 files changed

+134
-69
lines changed

4 files changed

+134
-69
lines changed

dist/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
77
<title>Codekin</title>
8-
<script type="module" crossorigin src="/assets/index-CfSEhsc3.js"></script>
8+
<script type="module" crossorigin src="/assets/index-DFdIqWQS.js"></script>
99
<link rel="stylesheet" crossorigin href="/assets/index-a8q-qxCN.css">
1010
</head>
1111
<body class="bg-neutral-12 text-neutral-2">

src/components/AddWorkflowModal.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
buildCron, formatHour, describeCron, slugify,
1313
} from '../lib/workflowHelpers'
1414
import { CategoryBadge } from './WorkflowBadges'
15+
import { RepoList } from './RepoList'
1516

1617
interface FormState {
1718
kind: string
@@ -29,7 +30,7 @@ interface Props {
2930
}
3031

3132
export function AddWorkflowModal({ token, onClose, onAdd }: Props) {
32-
const { groups, loading: reposLoading } = useRepos(token)
33+
const { groups, loading: reposLoading, error: reposError } = useRepos(token)
3334
const allRepos = groups.flatMap(g => g.repos)
3435

3536
const [selectedRepoId, setSelectedRepoId] = useState<string>('')
@@ -126,21 +127,17 @@ export function AddWorkflowModal({ token, onClose, onAdd }: Props) {
126127
<IconLoader2 size={14} stroke={2} className="animate-spin" />
127128
Loading repos…
128129
</div>
130+
) : reposError ? (
131+
<div className="rounded-md bg-error-10/50 px-3 py-2 text-[13px] text-error-4">
132+
Failed to load repos: {reposError}
133+
</div>
129134
) : (
130-
<select
131-
value={selectedRepoId}
132-
onChange={e => handleRepoSelect(e.target.value)}
133-
className="w-full rounded-md border border-neutral-7 bg-neutral-10 px-3 py-2 text-[15px] text-neutral-1 focus:border-accent-6 focus:outline-none"
134-
>
135-
<option value="">Select a repository…</option>
136-
{groups.map(group => (
137-
<optgroup key={group.owner} label={group.owner}>
138-
{group.repos.map(repo => (
139-
<option key={repo.id} value={repo.id}>{repo.name}</option>
140-
))}
141-
</optgroup>
142-
))}
143-
</select>
135+
<RepoList
136+
groups={groups}
137+
selectedId={selectedRepoId}
138+
onSelect={repo => handleRepoSelect(repo.id)}
139+
maxHeight="180px"
140+
/>
144141
)}
145142
</div>
146143

src/components/RepoList.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Searchable, grouped repo list used by both RepoSelector (sidebar)
3+
* and AddWorkflowModal.
4+
*/
5+
6+
import { useState } from 'react'
7+
import { IconGitBranch, IconCloud } from '@tabler/icons-react'
8+
import type { ApiRepo, RepoGroup } from '../hooks/useRepos'
9+
10+
interface Props {
11+
groups: RepoGroup[]
12+
/** Currently selected repo id (highlighted with accent border). */
13+
selectedId?: string
14+
onSelect: (repo: ApiRepo) => void
15+
/** Repo currently being cloned (shows pulsing badge). */
16+
cloningId?: string | null
17+
/** Max height for the scrollable list. */
18+
maxHeight?: string
19+
autoFocus?: boolean
20+
}
21+
22+
export function RepoList({ groups, selectedId, onSelect, cloningId, maxHeight = '240px', autoFocus }: Props) {
23+
const [search, setSearch] = useState('')
24+
25+
const filteredGroups = search.trim()
26+
? groups.map(g => ({
27+
...g,
28+
repos: g.repos.filter(r =>
29+
r.name.toLowerCase().includes(search.toLowerCase()) ||
30+
r.description?.toLowerCase().includes(search.toLowerCase())
31+
),
32+
})).filter(g => g.repos.length > 0)
33+
: groups
34+
35+
const totalRepos = groups.reduce((n, g) => n + g.repos.length, 0)
36+
37+
if (totalRepos === 0) {
38+
return <p className="text-center text-[15px] text-neutral-6 py-2">No repositories available</p>
39+
}
40+
41+
return (
42+
<>
43+
<input
44+
type="text"
45+
value={search}
46+
onChange={e => setSearch(e.target.value)}
47+
placeholder="Search repos…"
48+
className="mb-2 w-full rounded-lg border border-neutral-7 bg-neutral-11/50 px-3 py-2 text-[15px] text-neutral-2 placeholder-neutral-5 focus:border-accent-6 focus:outline-none"
49+
autoFocus={autoFocus}
50+
/>
51+
<div className="overflow-y-auto rounded-lg border border-neutral-7 bg-neutral-11/50" style={{ maxHeight }}>
52+
{filteredGroups.length === 0 ? (
53+
<p className="px-3 py-3 text-[13px] text-neutral-5 text-center">No matching repos</p>
54+
) : (
55+
filteredGroups.map((group) => (
56+
<div key={group.owner}>
57+
<div className="sticky top-0 z-10 bg-neutral-8 backdrop-blur-sm px-3 py-1.5 text-[13px] font-medium uppercase tracking-wider text-neutral-4 border-b border-neutral-7">
58+
{group.owner}
59+
</div>
60+
{group.repos.map((repo) => {
61+
const isCloning = cloningId === repo.id
62+
const isSelected = selectedId === repo.id
63+
return (
64+
<button
65+
key={repo.id}
66+
type="button"
67+
onClick={() => onSelect(repo)}
68+
disabled={!!cloningId}
69+
className={`group flex w-full items-start gap-3 border-b border-neutral-7/50 px-3 py-2.5 text-left transition last:border-b-0 ${
70+
isSelected
71+
? 'bg-accent-9/20 border-l-2 border-l-accent-6'
72+
: cloningId ? 'cursor-wait opacity-60' : 'hover:bg-neutral-7/20'
73+
}`}
74+
>
75+
<div className="mt-0.5 flex-shrink-0 text-neutral-4 group-hover:text-neutral-3 transition-colors">
76+
{repo.cloned ? (
77+
<IconGitBranch size={16} stroke={1.5} />
78+
) : (
79+
<IconCloud size={16} stroke={1.5} />
80+
)}
81+
</div>
82+
<div className="min-w-0 flex-1">
83+
<div className="flex items-center gap-2">
84+
<span className="text-[15px] font-medium text-neutral-2 group-hover:text-neutral-1 transition-colors truncate">
85+
{repo.name}
86+
</span>
87+
{isCloning && (
88+
<span className="flex-shrink-0 rounded bg-primary-9/30 px-1.5 py-0.5 text-[13px] text-primary-4 animate-pulse">
89+
cloning...
90+
</span>
91+
)}
92+
{!repo.cloned && !isCloning && (
93+
<span className="flex-shrink-0 rounded border border-neutral-6 bg-neutral-8/50 px-1.5 py-0.5 text-[13px] text-neutral-3">
94+
remote
95+
</span>
96+
)}
97+
</div>
98+
{repo.description && (
99+
<div className="mt-0.5 text-[13px] text-neutral-6 truncate">
100+
{repo.description}
101+
</div>
102+
)}
103+
</div>
104+
</button>
105+
)
106+
})}
107+
</div>
108+
))
109+
)}
110+
</div>
111+
</>
112+
)
113+
}

src/components/RepoSelector.tsx

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
*/
88

99
import { useState } from 'react'
10-
import { IconGitBranch, IconCloud } from '@tabler/icons-react'
10+
import { IconGitBranch } from '@tabler/icons-react'
1111
import type { Repo } from '../types'
1212
import type { ApiRepo, RepoGroup } from '../hooks/useRepos'
13+
import { RepoList } from './RepoList'
1314

1415
interface Props {
1516
groups: RepoGroup[]
@@ -63,58 +64,12 @@ export function RepoSelector({ groups, token, onOpen }: Props) {
6364
{totalRepos === 0 ? (
6465
<p className="text-center text-[17px] text-neutral-6">No repositories configured</p>
6566
) : (
66-
<div className="max-h-[240px] overflow-y-auto rounded-lg border border-neutral-7 bg-neutral-11/50">
67-
{groups.map((group) => (
68-
<div key={group.owner}>
69-
<div className="sticky top-0 z-10 bg-neutral-8 backdrop-blur-sm px-3 py-1.5 text-[13px] font-medium uppercase tracking-wider text-neutral-4 border-b border-neutral-7">
70-
{group.owner}
71-
</div>
72-
{group.repos.map((repo) => {
73-
const isCloning = cloning === repo.id
74-
return (
75-
<button
76-
key={repo.id}
77-
onClick={() => handleSelect(repo)}
78-
disabled={!!cloning}
79-
className={`group flex w-full items-start gap-3 border-b border-neutral-7/50 px-3 py-2.5 text-left transition last:border-b-0 ${
80-
cloning ? 'cursor-wait opacity-60' : 'hover:bg-neutral-7/20'
81-
}`}
82-
>
83-
<div className="mt-0.5 flex-shrink-0 text-neutral-4 group-hover:text-neutral-3 transition-colors">
84-
{repo.cloned ? (
85-
<IconGitBranch size={16} stroke={1.5} />
86-
) : (
87-
<IconCloud size={16} stroke={1.5} />
88-
)}
89-
</div>
90-
<div className="min-w-0 flex-1">
91-
<div className="flex items-center gap-2">
92-
<span className="text-[17px] font-medium text-neutral-2 group-hover:text-neutral-1 transition-colors truncate">
93-
{repo.name}
94-
</span>
95-
{isCloning && (
96-
<span className="flex-shrink-0 rounded bg-primary-9/30 px-1.5 py-0.5 text-[13px] text-primary-4 animate-pulse">
97-
cloning...
98-
</span>
99-
)}
100-
{!repo.cloned && !isCloning && (
101-
<span className="flex-shrink-0 rounded border border-neutral-6/40 bg-neutral-8/30 px-1.5 py-0.5 text-[13px] text-neutral-5">
102-
remote
103-
</span>
104-
)}
105-
</div>
106-
{repo.description && (
107-
<div className="mt-0.5 text-[15px] text-neutral-6 truncate">
108-
{repo.description}
109-
</div>
110-
)}
111-
</div>
112-
</button>
113-
)
114-
})}
115-
</div>
116-
))}
117-
</div>
67+
<RepoList
68+
groups={groups}
69+
onSelect={handleSelect}
70+
cloningId={cloning}
71+
autoFocus
72+
/>
11873
)}
11974
</div>
12075
</div>

0 commit comments

Comments
 (0)