import { Component, SyntheticEvent, ReactNode, Fragment, KeyboardEvent, FC } from 'react'; import { Checkbox, TableFilterCellProps, Radio, Input, Spinner, BodyText, } from '@servicetitan/design-system'; import { IdType } from '@servicetitan/data-query'; import { CustomColumnMenuFilterSingleOpts, renderCustomColumnMenuFilter, } from '../column-menu-filters'; import { makeObservable, observable, runInAction, toJS } from 'mobx'; import { observer } from 'mobx-react'; import { getSimpleValue } from './value-getter'; import { objectSearch } from './object-search'; import { selectColumnMenuFilterOperators } from './operators'; export type SelectFilterDataFetcher = (opts: { search?: string }) => Promise<{ data: TO[] }>; export interface SelectFilterSearchOptions { placeholder?: string; filter(search: string): (item: TO) => boolean | undefined; } interface SelectorProps { search?: SelectFilterSearchOptions; selected: TO[]; data?: TO[]; dataFetcher?: SelectFilterDataFetcher; itemComponent: FC>; onChange(option: TO, checked: boolean, event: SyntheticEvent): void; valueSelector(item: TO): IdType; renderItem(item: TO): ReactNode; } interface SelectorItemProps { option: TO; checked: boolean; onChange(option: TO, checked: boolean, event: SyntheticEvent): void; renderer?(item: TO): ReactNode; } @observer class SelectorAsync extends Component> { @observable shownOptions: TO[] = []; @observable search = ''; @observable error = false; @observable loading = false; constructor(props: SelectorProps) { super(props); makeObservable(this); } componentDidMount() { this.searchOptions().catch(); } handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { event.stopPropagation(); } }; handleSearch = (_0: SyntheticEvent, data: { value: string }) => { runInAction(() => (this.search = data.value)); this.searchOptions().catch(); }; searchOptions = async () => { if (this.props.dataFetcher) { runInAction(() => { this.loading = true; }); } try { const data = await this.getData(); runInAction(() => { this.shownOptions = data; this.error = false; this.loading = false; }); } catch { runInAction(() => { this.error = true; this.loading = false; }); } }; render() { const selectedOptions = this.props.selected; const selected = new Set(selectedOptions.map(opt => this.props.valueSelector(opt))); const ItemComponent = this.props.itemComponent; return ( {!!this.props.search && ( )}
{!!selectedOptions.length && (
{selectedOptions.map((option, index) => ( ))}
)} {this.error ? ( Unable to load options ) : this.shownOptions.length ? ( this.shownOptions .filter(opt => !selected.has(this.props.valueSelector(opt))) .map((option, index) => ( )) ) : this.loading ? (   ) : ( No options match search criteria )} {this.loading && (
)}
); } private async getData(): Promise { if (this.props.dataFetcher) { const { data } = await this.props.dataFetcher({ search: this.search, }); return data; } let data = this.props.data ?? []; if (this.props.search?.filter) { data = data.filter(this.props.search.filter(this.search)); } return data; } private getItemKey = (item: TO, index: number) => { return `${index}__${this.props.valueSelector(item)}`; }; } const SelectorItemSingle: FC> = ({ option, renderer, checked, onChange, }) => ( onChange(option, true, event)} className="m-b-1" /> ); const SelectorItemMultiple: FC> = ({ option, renderer, checked, onChange, }) => ( onChange(option, checked, event)} className="m-b-1" /> ); export interface SelectFilterOptions extends CustomColumnMenuFilterSingleOpts { /** Can select multiple options in filter */ multiple?: boolean; /** Ability to search options in filter */ search?: boolean | Partial; /** Static options to show in filter */ data?: TO[]; /** Method to fetch filter options asynchronously */ dataFetcher?: SelectFilterDataFetcher; /** Search operator passed to table state */ operator?: ((value: any, options?: TO[]) => boolean) | ((value: any, options?: TO) => boolean); /** Select item value (ex id) for complex items */ valueSelector?(item: TO): IdType; /** Select row item value (from table source row field) for complex items */ rowValueSelector?(item: any): IdType | undefined; /** Render option label */ renderItem?(item: TO): ReactNode; } interface TableFilterCellPropsTyped extends Omit { value?: T; } export function selectColumnMenuFilter({ dataFetcher, data, search, multiple, valueSelector = getSimpleValue, rowValueSelector = getSimpleValue, renderItem, operator, ...opts }: SelectFilterOptions) { const renderer = renderItem ?? (item => valueSelector(item)); const searchOptions = search ? { filter: objectSearch, ...(typeof search === 'boolean' ? {} : search), } : undefined; if (multiple) { const FilterCell = ({ value, onChange }: TableFilterCellPropsTyped) => { const handleChange = ( option: TO, checked: boolean, event: SyntheticEvent ) => { const val = checked ? (value ?? []).concat(option) : (value ?? []).filter(opt => valueSelector ? valueSelector(opt) !== valueSelector(option) : option !== opt ); onChange({ value: val.length ? toJS(val) : undefined, operator: val.length ? (operator ?? selectColumnMenuFilterOperators.getContains({ valueSelector, rowValueSelector, })) : '', syntheticEvent: event, }); }; return ( ); }; return renderCustomColumnMenuFilter(FilterCell, opts); } const FilterCell = ({ value, onChange }: TableFilterCellPropsTyped) => { const handleChange = (option: TO, _: boolean, event: SyntheticEvent) => { onChange({ value: toJS(option), operator: operator ?? selectColumnMenuFilterOperators.getEquals({ valueSelector, rowValueSelector }), syntheticEvent: event, }); }; return ( ); }; return renderCustomColumnMenuFilter(FilterCell, opts); } export { selectColumnMenuFilterOperators };