Skip to content

Instantly share code, notes, and snippets.

@liamkernighan
Created March 11, 2020 12:23
Show Gist options
  • Save liamkernighan/25456b661556117ecb54c8da23ade905 to your computer and use it in GitHub Desktop.
Save liamkernighan/25456b661556117ecb54c8da23ade905 to your computer and use it in GitHub Desktop.
React Select
import { useTheme } from '@material-ui/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import Select from 'react-select';
import { ValueType } from 'react-select/src/types';
import { SelectedOption, SelectorProps } from './Types';
type Props = SelectorProps & {
required?: boolean;
};
export const AutocompleteSelector = observer((props: Props) => {
const theme = useTheme();
const selectStyles = {
input: (base: React.CSSProperties) => ({
...base,
color: theme.palette.text.primary,
'& input': {
font: 'inherit',
},
}),
menu: (styles: React.CSSProperties) => ({ ...styles, zIndex: 1000 })
};
const handleSelected = (option: ValueType<SelectedOption>) => {
if (option !== null) {
props.onSelect(option);
}
else if (props.isMulti) {
props.onSelect([]);
}
else {
props.onSelect(null);
}
};
return <>
<Select
options={props.options}
onChange={handleSelected}
value={props.selectedValues}
placeholder={props.placeholder || 'Выберите'}
styles={selectStyles}
isSearchable={true}
isClearable={true}
isLoading={props.isLoading}
loadingMessage={() => 'ищем...'}
isMulti={props.isMulti}
onMenuOpen={props.onMenuOpen}
onInputChange={props.onInputChange}
noOptionsMessage={() => 'Пусто'} />
{props.required && <input
tabIndex={-1}
autoComplete='off'
style={{ opacity: 0, height: 0 }}
value={props.selectedValues ? 'Filled' : undefined}
required
/>}
</>;
});
import { Button } from '@material-ui/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AutocompleteSelector } from '../../Shared/AutocompleteSelector/AutocompleteSelector';
import { MonthRangePicker } from '../../Shared/DatePicker/MonthRangePicker';
import { AlignRight } from '../../SportsmanCard/StyledComponents';
import { ButtonPanel, PanelButton, Paragraph } from '../StyledComponents';
import { UmoStore } from './Store';
type Props = {
store: UmoStore;
};
@observer
export class UmoFilterContent extends React.Component<Props> {
render() {
const {store} = this.props;
return <>
<span style={{display: 'flex'}}>
<AlignRight>
<Button onClick={store.resetButtonOnClick}>
Сбросить поиск
</Button>
</AlignRight>
</span>
<Paragraph>
<label>Вид спорта / дисциплина: </label>
<AutocompleteSelector
onMenuOpen={store.fetchSportsAndDisciplines}
isLoading={store.sportsAndDisciplinesDropdownIsLoading}
options={store.sportsAndDisciplinesOptions}
onSelect={store.onSportsDisciplinesSelect}
selectedValues={store.selectedSportsAndDisciplinesWithFilter}
isMulti={true} />
</Paragraph>
{/* undone important разобраться с проблемой типизации у селекта (мульти\не мульти) */}
<Paragraph>
<label>Место осмотра: </label>
<AutocompleteSelector
onMenuOpen={store.fetchPlaces}
isLoading={store.placesDropdownIsLoading}
options={store.placesOptions}
onSelect={store.onPlaceSelect}
selectedValues={store.selectedPlaces}
isMulti={true}
/>
</Paragraph>
<label>Период:</label>
<ButtonPanel>
<PanelButton onClick={store.setMinus3Plus6} variant='contained'>-6 +3 мес.</PanelButton>
<PanelButton onClick={store.setThisMonth} variant='contained'>Этот месяц</PanelButton>
<PanelButton onClick={store.setPreviousMonth} variant='contained'>Предыдущий месяц</PanelButton>
</ButtonPanel>
<MonthRangePicker store={store.rangePickerStore} />
</>;
}
}
import Axios from 'axios';
import * as dayjs from 'dayjs';
import groupBy from 'lodash-es/groupBy';
import uniq from 'lodash-es/uniq';
import { action, computed, observable, toJS } from 'mobx';
import { ValueType } from 'react-select/src/types';
import { DateHelpers } from '../../../Helpers/DateHelpers';
import { Helpers } from '../../../Helpers/Helpers';
import { ClientHttp } from '../../../Helpers/HttpService/ClientHttp';
import { LocalStorageFacade } from '../../../Helpers/HttpService/LocalStorageFacade';
import { getTeamDataRequest } from '../../../Server/ApiRequestBlueprints';
import { GlobalContext } from '../../RootSSRComponent/GlobalContext';
import { SelectedOption } from '../../Shared/AutocompleteSelector/Types';
import { MonthRangePickerStore } from '../../Shared/DatePicker/Store';
import { SingleCard, SportAndDisciplineDropdownValue, UmoFilterSettings, ValueAndGuid } from './Types';
export class UmoStore {
constructor() {
if(Helpers.isClient()) {
GlobalContext.instance.store.clearCallback();
}
}
@action
public setCurrentlyOpened = (id: string) => {
const element = this.data.find(r => r.data._id.$oid === id);
if (element) {
element.expanded = !element.expanded;
!!element.expanded
? LocalStorageFacade.setValue('expandedElementId', id)
: LocalStorageFacade.deleteValue('expandedElementId');
}
}
public deletePlace = (id: string) => {
this.selectedPlaces = this.selectedPlaces.filter(r => r.value !== id);
this.startSearchingWithNewFilterSet();
}
public deleteSportAndDiscipline = (id: string) => {
this.selectedSportsAndDisciplines = this.selectedSportsAndDisciplines.filter(r => r.value !== id);
this.startSearchingWithNewFilterSet();
}
public loadInitialData = async () => {
this.isLoading = true;
await this.fetchSportsAndDisciplines();
await Promise.all([this.fetchPlaces(), this.restoreFilterSettings()]);
await this.startSearchingWithNewFilterSet(false);
this.restoreOpenedCard();
}
public restoreOpenedCard = () => {
const openedCardId = LocalStorageFacade.getValue('expandedElementId');
// tslint:disable-next-line:no-unused-expression
openedCardId && this.setCurrentlyOpened(openedCardId);
//openedCardId && this.loadDetails(openedCardId);
}
private saveFilterSettings = () => {
const settings: UmoFilterSettings = {
selectedSportsAndDisciplines: this.selectedSportsAndDisciplines,
selectedPlaces: this.selectedPlaces,
startDate: this.startDate.format(),
endDate: this.endDate.format(),
};
LocalStorageFacade.setValue('umoFilterSettings', settings);
}
public restoreFilterSettings = () => {
const settings = LocalStorageFacade.getValue('umoFilterSettings');
if (!settings) {
return Promise.resolve();
}
if (settings.selectedSportsAndDisciplines) {
this.selectedSportsAndDisciplines = settings.selectedSportsAndDisciplines;
}
if (settings.selectedPlaces) {
this.selectedPlaces = settings.selectedPlaces;
}
if (settings.startDate) {
this.startDate = dayjs(settings.startDate);
}
if (settings.endDate) {
this.endDate = dayjs(settings.endDate);
}
return Promise.resolve();
}
//#region Настройки периода
@observable startDate = dayjs().add(-6, 'month').startOf('month');
@observable endDate = dayjs().add(3, 'month').endOf('month');
private defaultStartDate = dayjs().add(-20, 'year').startOf('month');
private defaultEndDate = dayjs().add(20, 'year').endOf('month');
public setEmptyDateInterval = () => {
this.startDate = this.defaultStartDate;
this.endDate = this.defaultEndDate;
this.startSearchingWithNewFilterSet();
}
@computed get showDateChip() {
return !this.startDate.isSame(this.defaultStartDate) && !this.endDate.isSame(this.defaultEndDate);
}
public setMinus3Plus6 = () => {
this.startDate = dayjs().add(-6, 'month').startOf('month');
this.endDate = dayjs().add(3, 'month').endOf('month');
this.startSearchingWithNewFilterSet();
}
public setThisMonth = () => {
this.startDate = dayjs().startOf('month');
this.endDate = dayjs().endOf('month');
this.startSearchingWithNewFilterSet();
}
public setPreviousMonth = () => {
this.startDate = dayjs().startOf('month').add(-1, 'month').startOf('month');
this.endDate = dayjs().endOf('month').add(-1, 'month').endOf('month');
this.startSearchingWithNewFilterSet();
}
//#endregion
@observable private _drawerIsOpened = false;
public drawerIsOpened = () => this._drawerIsOpened;
public setDrawerIsOpened = (value: boolean) => {
this._drawerIsOpened = value;
const globalStore = GlobalContext.instance.store;
if (value) {
globalStore.setCallback(() => this.setDrawerIsOpened(false));
}
else {
globalStore.clearCallback();
}
}
@observable public isLoading = true;
@computed private get currentFilters() {
return [this.selectedSportsAndDisciplines, this.selectedPlaces, this.startDate, this.endDate];
}
@computed get filtersAreActive() {
return this.currentFilters.some(r => r !== null);
}
private clearAllFilters = () => {
this.selectedSportsAndDisciplines = [];
this.selectedPlaces = [];
this.setMinus3Plus6();
}
public resetButtonOnClick = () => {
this.clearAllFilters();
}
@observable public cardStates: SingleCard[] = [];
@observable public data: SingleCard[] = [];
@computed public get dates(): string[] {
const dates = this.data.map(r => r.data.approvalOrProposalDate['$date']);
return uniq(dates);
}
@observable public loadMoreIsLoading = false;
@action public loadMoreButtonOnClick = () => {
this.loadMoreIsLoading = true;
this.currentPage++;
this.startLoadingElementsFromApi(() => this.loadMoreIsLoading = false, 'append');
}
@observable private currentPage = 1;
// места проведения осмотра
@observable public placesDropdownIsLoading = false;
@observable public placesList: ValueAndGuid[] | null = null;
@observable public selectedPlaces: SelectedOption[] = [];
@action public onPlaceSelect = (selectedOptions: ValueType<SelectedOption>) => {
this.selectedPlaces = selectedOptions as SelectedOption[];
this.startSearchingWithNewFilterSet();
}
@computed public get placesOptions(): SelectedOption[] {
if (this.placesList !== null) {
return this.placesList.map(val => ({ value: val.id, label: val.value }));
}
return [];
}
// комбинированный селектор видов спорта и дисциплин
@observable public sportsAndDisciplinesDropdownIsLoading = false;
@observable public sportsAndDisciplinesList: SportAndDisciplineDropdownValue[] | null = null;
@observable public selectedSportsAndDisciplines: SelectedOption[] = [];
@action public onSportsDisciplinesSelect = (selectedOptions: ValueType<SelectedOption>) => {
this.selectedSportsAndDisciplines = selectedOptions as SelectedOption[];
this.startSearchingWithNewFilterSet();
}
@computed public get sportsAndDisciplinesOptions(): SelectedOption[] {
return this.sportsAndDisciplinesListWithFilter.map(r => ({ value: r.id, label: this.createLabel(r) }));
}
@computed
public get selectedSportsAndDisciplinesWithFilter() {
const filterIds = this.sportsAndDisciplinesListWithFilter.map(r => r.id);
return this.selectedSportsAndDisciplines.filter(r => filterIds.includes(r.value));
}
@computed
public get sportsAndDisciplinesListWithFilter() {
const currentDoctorSports = GlobalContext.instance.store.currentSports;
return this.sportsAndDisciplinesList
&& this.sportsAndDisciplinesList.filter(r => currentDoctorSports.includes(r.sport)) || [];
}
private createLabel = (value: SportAndDisciplineDropdownValue) => [value.sport, value.discipline].filter(r => r !== null).join('/');
private getQueryModel = () => {
const requestModel = getTeamDataRequest();
const filterModifiers: object[] = [];
if (this.selectedPlaces !== null && this.selectedPlaces.length > 0) {
filterModifiers.push({ 'facility.name': { '$in': this.selectedPlaces.map(x => x.label) } });
}
filterModifiers.push({ approvalOrProposalDate: DateHelpers.getDateInterval(this.startDate, this.endDate) });
filterModifiers.forEach(mod => {
requestModel.filter = { ...requestModel.filter, ...mod };
});
requestModel.page = this.currentPage;
return requestModel;
}
public startSearchingWithNewFilterSet = async (save = true) => {
this.currentPage = 1;
this.isLoading = true;
if (save) {
this.saveFilterSettings();
}
await this.startLoadingElementsFromApi(() => this.isLoading = false, 'replace');
this.rangePickerStore.recieveNewRange(this.startDate, this.endDate);
}
private startLoadingElementsFromApi = async (callback: () => void, mode: 'append' | 'replace') => {
const sportsAndDiscpilinesToLoad = this.selectedSportsAndDisciplinesWithFilter.length === 0
? GlobalContext.instance.store.currentSports.map(s => `{"sport":"${s}","discipline":null}`) // Ну это просто пиздец
: this.selectedSportsAndDisciplinesWithFilter.map(s => s.value);
const selectedSportsAndDisciplines =
toJS(this.sportsAndDisciplinesListWithFilter)
.filter(r => sportsAndDiscpilinesToLoad.includes(r.id));
const groupedDisciplines = Object.entries(groupBy(selectedSportsAndDisciplines, r => r.sport));
const promises = groupedDisciplines
.map(r => {
const query = this.getQueryModel();
const withoutNulls = r[1].filter(x => x.discipline !== null);
if (withoutNulls.length > 0) {
query.filter.discipline = { '$in': withoutNulls.map(r => r.discipline as string) };
}
query.filter.sport = { '$in': [r[0]] };
return Axios.post('/api/dashboard-survey', query);
});
const results = await Promise.all<unknown>(promises);
const values: SingleCard[] = [];
results.forEach(r => {
const data = (r as any).data.result as object[];
values.push(...data.map(val => ({ data: val, expanded: false, id: Helpers.generateGuid() })));
});
if (mode === 'replace') {
this.data = values;
}
else {
this.data.push(...values);
}
callback();
}
public loadDetails = async (orderId: string) => {
const result = await Axios.post('/api/team-data-details', { orderId: orderId });
const data = ((result as any).data as object);
return data;
}
public fetchSportsAndDisciplines = async () => {
if (this.sportsAndDisciplinesList !== null) {
return;
}
this.sportsAndDisciplinesDropdownIsLoading = true;
const values: SportAndDisciplineDropdownValue[] = [];
const result: any[] = await new ClientHttp('POST', '/api/sports-and-disciplines', {}).go();
if (result) {
result.sort((a: any, b: any) => a.sport.toLowerCase().localeCompare(b.sport.toLowerCase()))
.forEach((group: any) => {
const value = { sport: group.sport, discipline: null };
values.push({ ...value, id: JSON.stringify(value) });
group.disciplines.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase()))
.forEach((discipline: any) => {
const value = { sport: group.sport, discipline: discipline };
values.push({ ...value, id: JSON.stringify(value) });
});
});
this.sportsAndDisciplinesList = values;
}
this.sportsAndDisciplinesDropdownIsLoading = false;
}
public fetchPlaces = () => {
if (this.placesList === null) {
this.placesDropdownIsLoading = true;
return new ClientHttp('POST', '/api/places', {}).go().then((result: any) => {
const data = ((result as string[]).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).filter(r => !!r.length));
this.placesList = data.map(r => ({ value: r, id: r }));
this.placesDropdownIsLoading = false;
});
}
return Promise.resolve();
}
public rangePickerStore = new MonthRangePickerStore(this.startDate,
this.endDate, (start, end) => {
this.startDate = start;
this.endDate = end;
this.startSearchingWithNewFilterSet();
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment