95 lines
3.1 KiB
JavaScript
95 lines
3.1 KiB
JavaScript
/* ============================================================
|
|
SearchSelect — type-to-filter picker whose value is always an option id.
|
|
|
|
Moved out of screens/Jobs.jsx so Requisitions can use the same control.
|
|
Options are `{ id, name, email?, role_name? }`. Pass `onQueryChange` when the
|
|
caller filters server-side; without it the list is filtered in place.
|
|
============================================================ */
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
/**
|
|
* Searchable picker: type to filter, click a row to store the id.
|
|
* Not free-text — the value is always an option id (or '' when allowEmpty).
|
|
*/
|
|
export default function SearchSelect({
|
|
options = [],
|
|
value,
|
|
onChange,
|
|
placeholder = 'Search…',
|
|
disabled = false,
|
|
loading = false,
|
|
allowEmpty = false,
|
|
emptyLabel = 'Unassigned',
|
|
error = false,
|
|
onQueryChange,
|
|
}) {
|
|
const [q, setQ] = useState('')
|
|
const [open, setOpen] = useState(false)
|
|
const root = useRef(null)
|
|
const selected = options.find((o) => String(o.id) === String(value || ''))
|
|
|
|
useEffect(() => {
|
|
function onDoc(e) {
|
|
if (root.current && !root.current.contains(e.target)) setOpen(false)
|
|
}
|
|
document.addEventListener('mousedown', onDoc)
|
|
return () => document.removeEventListener('mousedown', onDoc)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!onQueryChange || !open) return
|
|
onQueryChange(q)
|
|
}, [q, open, onQueryChange])
|
|
|
|
const term = q.trim().toLowerCase()
|
|
const filtered = onQueryChange
|
|
? options
|
|
: options.filter((o) => {
|
|
if (!term) return true
|
|
const hay = [o.name, o.email, o.role_name].filter(Boolean).join(' ').toLowerCase()
|
|
return hay.includes(term)
|
|
})
|
|
|
|
return (
|
|
<div className={`dropdown${open ? ' open' : ''}`} ref={root} style={{ width: '100%' }}>
|
|
<input
|
|
className={error ? 'err' : ''}
|
|
value={open ? q : (selected?.name || '')}
|
|
disabled={disabled || loading}
|
|
placeholder={loading ? 'Loading…' : placeholder}
|
|
autoComplete="off"
|
|
onFocus={() => { setOpen(true); setQ('') }}
|
|
onChange={(e) => { setQ(e.target.value); setOpen(true) }}
|
|
/>
|
|
{open && !disabled && !loading && (
|
|
<div className="dropdown-menu" style={{ left: 0, right: 'auto', width: '100%', minWidth: 0, maxHeight: 240, overflowY: 'auto' }}>
|
|
{allowEmpty && (
|
|
<button
|
|
type="button"
|
|
className="dropdown-link"
|
|
onClick={() => { onChange(''); setQ(''); setOpen(false) }}
|
|
>
|
|
{emptyLabel}
|
|
</button>
|
|
)}
|
|
{filtered.length === 0 && (
|
|
<div className="dropdown-link" style={{ color: 'var(--text-3)' }}>No matches</div>
|
|
)}
|
|
{filtered.map((o) => (
|
|
<button
|
|
type="button"
|
|
key={o.id}
|
|
className="dropdown-link"
|
|
onClick={() => { onChange(String(o.id)); setQ(''); setOpen(false) }}
|
|
>
|
|
{o.name}
|
|
{o.email ? <span className="cell-sub" style={{ marginLeft: 8 }}>{o.email}</span> : null}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|