uui/components/filters/FiltersPanelItem.tsx (248 lines of code) (raw):
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { uuiDayjs } from '../../helpers/dayJsHelper';
import cx from 'classnames';
import { Modifier } from 'react-popper';
import { DropdownBodyProps, TableFiltersConfig, IDropdownToggler, IEditable, isMobile, FilterPredicateName, getSeparatedValue, DataRowProps, PickerFilterConfig, useForceUpdate, IDataSource, DataSourceState } from '@epam/uui-core';
import { Dropdown } from '@epam/uui-components';
import { i18n } from '../../i18n';
import { FilterPanelItemToggler } from './FilterPanelItemToggler';
import { LinkButton } from '../buttons';
import { MultiSwitch } from '../inputs';
import { Text, TextPlaceholder } from '../typography';
import { FilterItemBody } from './FilterItemBody';
import { DropdownContainer } from '../overlays';
import { PickerBodyMobileView } from '../pickers';
import { UUI_FILTERS_PANEL_ITEM_BODY } from './constants';
import { ReactComponent as RemoveIcon } from '@epam/assets/icons/action-delete-outline.svg';
import css from './FiltersPanelItem.module.scss';
export type FiltersToolbarItemProps = TableFiltersConfig<any> &
IEditable<any> & {
autoFocus?: boolean;
removeFilter?: (field: any) => void;
size?: '24' | '30' | '36' | '42' | '48';
};
function useView(props: FiltersToolbarItemProps, value: any) {
const forceUpdate = useForceUpdate();
let useViewFn: IDataSource<any, any, any>['useView'];
const dataSourceState: DataSourceState = {};
if (props.type === 'singlePicker' || props.type === 'multiPicker') {
useViewFn = props.dataSource.useView.bind(props.dataSource);
if (props.type === 'singlePicker') {
dataSourceState.selectedId = value;
}
if (props.type === 'multiPicker') {
dataSourceState.checked = value;
}
}
return useViewFn?.(dataSourceState, forceUpdate, { showSelectedOnly: true });
}
function FiltersToolbarItemImpl(props: FiltersToolbarItemProps) {
const { maxCount = 2 } = props;
const isPickersType = props?.type === 'multiPicker' || props?.type === 'singlePicker';
const isMobileScreen = isMobile();
const popperModifiers: Modifier<any>[] = useMemo(() => {
const modifiers: Modifier<any>[] = [
{
name: 'offset',
options: { offset: isPickersType && isMobileScreen ? [0, 0] : [0, 6] },
},
];
if (isPickersType && isMobileScreen) {
modifiers.push({
name: 'resetTransform',
enabled: true,
phase: 'beforeWrite',
requires: ['computeStyles'],
fn: ({ state }) => {
state.styles.popper.transform = '';
},
});
}
return modifiers;
}, [isPickersType]);
const getDefaultPredicate = () => {
if (!props.predicates) {
return null;
}
return Object.keys(props.value || {})[0] || props.predicates.find((i) => i.isDefault)?.predicate || props.predicates?.[0].predicate;
};
const [isOpen, isOpenChange] = useState(props.autoFocus);
const [predicate, setPredicate] = useState(getDefaultPredicate());
const predicateName: string = React.useMemo(() => predicate && props.predicates.find((p) => p.predicate === predicate).name, [predicate]);
useEffect(() => {
if (props.predicates && Object.keys(props.value || {})[0] && Object.keys(props.value || {})[0] !== predicate) {
setPredicate(Object.keys(props.value || {})[0]);
}
}, [props.value]);
const onValueChange = useCallback(
(value: any) => {
if (props.predicates) {
props.onValueChange({ [props.field]: { [predicate]: value } });
} else {
props.onValueChange({ [props.field]: value });
}
},
[props.field, props.onValueChange],
);
const removeOnclickHandler = () => {
props.removeFilter(props.field);
};
const changePredicate = (val: FilterPredicateName) => {
const isInRange = (p: FilterPredicateName) => p === 'inRange' || p === 'notInRange';
if (props.type === 'numeric') {
let predicateValue = {
[props.field]: { [val]: getValue() },
};
if (isInRange(val) && !isInRange(predicate as FilterPredicateName)) {
// from simple predicate -> to Range
predicateValue = { [props.field]: { [val]: { from: null, to: null } } };
} else if (!isInRange(val) && isInRange(predicate as FilterPredicateName)) {
// from Range -> to simple predicate
predicateValue = { [props.field]: { [val]: null } };
}
props.onValueChange(predicateValue);
} else {
props.onValueChange({ [props.field]: { [val]: getValue() } });
}
setPredicate(val);
};
const renderHeader = (hideTitle: boolean) => (
<div className={ cx(css.header, isPickersType && (props.showSearch ?? css.withSearch)) }>
{props.predicates ? (
<MultiSwitch items={ props.predicates.map((i) => ({ id: i.predicate, caption: i.name })) } value={ predicate } onValueChange={ changePredicate } size="24" />
) : (
!hideTitle && (
<Text color="secondary" size="24" fontSize="14">
{props.title}
</Text>
)
)}
{!props?.isAlwaysVisible && (
<LinkButton cx={ css.removeButton } caption={ i18n.filterToolbar.datePicker.removeCaption } onClick={ removeOnclickHandler } size="24" icon={ RemoveIcon } />
)}
</div>
);
const renderBody = (dropdownProps: DropdownBodyProps) => {
const hideHeaderTitle = isPickersType && isMobileScreen;
return isPickersType ? (
<PickerBodyMobileView
{ ...dropdownProps }
cx={ UUI_FILTERS_PANEL_ITEM_BODY }
title={ props.title }
width={ 360 }
onClose={ () => isOpenChange(false) }
>
{ renderHeader(hideHeaderTitle) }
<FilterItemBody
{ ...props }
{ ...dropdownProps }
selectedPredicate={ predicate }
value={ getValue() }
onValueChange={ onValueChange }
/>
</PickerBodyMobileView>
) : (
<DropdownContainer cx={ UUI_FILTERS_PANEL_ITEM_BODY } { ...dropdownProps }>
{ renderHeader(hideHeaderTitle) }
<FilterItemBody
{ ...props }
{ ...dropdownProps }
selectedPredicate={ predicate }
value={ getValue() }
onValueChange={ onValueChange }
/>
</DropdownContainer>
);
};
const getValue = () => {
return predicate ? props.value?.[predicate] : props.value;
};
const getPickerItemName = (item: DataRowProps<any, any>, config: PickerFilterConfig<any>) => {
if (item.isLoading) {
return <TextPlaceholder />;
}
if (item.isUnknown) {
return 'Unknown';
}
return config.getName ? config.getName(item.value) : item.value.name;
};
const view = useView(props, getValue());
const getTogglerValue = () => {
const currentValue = getValue();
const defaultFormat = 'MMM DD, YYYY';
switch (props.type) {
case 'multiPicker': {
let isLoading = false;
const selection = currentValue
? currentValue?.slice(0, maxCount).map((i: any) => {
const item = view.getById(i, null);
isLoading = item?.isLoading;
return getPickerItemName(item, props);
})
: currentValue;
const postfix = (!isLoading && currentValue?.length > maxCount) ? ` +${(currentValue.length - maxCount).toString()} ${i18n.filterToolbar.pickerInput.itemsPlaceholder}` : null;
return { selection, postfix };
}
case 'numeric': {
const isRangePredicate = predicate === 'inRange' || predicate === 'notInRange';
const decimalFormat = (val: number) => getSeparatedValue(val, { maximumFractionDigits: 2 });
if ((isRangePredicate && !currentValue) || (!isRangePredicate && !currentValue && currentValue !== 0)) {
return { selection: undefined };
}
const selection = isRangePredicate
? `${!currentValue?.from && currentValue?.from !== 0 ? 'Min' : decimalFormat(currentValue?.from)} - ${
!currentValue?.to && currentValue?.to !== 0 ? 'Max' : decimalFormat(currentValue?.to)
}`
: `${!currentValue && currentValue !== 0 ? 'ALL' : decimalFormat(currentValue)}`;
return { selection: [selection] };
}
case 'singlePicker': {
if (currentValue === null || currentValue === undefined) {
return { selection: undefined };
}
const item = view.getById(currentValue, null);
const selection = getPickerItemName(item, props);
return { selection: [selection] };
}
case 'datePicker': {
return { selection: currentValue ? [uuiDayjs.dayjs(currentValue).format(props.format || defaultFormat)] : currentValue };
}
case 'rangeDatePicker': {
if (!currentValue || (!currentValue.from && !currentValue.to)) {
return { selection: undefined };
}
const currentValueFrom = currentValue?.from
? uuiDayjs.dayjs(currentValue?.from).format(props.format || defaultFormat)
: i18n.filterToolbar.rangeDatePicker.emptyPlaceholderFrom;
const currentValueTo = currentValue?.to
? uuiDayjs.dayjs(currentValue?.to).format(props.format || defaultFormat)
: i18n.filterToolbar.rangeDatePicker.emptyPlaceholderTo;
const selection = `${currentValueFrom} - ${currentValueTo}`;
return { selection: [selection] };
}
case 'custom': {
const value = props.getTogglerValue(props);
return { selection: value !== undefined ? [value] : undefined };
}
}
};
const getTogglerWidth = () => {
if (props.togglerWidth) return props.togglerWidth;
return props.type === 'datePicker' || props.type === 'rangeDatePicker' ? null : 300;
};
const renderTarget = (dropdownProps: IDropdownToggler) => (
<FilterPanelItemToggler
{ ...dropdownProps }
{ ...getTogglerValue() }
title={ props.title }
predicateName={ props.value ? predicateName : null }
maxWidth={ getTogglerWidth() }
size={ props.size }
/>
);
return (
<Dropdown
renderTarget={ renderTarget }
renderBody={ renderBody }
closeBodyOnTogglerHidden={ !isMobile() }
value={ isOpen }
onValueChange={ isOpenChange }
modifiers={ popperModifiers }
/>
);
}
export const FiltersPanelItem = React.memo(FiltersToolbarItemImpl);