feat: WIP advanced filter.

This commit is contained in:
a.bouhuolia
2021-08-10 19:38:36 +02:00
parent aefb89e1c0
commit 23e8e251a1
97 changed files with 2008 additions and 1937 deletions

View File

@@ -0,0 +1,13 @@
import * as Yup from 'yup';
export const getFilterDropdownSchema = () =>
Yup.object().shape({
conditions: Yup.array().of(
Yup.object().shape({
fieldKey: Yup.string(),
value: Yup.string().nullable(),
condition: Yup.string().nullable(),
comparator: Yup.string().nullable(),
}),
),
});

View File

@@ -1,13 +0,0 @@
import * as Yup from 'yup';
import { DATATYPES_LENGTH } from 'common/dataTypes';
export const getFilterDropdownSchema = () => Yup.object().shape({
conditions: Yup.array().of(
Yup.object().shape({
fieldKey: Yup.string().max(DATATYPES_LENGTH.TEXT),
value: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
condition: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
comparator: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
}),
),
});

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Classes } from '@blueprintjs/core';
import ListSelect from '../ListSelect';
import { getConditionTypeCompatators } from './utils';
export default function DynamicFilterCompatatorField({
dataType,
...restProps
}) {
const options = getConditionTypeCompatators(dataType);
return (
<ListSelect
textProp={'label'}
selectedItemProp={'value'}
items={options}
className={Classes.FILL}
filterable={false}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
{...restProps}
/>
);
}

View File

@@ -1,24 +0,0 @@
import React, { useMemo } from 'react';
import { HTMLSelect, Classes } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { getConditionTypeCompatators } from './utils';
export default function DynamicFilterCompatatorField({
dataType,
...restProps
}) {
const options = useMemo(
() => getConditionTypeCompatators(dataType).map(comp => ({
value: comp.value, label: intl.get(comp.label),
})),
[dataType]
);
return (
<HTMLSelect
options={options}
className={Classes.FILL}
{...{ ...restProps }}
/>
);
}

View File

@@ -0,0 +1,397 @@
import React from 'react';
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
import {
Button,
FormGroup,
Classes,
InputGroup,
MenuItem,
} from '@blueprintjs/core';
import { get, first, defaultTo, isEqual, isEmpty } from 'lodash';
import intl from 'react-intl-universal';
import { Choose, Icon, FormattedMessage as T, ListSelect } from 'components';
import { useUpdateEffect } from 'hooks';
import {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useFilterCondition,
useAdvancedFilterContext,
} from './AdvancedFilterDropdownContext';
import AdvancedFilterCompatatorField from './AdvancedFilterCompatatorField';
import AdvancedFilterValueField from './AdvancedFilterValueField';
import {
filterConditionRoles,
getConditionalsOptions,
transformFieldsToOptions,
shouldFilterValueFieldUpdate,
getConditionTypeCompatators,
} from './utils';
import { getFilterDropdownSchema } from './AdvancedFilter.schema';
import { useAdvancedFilterAutoSubmit } from './components';
/**
* Condition item list renderer.
*/
function ConditionItemRenderer(condition, { handleClick, modifiers, query }) {
return (
<MenuItem
text={
<>
<div>{condition.label}</div>
<span className="text-hint">{condition.text}</span>
</>
}
key={condition.value}
onClick={handleClick}
/>
);
}
/**
* Filter condition field.
*/
function FilterConditionField() {
const conditionalsOptions = getConditionalsOptions();
const { conditionIndex, getConditionFieldPath } = useFilterCondition();
const conditionFieldPath = getConditionFieldPath('condition');
return (
<FastField name={conditionFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--condition'}>
<Choose>
<Choose.When condition={conditionIndex === 0}>
<InputGroup disabled value={intl.get('filter.when')} />
</Choose.When>
<Choose.Otherwise>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
labelProp={'text'}
items={conditionalsOptions}
className={Classes.FILL}
filterable={false}
onItemSelect={(option) => {
form.setFieldValue(conditionFieldPath, option.value);
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
itemRenderer={ConditionItemRenderer}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
)}
</FastField>
);
}
/**
* Compatator field.
*/
function FilterCompatatorFilter() {
const { getConditionFieldPath, fieldMeta } = useFilterCondition();
const comparatorFieldPath = getConditionFieldPath('comparator');
const fieldType = get(fieldMeta, 'fieldType');
return (
<FastField name={comparatorFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--comparator'}>
<AdvancedFilterCompatatorField
dataType={fieldType}
className={Classes.FILL}
selectedItem={field.value}
onItemSelect={(option) => {
form.setFieldValue(comparatorFieldPath, option.value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Changes default value of comparator field in the condition row once the
* field option changing.
*/
function useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
}) {
const fieldKeyValue = getConditionValue('fieldKey');
const comparatorsOptions = React.useMemo(
() => getConditionTypeCompatators(fieldMeta.fieldType),
[fieldMeta.fieldType],
);
useUpdateEffect(() => {
if (fieldKeyValue) {
const defaultValue = get(first(comparatorsOptions), 'value');
setConditionValue('comparator', defaultValue);
}
}, [fieldKeyValue, setConditionValue, comparatorsOptions]);
}
/**
* Resource fields field.
*/
function FilterFieldsField() {
const {
getConditionFieldPath,
getConditionValue,
setConditionValue,
fieldMeta,
} = useFilterCondition();
const { fields } = useAdvancedFilterContext();
const fieldPath = getConditionFieldPath('fieldKey');
const valueFieldPath = getConditionFieldPath('value');
useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
});
return (
<FastField name={fieldPath}>
{({ field, form }) => (
<FormGroup className={'form-group--fieldKey'}>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
items={transformFieldsToOptions(fields)}
className={Classes.FILL}
onItemSelect={(option) => {
form.setFieldValue(fieldPath, option.value);
// Resets the value field to empty once the field option changing.
form.setFieldValue(valueFieldPath, '');
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter value field.
*/
function FilterValueField() {
const { conditionIndex, fieldMeta, getConditionFieldPath } =
useFilterCondition();
// Can't continue if the given field key is not selected yet.
if (!fieldMeta) {
return null;
}
// Field meta type, name and options.
const fieldType = get(fieldMeta, 'fieldType');
const fieldName = get(fieldMeta, 'name');
const options = get(fieldMeta, 'options');
const valueFieldPath = getConditionFieldPath('value');
return (
<FastField
name={valueFieldPath}
fieldKey={fieldType} // Pass to shouldUpdate function.
shouldUpdate={shouldFilterValueFieldUpdate}
>
{({ form: { setFieldValue }, field }) => (
<FormGroup className={'form-group--value'}>
<AdvancedFilterValueField
isFocus={conditionIndex === 0}
value={field.value}
key={'name'}
label={fieldName}
fieldType={fieldType}
options={options}
onChange={(value) => {
setFieldValue(valueFieldPath, value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter condition line.
*/
function AdvancedFilterDropdownCondition({ conditionIndex, onRemoveClick }) {
// Handle click remove condition.
const handleClickRemoveCondition = () => {
onRemoveClick && onRemoveClick(conditionIndex);
};
return (
<div className="filter-dropdown__condition">
<FilterConditionProvider conditionIndex={conditionIndex}>
<FilterConditionField />
<FilterFieldsField />
<FilterCompatatorFilter />
<FilterValueField />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={handleClickRemoveCondition}
className={'button--remove'}
/>
</FilterConditionProvider>
</div>
);
}
/**
* Advanced filter dropdown condition.
*/
function AdvancedFilterDropdownConditions({ push, remove, replace, form }) {
const { initialCondition } = useAdvancedFilterContext();
// Handle remove condition.
const handleClickRemoveCondition = (conditionIndex) => {
if (form.values.conditions.length > 1) {
remove(conditionIndex);
} else {
replace(0, { ...initialCondition });
}
};
// Handle new condition button click.
const handleNewConditionBtnClick = (index) => {
push({ ...initialCondition });
};
return (
<div className="filter-dropdonw__conditions-wrap">
<div className={'filter-dropdown__conditions'}>
{form.values.conditions.map((condition, index) => (
<AdvancedFilterDropdownCondition
conditionIndex={index}
onRemoveClick={handleClickRemoveCondition}
/>
))}
</div>
<AdvancedFilterDropdownFooter onClick={handleNewConditionBtnClick} />
</div>
);
}
/**
* Advanced filter dropdown form.
*/
function AdvancedFilterDropdownForm() {
// Advanced filter auto-save.
useAdvancedFilterAutoSubmit();
return (
<div className="filter-dropdown__form">
<FieldArray
name={'conditions'}
render={({ ...fieldArrayProps }) => (
<AdvancedFilterDropdownConditions {...fieldArrayProps} />
)}
/>
</div>
);
}
/**
* Advanced filter dropdown footer.
*/
function AdvancedFilterDropdownFooter({ onClick }) {
// Handle new filter condition button click.
const onClickNewFilter = (event) => {
onClick && onClick(event);
};
return (
<div className="filter-dropdown__footer">
<Button minimal={true} onClick={onClickNewFilter}>
<T id={'new_conditional'} />
</Button>
</div>
);
}
/**
* Advanced filter dropdown.
*/
export function AdvancedFilterDropdown({
fields,
conditions,
defaultFieldKey,
defaultComparator,
defaultValue,
defaultCondition,
onFilterChange,
}) {
// Initial condition.
const initialCondition = {
fieldKey: defaultFieldKey,
comparator: defaultTo(defaultComparator, 'contain'),
condition: defaultTo(defaultCondition, 'or'),
value: defaultTo(defaultValue, ''),
};
// Initial conditions.
const initialConditions = !isEmpty(conditions)
? conditions
: [initialCondition, initialCondition];
const [prevConditions, setPrevConditions] = React.useState(initialConditions);
// Handle the filter dropdown form submit.
const handleFitlerDropdownSubmit = (values) => {
const conditions = filterConditionRoles(values.conditions);
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, conditions)) {
onFilterChange && onFilterChange(conditions);
setPrevConditions(conditions);
}
};
// Filter dropdown validation schema.
const validationSchema = getFilterDropdownSchema();
// Initial values.
const initialValues = {
conditions: initialConditions,
};
return (
<div className="filter-dropdown">
<AdvancedFilterDropdownProvider
initialCondition={initialCondition}
fields={fields}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
component={AdvancedFilterDropdownForm}
onSubmit={handleFitlerDropdownSubmit}
/>
</AdvancedFilterDropdownProvider>
</div>
);
}

View File

@@ -1,306 +0,0 @@
import React from 'react';
import { Formik, Field, FieldArray, useFormikContext } from 'formik';
import {
Button,
Intent,
FormGroup,
Classes,
HTMLSelect,
} from '@blueprintjs/core';
import { get, defaultTo, isEqual } from 'lodash';
import { Icon, FormattedMessage as T } from 'components';
import {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useFilterCondition,
useAdvancedFilterContext,
} from './AdvancedFilterDropdownContext';
import AdvancedFilterCompatatorField from './AdvancedFilterCompatatorField';
import AdvancedFilterValueField from './AdvancedFilterValueField2';
import {
filterConditionRoles,
getConditionalsOptions,
transformFieldsToOptions,
} from './utils';
import { getFilterDropdownSchema } from './AdvancedFilter.schema';
import {
IAdvancedFilterDropdown,
IAdvancedFilterDropdownFooter,
IFilterDropdownFormikValues,
IAdvancedFilterDropdownConditionsProps,
IAdvancedFilterDropdownCondition,
IFilterRole,
} from './interfaces';
import { useAdvancedFilterAutoSubmit } from './components';
/**
* Filter condition field.
*/
function FilterConditionField() {
const conditionalsOptions = getConditionalsOptions();
const { conditionIndex } = useFilterCondition();
return (
<Field name={`conditions[${conditionIndex}].condition`}>
{({ field }) => (
<FormGroup className={'form-group--condition'}>
<HTMLSelect
options={conditionalsOptions}
className={Classes.FILL}
{...field}
/>
</FormGroup>
)}
</Field>
);
}
/**
* Compatator field.
*/
function FilterCompatatorFilter() {
const { conditionIndex } = useFilterCondition();
return (
<Field name={`conditions[${conditionIndex}].comparator`}>
{({ field }) => (
<FormGroup className={'form-group--comparator'}>
<AdvancedFilterCompatatorField
className={Classes.FILL}
dataType={'text'}
{...field}
/>
</FormGroup>
)}
</Field>
);
}
/**
* Resource fields field.
*/
function FilterFieldsField() {
const { conditionIndex } = useFilterCondition();
const { fields } = useAdvancedFilterContext();
return (
<Field name={`conditions[${conditionIndex}].fieldKey`}>
{({ field }) => (
<FormGroup className={'form-group--fieldKey'}>
<HTMLSelect
options={transformFieldsToOptions(fields)}
value={1}
className={Classes.FILL}
{...field}
/>
</FormGroup>
)}
</Field>
);
}
/**
* Advanced filter value field.
*/
function FilterValueField(): JSX.Element | null {
const { values } = useFormikContext();
const { conditionIndex } = useFilterCondition();
const { fieldsByKey } = useAdvancedFilterContext();
// Current condition field key.
const conditionFieldKey = get(
values.conditions,
`[${conditionIndex}].fieldKey`,
);
// Field meta.
const fieldMeta = fieldsByKey[conditionFieldKey];
// Can't continue if the given field key is not selected yet.
if (!conditionFieldKey || !fieldMeta) {
return null;
}
// Field meta type, name and options.
const fieldType = get(fieldMeta, 'fieldType');
const fieldName = get(fieldMeta, 'name');
const options = get(fieldMeta, 'options');
const valueFieldPath = `conditions[${conditionIndex}].value`;
return (
<Field name={valueFieldPath}>
{({ form: { setFieldValue } }) => (
<FormGroup className={'form-group--value'}>
<AdvancedFilterValueField
key={'name'}
label={fieldName}
fieldType={fieldType}
options={options}
onChange={(value) => {
setFieldValue(valueFieldPath, value);
}}
/>
</FormGroup>
)}
</Field>
);
}
/**
* Advanced filter condition line.
*/
function AdvancedFilterDropdownCondition({
conditionIndex,
onRemoveClick,
}: IAdvancedFilterDropdownCondition) {
// Handle click remove condition.
const handleClickRemoveCondition = () => {
onRemoveClick && onRemoveClick(conditionIndex);
};
return (
<div className="filter-dropdown__condition">
<FilterConditionProvider conditionIndex={conditionIndex}>
<FilterConditionField />
<FilterCompatatorFilter />
<FilterFieldsField />
<FilterValueField />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={handleClickRemoveCondition}
/>
</FilterConditionProvider>
</div>
);
}
/**
* Advanced filter dropdown condition.
*/
function AdvancedFilterDropdownConditions({
push,
remove,
form,
}: IAdvancedFilterDropdownConditionsProps) {
const { initialCondition } = useAdvancedFilterContext();
// Handle remove condition.
const handleClickRemoveCondition = (conditionIndex: number) => {
remove(conditionIndex);
};
// Handle new condition button click.
const handleNewConditionBtnClick = (index: number) => {
push({ ...initialCondition });
};
return (
<div className="filter-dropdonw__conditions-wrap">
<div className={'filter-dropdown__conditions'}>
{form.values.conditions.map((condition: IFilterRole, index: number) => (
<AdvancedFilterDropdownCondition
conditionIndex={index}
onRemoveClick={handleClickRemoveCondition}
/>
))}
</div>
<AdvancedFilterDropdownFooter onClick={handleNewConditionBtnClick} />
</div>
);
}
/**
* Advanced filter dropdown form.
*/
function AdvancedFilterDropdownForm() {
//
useAdvancedFilterAutoSubmit();
return (
<div className="filter-dropdown__form">
<FieldArray
name={'conditions'}
render={({ ...fieldArrayProps }) => (
<AdvancedFilterDropdownConditions {...fieldArrayProps} />
)}
/>
</div>
);
}
/**
* Advanced filter dropdown footer.
*/
function AdvancedFilterDropdownFooter({
onClick,
}: IAdvancedFilterDropdownFooter) {
// Handle new filter condition button click.
const onClickNewFilter = (event) => {
onClick && onClick(event);
};
return (
<div className="filter-dropdown__footer">
<Button minimal={true} intent={Intent.PRIMARY} onClick={onClickNewFilter}>
<T id={'new_conditional'} />
</Button>
</div>
);
}
/**
* Advanced filter dropdown.
*/
export default function AdvancedFilterDropdown({
fields,
defaultFieldKey,
defaultComparator,
defaultValue,
onFilterChange,
}: IAdvancedFilterDropdown) {
const [prevConditions, setPrevConditions] = React.useState({});
// Handle the filter dropdown form submit.
const handleFitlerDropdownSubmit = (values: IFilterDropdownFormikValues) => {
const conditions = filterConditionRoles(values.conditions);
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, conditions) && conditions.length > 0) {
onFilterChange && onFilterChange(conditions);
setPrevConditions(conditions);
}
};
// Filter dropdown validation schema.
const validationSchema = getFilterDropdownSchema();
// Initial condition.
const initialCondition = {
fieldKey: defaultFieldKey,
comparator: defaultTo(defaultComparator, 'equals'),
condition: '',
value: defaultTo(defaultValue, ''),
};
// Initial values.
const initialValues: IFilterDropdownFormikValues = {
conditions: [initialCondition],
};
return (
<div className="filter-dropdown">
<AdvancedFilterDropdownProvider
initialCondition={initialCondition}
fields={fields}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
component={AdvancedFilterDropdownForm}
onSubmit={handleFitlerDropdownSubmit}
/>
</AdvancedFilterDropdownProvider>
</div>
);
}

View File

@@ -0,0 +1,84 @@
import React, { createContext, useContext } from 'react';
import { get, keyBy } from 'lodash';
import { useFormikContext } from 'formik';
const AdvancedFilterContext = createContext({});
const FilterConditionContext = createContext({});
/**
* Advanced filter dropdown context provider.
*/
function AdvancedFilterDropdownProvider({
initialCondition,
fields,
...props
}) {
const fieldsByKey = keyBy(fields, 'key');
// Retrieve field meta by the given field key.
const getFieldMetaByKey = React.useCallback(
(key) => get(fieldsByKey, key),
[fieldsByKey],
);
// Provider payload.
const provider = { initialCondition, fields, fieldsByKey, getFieldMetaByKey };
return <AdvancedFilterContext.Provider value={provider} {...props} />;
}
/**
* Filter condition row context provider.
*/
function FilterConditionProvider({ conditionIndex, ...props }) {
const { setFieldValue, values } = useFormikContext();
const { getFieldMetaByKey } = useAdvancedFilterContext();
// Condition value path.
const conditionPath = `conditions[${conditionIndex}]`;
// Sets conditions value.
const setConditionValue = React.useCallback(
(field, value) => {
return setFieldValue(`${conditionPath}.${field}`, value);
},
[conditionPath, setFieldValue],
);
// Retrieve condition field value.
const getConditionValue = React.useCallback(
(field) => get(values, `${conditionPath}.${field}`),
[conditionPath, values],
);
// The current condition field meta.
const fieldMeta = React.useMemo(
() => getFieldMetaByKey(getConditionValue('fieldKey')),
[getFieldMetaByKey, getConditionValue],
);
// Retrieve the condition field path.
const getConditionFieldPath = React.useCallback(
(field) => `${conditionPath}.${field}`,
[conditionPath],
);
// Provider payload.
const provider = {
fieldMeta,
conditionIndex,
getConditionValue,
getConditionFieldPath,
setConditionValue,
};
return <FilterConditionContext.Provider value={provider} {...props} />;
}
const useFilterCondition = () => useContext(FilterConditionContext);
const useAdvancedFilterContext = () => useContext(AdvancedFilterContext);
export {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useAdvancedFilterContext,
useFilterCondition,
};

View File

@@ -1,47 +0,0 @@
import React, { createContext, useContext } from 'react';
import { keyBy } from 'lodash';
import {
IAdvancedFilterContextProps,
IFilterConditionContextProps,
IAdvancedFilterProviderProps,
IFilterConditionProviderProps,
} from './interfaces';
const AdvancedFilterContext = createContext<IAdvancedFilterContextProps>({});
const FilterConditionContext = createContext<IFilterConditionContextProps>({});
/**
* Advanced filter dropdown context provider.
*/
function AdvancedFilterDropdownProvider({
initialCondition,
fields,
...props
}: IAdvancedFilterProviderProps) {
const fieldsByKey = keyBy(fields, 'key');
// Provider payload.
const provider = { initialCondition, fields, fieldsByKey };
return <AdvancedFilterContext.Provider value={provider} {...props} />;
}
/**
* Filter condition row context provider.
*/
function FilterConditionProvider({
conditionIndex,
...props
}: IFilterConditionProviderProps) {
// Provider payload.
const provider = { conditionIndex };
return <FilterConditionContext.Provider value={provider} {...props} />;
}
const useFilterCondition = () => useContext(FilterConditionContext);
const useAdvancedFilterContext = () => useContext(AdvancedFilterContext);
export {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useAdvancedFilterContext,
useFilterCondition,
};

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Popover, PopoverInteractionKind, Position } from '@blueprintjs/core';
import { AdvancedFilterDropdown } from './AdvancedFilterDropdown';
/**
* Advanced filter popover.
*/
export function AdvancedFilterPopover({
popoverProps,
advancedFilterProps,
children,
}) {
return (
<Popover
minimal={true}
content={
<AdvancedFilterDropdown
{...advancedFilterProps}
/>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
modifiers={{
offset: { offset: '0, 4' },
}}
{...popoverProps}
>
{children}
</Popover>
);
}

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { Position, Checkbox, InputGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import moment from 'moment';
import intl from 'react-intl-universal';
import { isUndefined } from 'lodash';
import { useAutofocus } from 'hooks';
import { Choose, ListSelect } from 'components';
import { momentFormatter } from 'utils';
function AdvancedFilterEnumerationField({ options, value, ...rest }) {
return (
<ListSelect
items={options}
selectedItem={value}
popoverProps={{
fill: true,
inline: true,
minimal: true,
captureDismiss: true,
}}
defaultText={`Select an option`}
textProp={'label'}
selectedItemProp={'key'}
{...rest}
/>
);
}
const IFieldType = {
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
NUMBER: 'number',
DATE: 'date',
}
function tansformDateValue(date, defaultValue = null) {
return date ? moment(date).toDate() : defaultValue;
}
/**
* Advanced filter value field detarminer.
*/
export default function AdvancedFilterValueField2({
value,
fieldType,
options,
onChange,
isFocus
}) {
const [localValue, setLocalValue] = React.useState(value);
React.useEffect(() => {
if (localValue !== value && !isUndefined(value)) {
setLocalValue(value)
}
}, [localValue, value]);
// Input field reference.
const valueRef = useAutofocus(isFocus);
const triggerOnChange = (value) => onChange && onChange(value);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
triggerOnChange(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
triggerOnChange(e.currentTarget.value);
}
};
// Handle enumeration field type change.
const handleEnumerationChange = (option) => {
setLocalValue(option.key);
triggerOnChange(option.key);
};
// Handle date field change.
const handleDateChange = (date) => {
const formattedDate = moment(date).format('YYYY/MM/DD');
setLocalValue(formattedDate);
triggerOnChange(formattedDate);
};
return (
<Choose>
<Choose.When condition={fieldType === IFieldType.ENUMERATION}>
<AdvancedFilterEnumerationField
options={options}
value={localValue}
onItemSelect={handleEnumerationChange}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.DATE}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={'Enter date'}
fill={true}
inputProps={{
fill: true
}}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.BOOLEAN}>
<Checkbox value={localValue} onChange={handleInputChange} />
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={intl.get('value')}
onChange={handleInputChange}
value={localValue}
inputRef={valueRef}
/>
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -1,209 +0,0 @@
import React, { useMemo, useRef, useEffect, useState } from 'react';
import {
FormGroup,
MenuItem,
InputGroup,
Position,
Checkbox,
} from '@blueprintjs/core';
import { connect } from 'react-redux';
import { useQuery } from 'react-query';
import { DateInput } from '@blueprintjs/datetime';
import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { debounce } from 'lodash';
import moment from 'moment';
import { Choose, ListSelect, MODIFIER } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withResourceActions from 'containers/Resources/withResourcesActions';
import { compose, momentFormatter } from 'utils';
/**
* Dynamic filter fields.
*/
function DynamicFilterValueField({
// #withResourceDetail
resourceName,
resourceData = [],
requestResourceData,
// #ownProps
fieldType,
fieldName,
value,
initialValue,
error,
optionsResource,
optionsKey = 'key',
optionsLabel = 'label',
options,
onChange,
inputDebounceWait = 250,
}) {
const [localValue, setLocalValue] = useState();
// Makes `localValue` controlled mode from `value`.
useEffect(() => {
if (value !== localValue) {
setLocalValue(value);
}
}, [value]);
// Fetches resource data.
const fetchResourceData = useQuery(
['resource-data', resourceName],
(key, _resourceName) => requestResourceData(_resourceName),
{
enabled: resourceName,
},
);
// Account type item of select filed.
const menuItem = (item, { handleClick, modifiers, query }) => {
return (<MenuItem
text={item[optionsLabel]}
key={item[optionsKey]}
onClick={handleClick}
/>);
};
// Handle list button click.
const handleBtnClick = () => {
};
const listOptions = useMemo(() => [
...(resourceData || []),
...(options || []),
], [
resourceData, options,
]);
// Filters accounts types items.
const filterItems = (query, item, _index, exactMatch) => {
const normalizedTitle = item.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Handle list item selected.
const onItemSelect = (item) => {
onChange && onChange(item[optionsKey]);
};
const handleInputChangeThrottled = useRef(
debounce((value) => { onChange && onChange(value); }, inputDebounceWait),
);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
}
handleInputChangeThrottled.current(e.currentTarget.value);
};
// Handle checkbox field change.
const handleCheckboxChange = (e) => {
const value = !!e.currentTarget.checked;
setLocalValue(value);
onChange && onChange(value);
}
// Handle date field change.
const handleDateChange = (date) => {
setLocalValue(date);
onChange && onChange(date);
};
const transformDateValue = (value) => {
return value ? moment(value || new Date()).toDate() : null;
};
return (
<FormGroup className={'form-group--value'}>
<Choose>
<Choose.When condition={fieldType === 'options'}>
<ListSelect
className={classNames(
'list-select--filter-dropdown',
'form-group--select-list',
MODIFIER.SELECT_LIST_FILL_POPOVER,
MODIFIER.SELECT_LIST_FILL_BUTTON,
)}
items={listOptions}
itemRenderer={menuItem}
loading={fetchResourceData.isFetching}
itemPredicate={filterItems}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
popoverClassName: 'popover--list-select-filter-dropdown',
}}
onItemSelect={onItemSelect}
selectedItem={value}
selectedItemProp={optionsKey}
defaultText={`Select an option`}
textProp={optionsLabel}
buttonProps={{
onClick: handleBtnClick
}}
/>
</Choose.When>
<Choose.When condition={fieldType === 'date'}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={transformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={'Select date'}
/>
</Choose.When>
<Choose.When condition={fieldType === 'checkbox'}>
<Checkbox value={localValue} onChange={handleCheckboxChange} />
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={intl.get('value')}
onChange={handleInputChange}
value={localValue}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
);
}
const mapStateToProps = (state, props) => ({
resourceName: props.optionsResource,
});
const withResourceFilterValueField = connect(mapStateToProps);
export default compose(
withResourceFilterValueField,
withResourceDetail(({ resourceData }) => ({ resourceData })),
withResourceActions,
)(DynamicFilterValueField);

View File

@@ -1,104 +0,0 @@
import React from 'react';
import { Position, Checkbox, InputGroup, FormGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import moment from 'moment';
import intl from 'react-intl-universal';
import { Choose, ListSelect } from 'components';
import { momentFormatter, tansformDateValue } from 'utils';
import { IFieldType, IAdvancedFilterValueField } from './interfaces';
function AdvancedFilterEnumerationField({ options, value, ...rest }) {
return (
<ListSelect
items={options}
selectedItem={value}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
defaultText={`Select an option`}
textProp={'label'}
selectedItemProp={'key'}
{...rest}
/>
);
}
/**
* Advanced filter value field detarminer.
*/
export default function AdvancedFilterValueField2({
fieldType,
options,
onChange,
}: IAdvancedFilterValueField) {
const [localValue, setLocalValue] = React.useState<string>('');
const triggerOnChange = (value: string) => onChange && onChange(value);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
triggerOnChange(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
triggerOnChange(e.currentTarget.value);
}
};
// Handle enumeration field type change.
const handleEnumerationChange = (option) => {
setLocalValue(option.key);
triggerOnChange(option.key);
};
// Handle date field change.
const handleDateChange = (date: Date) => {
const formattedDate: string = moment(date).format('YYYY/MM/DD');
setLocalValue(formattedDate);
triggerOnChange(formattedDate);
};
return (
<FormGroup className={'form-group--value'}>
<Choose>
<Choose.When condition={fieldType === IFieldType.ENUMERATION}>
<AdvancedFilterEnumerationField
options={options}
value={localValue}
onItemSelect={handleEnumerationChange}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.DATE}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={'Select date'}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.BOOLEAN}>
<Checkbox value={localValue} onChange={handleInputChange} />
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={intl.get('value')}
onChange={handleInputChange}
value={localValue}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
);
}

View File

@@ -9,7 +9,7 @@ const DEBOUNCE_MS = 100;
*/
export function useAdvancedFilterAutoSubmit() {
const { submitForm, values } = useFormikContext();
const [isSubmit, setIsSubmit] = React.useState<boolean>(false);
const [isSubmit, setIsSubmit] = React.useState(false);
const debouncedSubmit = React.useCallback(
debounce(() => {

View File

@@ -1,4 +1,5 @@
import { ArrayHelpers } from 'formik';
import { IPopoverProps } from '@blueprintjs/core';
export type IResourceFieldType = 'text' | 'number' | 'enumeration' | 'boolean';
@@ -10,9 +11,12 @@ export interface IResourceField {
export interface IAdvancedFilterDropdown {
fields: IResourceField[];
conditions?: IFilterRole[];
defaultFieldKey: string;
defaultComparator?: string;
defaultValue?: string;
defaultCondition?: string;
onFilterChange?: (filterRoles: IFilterRole[]) => void;
}
export interface IAdvancedFilterDropdownFooter {
@@ -69,6 +73,7 @@ export interface IFilterOption {
export interface IAdvancedFilterValueField {
fieldType: string;
value?: string;
key: string;
label: string;
options?: IFilterOption[];
@@ -82,3 +87,25 @@ export enum IFieldType {
ENUMERATION = 'enumeration',
BOOLEAN = 'boolean',
}
export interface IConditionTypeOption {
value: string;
label: string;
}
export interface IConditionOption {
label: string;
value: string;
text?: string;
}
export interface IAdvancedFilterPopover {
popoverProps?: IPopoverProps;
advancedFilterProps: IAdvancedFilterDropdown;
children: JSX.Element | JSX.Element[];
}
export interface IDynamicFilterCompatatorFieldProps {
dataType: string;
}

View File

@@ -0,0 +1,110 @@
import intl from 'react-intl-universal';
import {
defaultFastFieldShouldUpdate,
uniqueMultiProps,
checkRequiredProperties,
} from 'utils';
// Conditions options.
export const getConditionalsOptions = () => [
{
value: 'and',
label: intl.get('and'),
text: intl.get('filter.all_filters_must_match'),
},
{
value: 'or',
label: intl.get('or'),
text: intl.get('filter.atleast_one_filter_must_match'),
},
];
export const getBooleanCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getTextCompatators = () => [
{ value: 'contain', label: intl.get('contain') },
{ value: 'not_contain', label: intl.get('not_contain') },
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equals') },
];
export const getDateCompatators = () => [
{ value: 'in', label: intl.get('in') },
{ value: 'after', label: intl.get('after') },
{ value: 'before', label: intl.get('before') },
];
export const getOptionsCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getNumberCampatators = () => [
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equal') },
{ value: 'bigger_than', label: intl.get('bigger_than') },
{ value: 'bigger_or_equal', label: intl.get('bigger_or_equals') },
{ value: 'smaller_than', label: intl.get('smaller_than') },
{ value: 'smaller_or_equal', label: intl.get('smaller_or_equals') },
];
export const getConditionTypeCompatators = (
dataType,
) => {
return [
...(dataType === 'enumeration'
? [...getOptionsCompatators()]
: dataType === 'date'
? [...getDateCompatators()]
: dataType === 'boolean'
? [...getBooleanCompatators()]
: dataType === 'number'
? [...getNumberCampatators()]
: [...getTextCompatators()]),
];
};
export const getConditionDefaultCompatator = (
dataType,
) => {
const compatators = getConditionTypeCompatators(dataType);
return compatators[0];
};
export const transformFieldsToOptions = (fields) =>
fields.map((field) => ({
value: field.key,
label: field.name,
}));
/**
* Filtered conditions that don't contain atleast on required fields or
* fileds keys that not exists.
* @param {IFilterRole[]} conditions
* @returns
*/
export const filterConditionRoles = (
conditions,
) => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const filteredConditions = conditions.filter(
(condition) =>
!checkRequiredProperties(condition, requiredProps),
);
return uniqueMultiProps(filteredConditions, requiredProps);
};
/**
* Detarmines the value field when should update.
* @returns {boolean}
*/
export const shouldFilterValueFieldUpdate = (newProps, oldProps) => {
return (
newProps.fieldKey !== oldProps.fieldKey ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};

View File

@@ -1,98 +0,0 @@
import intl from 'react-intl-universal';
import { IResourceField, IFilterRole } from './interfaces';
import { uniqueMultiProps, checkRequiredProperties } from 'utils';
interface IConditionOption {
label: string;
value: string;
}
// Conditions options.
export const getConditionalsOptions = (): IConditionOption[] => [
{ value: 'and', label: intl.get('and') },
{ value: 'or', label: intl.get('or') },
];
interface IConditionTypeOption {
value: string;
label: string;
}
export const getBooleanCompatators = (): IConditionTypeOption[] => [
{ value: 'is', label: 'is' },
{ value: 'is_not', label: 'is_not' },
];
export const getTextCompatators = (): IConditionTypeOption[] => [
{ value: 'contain', label: 'contain' },
{ value: 'not_contain', label: 'not_contain' },
{ value: 'equals', label: 'equals' },
{ value: 'not_equal', label: 'not_equals' },
];
export const getDateCompatators = (): IConditionTypeOption[] => [
{ value: 'in', label: 'in' },
{ value: 'after', label: 'after' },
{ value: 'before', label: 'before' },
];
export const getOptionsCompatators = (): IConditionTypeOption[] => [
{ value: 'is', label: 'is' },
{ value: 'is_not', label: 'is_not' },
];
export const getNumberCampatators = (): IConditionTypeOption[] => [
{ value: 'equals', label: 'equals' },
{ value: 'not_equal', label: 'not_equal' },
{ value: 'bigger_than', label: 'bigger_than' },
{ value: 'bigger_or_equals', label: 'bigger_or_equals' },
{ value: 'smaller_than', label: 'smaller_than' },
{ value: 'smaller_or_equals', label: 'smaller_or_equals' },
];
export const getConditionTypeCompatators = (
dataType: string,
): IConditionTypeOption[] => {
return [
...(dataType === 'options'
? [...getOptionsCompatators()]
: dataType === 'date'
? [...getDateCompatators()]
: dataType === 'boolean'
? [...getBooleanCompatators()]
: dataType === 'number'
? [...getNumberCampatators()]
: [...getTextCompatators()]),
];
};
export const getConditionDefaultCompatator = (
dataType: string,
): IConditionTypeOption => {
const compatators = getConditionTypeCompatators(dataType);
return compatators[0];
};
export const transformFieldsToOptions = (fields: IResourceField[]) =>
fields.map((field) => ({
value: field.key,
label: field.name,
}));
/**
* Filtered conditions that don't contain atleast on required fields or
* fileds keys that not exists.
* @param {IFilterRole[]} conditions
* @returns
*/
export const filterConditionRoles = (
conditions: IFilterRole[],
): IFilterRole[] => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const filteredConditions = conditions.filter(
(condition: IFilterRole) =>
!checkRequiredProperties(condition, requiredProps),
);
return uniqueMultiProps(filteredConditions, requiredProps);
};

View File

@@ -0,0 +1,26 @@
import React from 'react';
import classNames from 'classnames';
import intl from "react-intl-universal";
import { Classes, Button } from '@blueprintjs/core';
import { T, Icon } from 'components';
/**
* Dashboard advanced filter button.
*/
export function DashboardFilterButton({ conditionsCount }) {
return (
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': conditionsCount > 0,
})}
text={
conditionsCount > 0 ? (
intl.get('count_filters_applied', { count: conditionsCount })
) : (
<T id={'filter'} />
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
);
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import {
Button,
PopoverInteractionKind,
Popover,
Menu,
MenuItem,
MenuDivider,
Classes
} from '@blueprintjs/core';
import { Icon } from 'components';
export function DashboardRowsHeightButton() {
return (
<Popover
minimal={true}
content={
<Menu>
<MenuDivider title={'Rows height'} />
<MenuItem text="Compact" />
<MenuItem text="Medium" />
</Menu>
}
placement="bottom-start"
modifiers={{
offset: { offset: '0, 4' },
}}
interactionKind={PopoverInteractionKind.CLICK}
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="rows-height" iconSize={16} />}
/>
</Popover>
);
}

View File

@@ -1,276 +0,0 @@
// @flow
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import {
FormGroup,
Classes,
HTMLSelect,
Button,
Intent,
} from '@blueprintjs/core';
import { useFormik } from 'formik';
import { isEqual, last } from 'lodash';
import { usePrevious } from 'react-use';
import Icon from 'components/Icon';
import { checkRequiredProperties, uniqueMultiProps } from 'utils';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import {
DynamicFilterValueField,
DynamicFilterCompatatorField,
} from 'components';
import Toaster from 'components/AppToaster';
import moment from 'moment';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator,
} from './DynamicFilter/DynamicFilterCompatators';
let limitToast;
type InitialCondition = {
fieldKey: string,
comparator: string,
value: string,
};
/**
* Filter popover content.
*/
export default function FilterDropdown({
fields,
onFilterChange,
initialCondition,
initialConditions,
}) {
// Fields key -> metadata table.
const fieldsKeyMapped = useMemo(() =>
new Map(fields.map((field) => [field.key, field])),
[fields]
);
// Conditions options.
const conditionalsOptions = useMemo(
() => [
{ value: '&&', label: intl.get('and') },
{ value: '||', label: intl.get('or') },
],
[],
);
// Resources fileds options for fields options.
const resourceFieldsOptions = useMemo(
() => [
...fields.map((field) => ({
value: field.key,
label: field.label,
})),
],
[fields],
);
// Default filter conition.
const defaultFilterCondition = useMemo(
() => ({
condition: '&&',
fieldKey: initialCondition.fieldKey,
comparator: initialCondition.comparator,
value: initialCondition.value,
}),
[initialCondition],
);
// Formik for validation purposes.
const { setFieldValue, getFieldProps, values } = useFormik({
initialValues: {
conditions: [
...((initialConditions && initialConditions.length) ?
[...initialConditions] : [defaultFilterCondition]),
],
},
});
// Handle click a new filter row.
const onClickNewFilter = useCallback(() => {
if (values.conditions.length >= 12) {
limitToast = Toaster.show(
{
message: intl.get('you_reached_conditions_limit'),
intent: Intent.WARNING,
},
limitToast,
);
} else {
setFieldValue('conditions', [
...values.conditions,
defaultFilterCondition
]);
}
}, [values, setFieldValue, defaultFilterCondition]);
// Filtered conditions that filters conditions that don't contain atleast
// on required fields or fileds keys that not exists.
const filteredFilterConditions = useMemo(() => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const conditions = values.conditions
.filter(
(condition) => !checkRequiredProperties(condition, requiredProps),
)
.filter(
(condition) => !!fieldsKeyMapped.get(condition.fieldKey),
);
return uniqueMultiProps(conditions, requiredProps);
}, [values.conditions, fieldsKeyMapped]);
// Previous filtered conditions.
const prevConditions = usePrevious(filteredFilterConditions);
useEffect(() => {
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChange && onFilterChange(filteredFilterConditions);
}
}, [filteredFilterConditions, prevConditions, onFilterChange]);
// Handle click remove condition.
const onClickRemoveCondition = (index) => () => {
if (values.conditions.length === 1) {
setFieldValue('conditions', [defaultFilterCondition]);
return;
}
const conditions = [...values.conditions];
conditions.splice(index, 1);
setFieldValue('conditions', [...conditions]);
};
// Transform dynamic value field.
const transformValueField = (value) => {
if (value instanceof Date) {
return moment(value).format('YYYY-MM-DD');
} else if (typeof value === 'object') {
return value.id;
}
return value;
};
// Override getFieldProps for conditions fields.
const fieldProps = (name, index) => {
const override = {
...getFieldProps(`conditions[${index}].${name}`),
};
return {
...override,
onChange: (e) => {
if (name === 'fieldKey') {
const currentField = fieldsKeyMapped.get(
values.conditions[index].fieldKey,
);
const nextField = fieldsKeyMapped.get(e.currentTarget.value);
if (currentField.field_type !== nextField.field_type) {
setFieldValue(`conditions[${index}].value`, '');
}
const comparatorsObs = getConditionTypeCompatators(
nextField.field_type,
);
const currentCompatator = values.conditions[index].comparator;
if (
!currentCompatator ||
comparatorsObs.map((c) => c.value).indexOf(currentCompatator) === -1
) {
const defaultCompatator = getConditionDefaultCompatator(
nextField.field_type,
);
setFieldValue(
`conditions[${index}].comparator`,
defaultCompatator.value,
);
}
}
override.onChange(e);
},
};
};
// Compatator field props.
const comparatorFieldProps = (name, index) => {
const condition = values.conditions[index];
const field = fieldsKeyMapped.get(condition.fieldKey);
return {
...fieldProps(name, index),
dataType: field.field_type,
};
};
// Value field props.
const valueFieldProps = (name, index) => {
const condition = values.conditions[index];
const field = fieldsKeyMapped.get(condition.fieldKey);
return {
...fieldProps(name, index),
fieldName: field.label,
fieldType: field.field_type,
options: field.options,
optionsResource: field.options_resource,
onChange: (value) => {
const transformedValue = transformValueField(value);
setFieldValue(`conditions[${index}].${name}`, transformedValue);
},
};
};
return (
<div class="filter-dropdown">
<div class="filter-dropdown__body">
{values.conditions.map((condition, index) => (
<div class="filter-dropdown__condition">
<FormGroup className={'form-group--condition'}>
<HTMLSelect
options={conditionalsOptions}
className={Classes.FILL}
disabled={index > 1}
{...fieldProps('condition', index)}
/>
</FormGroup>
<FormGroup className={'form-group--field'}>
<HTMLSelect
options={resourceFieldsOptions}
value={1}
className={Classes.FILL}
{...fieldProps('fieldKey', index)}
/>
</FormGroup>
<FormGroup className={'form-group--comparator'}>
<DynamicFilterCompatatorField
className={Classes.FILL}
{...comparatorFieldProps('comparator', index)}
/>
</FormGroup>
<DynamicFilterValueField
{...valueFieldProps('value', index)} />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={onClickRemoveCondition(index)}
/>
</div>
))}
</div>
<div class="filter-dropdown__footer">
<Button
minimal={true}
intent={Intent.PRIMARY}
onClick={onClickNewFilter}
>
<T id={'new_conditional'} />
</Button>
</div>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import React, { useState, useMemo, useEffect } from 'react';
import { Button, MenuItem } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
@@ -66,10 +67,23 @@ export default function ListSelect({
onItemSelect && onItemSelect(_item);
};
// Filters accounts types items.
const filterItems = (query, item, _index, exactMatch) => {
const normalizedTitle = item[textProp].toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
return (
<Select
itemRenderer={itemRenderer}
onItemSelect={handleItemSelect}
itemPredicate={filterItems}
{...selectProps}
noResults={noResults}
disabled={disabled}
@@ -83,6 +97,7 @@ export default function ListSelect({
loading={isLoading}
disabled={disabled}
{...buttonProps}
fill={true}
/>
</Select>
);

View File

@@ -62,6 +62,10 @@ import Card from './Card';
import { ItemsMultiSelect } from './Items';
export * from './Menu';
export * from './AdvancedFilter/AdvancedFilterDropdown';
export * from './AdvancedFilter/AdvancedFilterPopover';
export * from './Dashboard/DashboardFilterButton';
export * from './Dashboard/DashboardRowsHeightButton';
const Hint = FieldHint;