1
1
"use client" ;
2
- import { useState , FormEvent , useEffect , Fragment } from "react" ;
2
+ import { useState , useRef , FormEvent , useEffect , Fragment } from "react" ;
3
3
import clsx from "clsx" ;
4
- import { Listbox , Transition } from "@headlessui/react" ;
5
- import { CheckIcon , ChevronUpDownIcon } from "@heroicons/react/20/solid" ;
4
+ import { Listbox , Transition , Combobox } from "@headlessui/react" ;
5
+ import {
6
+ CheckIcon ,
7
+ ChevronUpDownIcon ,
8
+ ArrowPathIcon ,
9
+ } from "@heroicons/react/20/solid" ;
6
10
import { useRouter } from "next/navigation" ;
7
11
import { ExclamationCircleIcon } from "@heroicons/react/20/solid" ;
8
12
import QueryStartedAlert from "@/app/alert" ;
@@ -12,7 +16,7 @@ import {
12
16
RemoteServersType ,
13
17
} from "@/app/query/servers" ;
14
18
import NewQueryId from "@/app/query/haikunator" ;
15
- import { Branch , Branches , isValidCommitHash } from "@/app/query/github" ;
19
+ import { Branch , Branches , Commits } from "@/app/query/github" ;
16
20
17
21
export default function Page ( ) {
18
22
const [ queryId , setQueryId ] = useState < string | null > ( null ) ;
@@ -110,24 +114,32 @@ function IPAForm({
110
114
} : {
111
115
handleIPAFormSubmit : ( event : FormEvent < HTMLFormElement > ) => void ;
112
116
} ) {
117
+ enum CommitSpecifier {
118
+ COMMIT_HASH ,
119
+ BRANCH ,
120
+ }
113
121
const owner = "private-attribution" ;
114
122
const repo = "ipa" ;
115
123
const [ branches , setBranches ] = useState < Branch [ ] > ( [ ] ) ;
124
+ const [ commitHashes , setCommitHashes ] = useState < string [ ] > ( [ ] ) ;
116
125
const branchNames = branches . map ( ( branch ) => branch . name ) ;
117
126
const [ selectedBranchName , setSelectedBranchName ] = useState < string > ( "main" ) ;
118
127
const [ selectedCommitHash , setSelectedCommitHash ] = useState < string > ( "" ) ;
119
128
const [ validCommitHash , setValidCommitHash ] = useState < boolean > ( true ) ;
120
-
121
- enum CommitSpecifier {
122
- COMMIT_HASH ,
123
- BRANCH ,
124
- }
129
+ const commitHashInputRef = useRef < HTMLInputElement > ( null ) ;
125
130
const [ commitSpecifier , setCommitSpecifier ] = useState < CommitSpecifier > (
126
131
CommitSpecifier . BRANCH ,
127
132
) ;
128
-
129
133
const disableBranch = commitSpecifier != CommitSpecifier . BRANCH ;
130
134
const disableCommitHash = commitSpecifier != CommitSpecifier . COMMIT_HASH ;
135
+ const filteredCommitHashes =
136
+ selectedCommitHash === ""
137
+ ? [ ]
138
+ : commitHashes . filter ( ( commit ) => {
139
+ return commit
140
+ . toLowerCase ( )
141
+ . startsWith ( selectedCommitHash . toLowerCase ( ) ) ;
142
+ } ) ;
131
143
132
144
useEffect ( ( ) => {
133
145
const branch = branches . find (
@@ -146,29 +158,48 @@ function IPAForm({
146
158
147
159
useEffect ( ( ) => {
148
160
const branch = branches . find (
149
- ( branch ) => branch . name === selectedCommitHash ,
161
+ ( branch ) => branch . commitHash === selectedCommitHash ,
150
162
) ;
163
+
151
164
const fetchCommitIsValid = async ( ) => {
152
- const _valid = await isValidCommitHash ( owner , repo , selectedCommitHash ) ;
165
+ const _valid = filteredCommitHashes . length > 0 ;
153
166
setValidCommitHash ( _valid ) ;
154
167
} ;
155
168
if ( branch ) {
156
169
setSelectedBranchName ( branch . name ) ;
157
170
setValidCommitHash ( true ) ;
158
171
} else if ( commitSpecifier != CommitSpecifier . BRANCH ) {
159
- setSelectedBranchName ( "N/A " ) ;
172
+ setSelectedBranchName ( "[Specific commit] " ) ;
160
173
fetchCommitIsValid ( ) . catch ( console . error ) ;
161
174
}
162
- } , [ selectedCommitHash , commitSpecifier , CommitSpecifier . COMMIT_HASH ] ) ;
175
+ } , [
176
+ selectedCommitHash ,
177
+ commitSpecifier ,
178
+ CommitSpecifier . BRANCH ,
179
+ branches ,
180
+ commitHashes ,
181
+ ] ) ;
163
182
164
183
useEffect ( ( ) => {
165
184
const fetchBranches = async ( ) => {
166
- const _branches = await Branches ( owner , repo ) ;
185
+ const _branches = await Branches ( owner , repo , false ) ;
167
186
setBranches ( _branches ) ;
168
187
} ;
188
+ const fetchCommitHashes = async ( ) => {
189
+ const _commitHashes = await Commits ( owner , repo , false ) ;
190
+ setCommitHashes ( _commitHashes ) ;
191
+ } ;
169
192
fetchBranches ( ) . catch ( console . error ) ;
193
+ fetchCommitHashes ( ) . catch ( console . error ) ;
170
194
} , [ ] ) ;
171
195
196
+ const refreshBranches = async ( selectedCommitHash : string ) => {
197
+ const _branches = await Branches ( owner , repo , true ) ;
198
+ setBranches ( _branches ) ;
199
+ const _commitHashes = await Commits ( owner , repo , true ) ;
200
+ setCommitHashes ( _commitHashes ) ;
201
+ } ;
202
+
172
203
return (
173
204
< form
174
205
onSubmit = { handleIPAFormSubmit }
@@ -177,19 +208,40 @@ function IPAForm({
177
208
< h2 className = "text-2xl mb-2 font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight" >
178
209
IPA Query
179
210
</ h2 >
180
- < div onClick = { ( ) => setCommitSpecifier ( CommitSpecifier . BRANCH ) } >
181
- < PassedStateSelectMenu
182
- id = "branch"
183
- label = "Branch"
184
- options = { branchNames }
185
- selected = { selectedBranchName }
186
- setSelected = { setSelectedBranchName }
187
- disabled = { disableBranch }
188
- />
211
+ < div className = "flex items-end" >
212
+ < div
213
+ className = "flex-grow"
214
+ onClick = { ( ) => {
215
+ setCommitSpecifier ( CommitSpecifier . BRANCH ) ;
216
+ } }
217
+ >
218
+ < PassedStateSelectMenu
219
+ id = "branch"
220
+ label = "Branch"
221
+ options = { branchNames }
222
+ selected = { selectedBranchName }
223
+ setSelected = { setSelectedBranchName }
224
+ disabled = { disableBranch }
225
+ />
226
+ </ div >
227
+ < button
228
+ className = "relative cursor-default rounded-md bg-white py-2.5 px-3 ml-1 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6"
229
+ onClick = { ( e ) => {
230
+ e . preventDefault ( ) ;
231
+ refreshBranches ( selectedCommitHash ) ;
232
+ } }
233
+ >
234
+ < ArrowPathIcon className = "h-4 w-4" />
235
+ </ button >
189
236
</ div >
190
237
< div
191
238
className = "relative mt-2 rounded-md shadow-sm"
192
- onClick = { ( ) => setCommitSpecifier ( CommitSpecifier . COMMIT_HASH ) }
239
+ onClick = { ( ) => {
240
+ setCommitSpecifier ( CommitSpecifier . COMMIT_HASH ) ;
241
+ if ( commitHashInputRef . current ) {
242
+ commitHashInputRef . current . select ( ) ;
243
+ }
244
+ } }
193
245
>
194
246
< label
195
247
htmlFor = "commit_hash"
@@ -200,21 +252,37 @@ function IPAForm({
200
252
>
201
253
Commit Hash
202
254
</ label >
203
- < input
204
- type = "string"
205
- name = "commit_hash"
206
- id = "commit_hash"
207
- className = { clsx (
208
- "block w-full rounded-md border-0 py-1.5 pl-3 text-gray-900 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6" ,
209
- ! validCommitHash &&
210
- "text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500" ,
211
- disableCommitHash && "opacity-25" ,
212
- ) }
213
- value = { selectedCommitHash }
214
- onChange = { ( e ) => setSelectedCommitHash ( e . target . value ) }
215
- aria-invalid = "true"
216
- aria-describedby = "email-error"
217
- />
255
+ < Combobox value = { selectedCommitHash } onChange = { setSelectedCommitHash } >
256
+ < Combobox . Input
257
+ onChange = { ( event ) => setSelectedCommitHash ( event . target . value ) }
258
+ type = "string"
259
+ name = "commit_hash"
260
+ id = "commit_hash"
261
+ ref = { commitHashInputRef }
262
+ className = { clsx (
263
+ "block w-full rounded-md border-0 py-1.5 pl-3 text-gray-900 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6" ,
264
+ ! validCommitHash &&
265
+ "text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500" ,
266
+ disableCommitHash && "opacity-25" ,
267
+ ) }
268
+ />
269
+ < Combobox . Options className = "absolute z-10 w-full mt-1 overflow-auto max-h-60 text-gray-900 bg-white shadow-lg rounded-md border border-gray-300 divide-y divide-gray-200 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" >
270
+ { filteredCommitHashes . map ( ( commit ) => (
271
+ < Combobox . Option key = { commit } value = { commit } as = { Fragment } >
272
+ { ( { active, selected } ) => (
273
+ < li
274
+ className = { clsx (
275
+ "py-2 px-2.5" ,
276
+ active ? "bg-blue-500 text-white" : "text-black" ,
277
+ ) }
278
+ >
279
+ { commit }
280
+ </ li >
281
+ ) }
282
+ </ Combobox . Option >
283
+ ) ) }
284
+ </ Combobox . Options >
285
+ </ Combobox >
218
286
219
287
{ ! validCommitHash && (
220
288
< div className = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 pt-10" >
@@ -343,15 +411,28 @@ function PassedStateSelectMenu({
343
411
disabled && "opacity-25" ,
344
412
) }
345
413
>
346
- < Listbox . Button className = "relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" >
347
- < span className = "block truncate" > { selected } </ span >
348
- < span className = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" >
349
- < ChevronUpDownIcon
350
- className = "h-5 w-5 text-gray-400"
351
- aria-hidden = "true"
352
- />
353
- </ span >
354
- </ Listbox . Button >
414
+ { disabled ? (
415
+ // Listbox.Button overrides the onClick, but we only need that to reactivate.
416
+ < div className = "relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" >
417
+ < span className = "block truncate" > { selected } </ span >
418
+ < span className = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" >
419
+ < ChevronUpDownIcon
420
+ className = "h-5 w-5 text-gray-400"
421
+ aria-hidden = "true"
422
+ />
423
+ </ span >
424
+ </ div >
425
+ ) : (
426
+ < Listbox . Button className = "relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" >
427
+ < span className = "block truncate" > { selected } </ span >
428
+ < span className = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" >
429
+ < ChevronUpDownIcon
430
+ className = "h-5 w-5 text-gray-400"
431
+ aria-hidden = "true"
432
+ />
433
+ </ span >
434
+ </ Listbox . Button >
435
+ ) }
355
436
356
437
< Transition
357
438
show = { open }
0 commit comments