mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
feat: WIP advanced filter.
This commit is contained in:
@@ -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(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -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),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
397
client/src/components/AdvancedFilter/AdvancedFilterDropdown.js
Normal file
397
client/src/components/AdvancedFilter/AdvancedFilterDropdown.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
131
client/src/components/AdvancedFilter/AdvancedFilterValueField.js
Normal file
131
client/src/components/AdvancedFilter/AdvancedFilterValueField.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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(() => {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
110
client/src/components/AdvancedFilter/utils.js
Normal file
110
client/src/components/AdvancedFilter/utils.js
Normal 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)
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
26
client/src/components/Dashboard/DashboardFilterButton.js
Normal file
26
client/src/components/Dashboard/DashboardFilterButton.js
Normal 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} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
36
client/src/components/Dashboard/DashboardRowsHeightButton.js
Normal file
36
client/src/components/Dashboard/DashboardRowsHeightButton.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user