Фронтенд для сервиса #5
@@ -328,6 +328,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, on
|
|||||||
clearLabel="Без отдела (маршрут по умолчанию)"
|
clearLabel="Без отдела (маршрут по умолчанию)"
|
||||||
placeholder="Без отдела (маршрут по умолчанию)"
|
placeholder="Без отдела (маршрут по умолчанию)"
|
||||||
searchPlaceholder="Поиск по названию отдела..."
|
searchPlaceholder="Поиск по названию отдела..."
|
||||||
|
emptyMessage="У выбранной компании нет отделов в справочнике"
|
||||||
onChange={departmentId => setForm({ ...form, departmentId })}
|
onChange={departmentId => setForm({ ...form, departmentId })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface ISearchableSelectProps {
|
|||||||
/** Показывать пункт «очистить» — для необязательных полей. */
|
/** Показывать пункт «очистить» — для необязательных полей. */
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
clearLabel?: string;
|
clearLabel?: string;
|
||||||
|
/** Текст, когда список опций пуст (не результат поиска, а вообще нечего выбирать). */
|
||||||
|
emptyMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchableSelect: React.FC<ISearchableSelectProps> = ({
|
export const SearchableSelect: React.FC<ISearchableSelectProps> = ({
|
||||||
@@ -31,25 +33,35 @@ export const SearchableSelect: React.FC<ISearchableSelectProps> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
loading,
|
loading,
|
||||||
allowClear,
|
allowClear,
|
||||||
clearLabel = 'Не выбрано'
|
clearLabel = 'Не выбрано',
|
||||||
|
emptyMessage = 'Ничего не найдено'
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [query, setQuery] = React.useState('');
|
const [query, setQuery] = React.useState('');
|
||||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const selected = options.filter(o => o.value === value)[0];
|
// Сохранённое значение может отсутствовать в справочнике (запись удалили в 1С
|
||||||
|
// или она не проходит текущий фильтр). Показать в этом случае плейсхолдер —
|
||||||
|
// значит соврать, будто поле пустое, и затереть значение при сохранении.
|
||||||
|
// Поэтому подмешиваем недостающую опцию явным образом.
|
||||||
|
const effectiveOptions = React.useMemo(() => {
|
||||||
|
if (!value || options.some(o => o.value === value)) return options;
|
||||||
|
return [{ value, label: value, sublabel: 'Нет в справочнике' }, ...options];
|
||||||
|
}, [options, value]);
|
||||||
|
|
||||||
|
const selected = effectiveOptions.filter(o => o.value === value)[0];
|
||||||
|
|
||||||
// Поиск на клиенте: справочник целиком уже загружен, отделов ~700 —
|
// Поиск на клиенте: справочник целиком уже загружен, отделов ~700 —
|
||||||
// фильтровать в памяти дешевле, чем ходить на сервер на каждый символ.
|
// фильтровать в памяти дешевле, чем ходить на сервер на каждый символ.
|
||||||
const filtered = React.useMemo(() => {
|
const filtered = React.useMemo(() => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
if (!q) return options;
|
if (!q) return effectiveOptions;
|
||||||
return options.filter(o =>
|
return effectiveOptions.filter(o =>
|
||||||
o.label.toLowerCase().indexOf(q) !== -1 ||
|
o.label.toLowerCase().indexOf(q) !== -1 ||
|
||||||
(o.sublabel ? o.sublabel.toLowerCase().indexOf(q) !== -1 : false)
|
(o.sublabel ? o.sublabel.toLowerCase().indexOf(q) !== -1 : false)
|
||||||
);
|
);
|
||||||
}, [options, query]);
|
}, [effectiveOptions, query]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -110,7 +122,11 @@ export const SearchableSelect: React.FC<ISearchableSelectProps> = ({
|
|||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
{filtered.length === 0 && <div className={styles.empty}>Ничего не найдено</div>}
|
{filtered.length === 0 && (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
{effectiveOptions.length === 0 ? emptyMessage : 'Ничего не найдено'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{filtered.map(opt => (
|
{filtered.map(opt => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user