Skip to content

Commit

Permalink
✨ Generic RepoFinder component (#144)
Browse files Browse the repository at this point in the history
* ✨ Add typeahead search in cmd palette

* ✨ Add a generic RepoFinder input field that searches by handle/did and shows profile details

* ✨ Bring back missing props for input field

* 🧹 use DID instead of handle for key
  • Loading branch information
foysalit authored Jul 4, 2024
1 parent 8b99f74 commit 54581e0
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 18 deletions.
17 changes: 9 additions & 8 deletions components/config/external-labeler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import client from '@/lib/client'
import { ErrorInfo } from '@/common/ErrorInfo'
import { buildBlueSkyAppUrl } from '@/lib/util'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid'
import { RepoFinder } from 'components/repo/Finder'

const BrowserReactJsonView = dynamic(() => import('react-json-view'), {
ssr: false,
Expand Down Expand Up @@ -58,14 +59,14 @@ export const ExternalLabelerConfig = () => {

<div className="flex flex-row justify-end items-end my-3 gap-2">
<FormLabel label="Labeler DID" htmlFor="did" className="flex-1">
<Input
type="text"
id="did"
name="did"
required
placeholder="did:plc:..."
className="block w-full"
onChange={(e) => setDid(e.target.value)}
<RepoFinder
onChange={(value) => setDid(value)}
inputProps={{
required: true,
className: 'block w-full',
id: 'did',
name: 'did',
}}
/>
</FormLabel>
<ActionButton
Expand Down
21 changes: 11 additions & 10 deletions components/mod-event/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CreateMacroForm } from './CreateMacroForm'
import { useFilterMacroUpsertMutation } from './useFilterMacrosList'
import { MacroList } from './MacroPicker'
import { useState } from 'react'
import { RepoFinder } from 'components/repo/Finder'

export const EventFilterPanel = ({
types,
Expand Down Expand Up @@ -166,20 +167,20 @@ export const EventFilterPanel = ({
htmlFor="createdBy"
className="flex-1 mt-2"
>
<Input
type="text"
id="createdBy"
name="createdBy"
placeholder="DID of the author of the event"
className="block w-full"
value={createdBy || ''}
onChange={(ev) =>
<RepoFinder
inputProps={{
className: 'w-full',
type: 'text',
id: 'createdBy',
name: 'createdBy',
placeholder: 'DID of the author of the event',
}}
onChange={(value) =>
changeListFilter({
field: 'createdBy',
value: ev.target.value,
value,
})
}
autoComplete="off"
/>
</FormLabel>

Expand Down
150 changes: 150 additions & 0 deletions components/repo/Finder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useState, useEffect } from 'react'
import { Combobox } from '@headlessui/react'
import client from '@/lib/client'
import { classNames } from '@/lib/util'

type TypeaheadResult = {
did: string
handle: string
avatar?: string
displayName?: string
}

type RepoFinderProps = {
selectionType?: 'did' | 'handle'
onChange: (value: string) => void
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
}

const DefaultResult = {
did: '',
handle: 'DID or Handle',
displayName: 'Start typing to search...',
}

const ErrorResult = {
did: '',
handle: 'Error searching',
displayName: 'Start typing to search...',
}

const getProfilesForQuery = async (q: string): Promise<TypeaheadResult[]> => {
const headers = { headers: client.proxyHeaders() }
if (q.startsWith('did:')) {
const { data: profile } = await client.api.app.bsky.actor.getProfile(
{
actor: q,
},
headers,
)

return profile
? [
{
displayName: profile.displayName,
handle: profile.handle,
avatar: profile.avatar,
did: profile.did,
},
]
: []
}

const {
data: { actors },
} = await client.api.app.bsky.actor.searchActorsTypeahead({ q }, headers)

return actors.map((actor) => ({
displayName: actor.displayName,
handle: actor.handle,
avatar: actor.avatar,
did: actor.did,
}))
}

export function RepoFinder({
selectionType = 'did',
onChange,
inputProps = {},
}: RepoFinderProps) {
const [query, setQuery] = useState('')
const [selectedItem, setSelectedItem] = useState<string>('')
const [items, setItems] = useState<TypeaheadResult[]>([DefaultResult])
const [loading, setLoading] = useState(false)

useEffect(() => {
if (query.length > 0) {
setLoading(true)
getProfilesForQuery(query)
.then((profiles) => {
setItems(profiles)
setLoading(false)
})
.catch((error) => {
console.error('Error fetching data:', error)
setLoading(false)
setItems([ErrorResult])
})
} else {
setItems([DefaultResult])
}
}, [query])

return (
<Combobox
value={selectedItem}
onChange={(item) => {
setSelectedItem(item)
onChange(item)
}}
>
<Combobox.Input
// This is intentionally spread on top so that any of the below props are passed via inputProps, they are ignored
// This also helps with the classname overwrite
{...inputProps}
className={classNames(
'rounded-md border-gray-300 dark:border-teal-500 dark:bg-slate-700 shadow-sm dark:shadow-slate-700 focus:border-indigo-500 focus:ring-indigo-500 dark:focus:ring-teal-500 sm:text-sm disabled:text-gray-500 dark:text-gray-100 disabled:dark:text-gray-300',
inputProps.className,
)}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<Combobox.Options className="rounded mt-1 max-h-60 overflow-y-auto bg-gray-100 dark:bg-slate-900 shadow-sm">
{loading ? (
<div className="p-2">Loading...</div>
) : items.length === 0 && query !== '' ? (
<div className="p-2">
<div className="font-semibold text-sm">No results found</div>
<div className="text-sm">
Queries must be partial handle or full DIDs
</div>
</div>
) : (
items.map((item) => (
<Combobox.Option
key={item.did}
value={selectionType === 'did' ? item.did : item.handle}
className={({ active }) =>
`cursor-pointer p-2 flex items-center space-x-3 ${
active ? 'bg-gray-400 text-white dark:bg-slate-700' : ''
}`
}
>
<img
alt={item.displayName || item.handle}
className="h-7 w-7 rounded-full"
src={item.avatar || '/img/default-avatar.jpg'}
/>
<div>
<div className="font-semibold text-sm">@{item.handle}</div>
<div className="text-sm">
{item.displayName || 'No display name'}
</div>
</div>
</Combobox.Option>
))
)}
</Combobox.Options>
</Combobox>
)
}

0 comments on commit 54581e0

Please sign in to comment.