mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: WIP adavanced filter.
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.4.0",
|
"@testing-library/react": "^9.4.0",
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
|
"@types/lodash": "^4.14.172",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
||||||
"@typescript-eslint/parser": "^2.10.0",
|
"@typescript-eslint/parser": "^2.10.0",
|
||||||
"accounting": "^0.4.1",
|
"accounting": "^0.4.1",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { HTMLSelect, Classes } from '@blueprintjs/core';
|
import { HTMLSelect, Classes } from '@blueprintjs/core';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { getConditionTypeCompatators } from './DynamicFilterCompatators';
|
import { getConditionTypeCompatators } from './utils';
|
||||||
|
|
||||||
export default function DynamicFilterCompatatorField({
|
export default function DynamicFilterCompatatorField({
|
||||||
dataType,
|
dataType,
|
||||||
@@ -9,7 +9,7 @@ export default function DynamicFilterCompatatorField({
|
|||||||
}) {
|
}) {
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() => getConditionTypeCompatators(dataType).map(comp => ({
|
() => getConditionTypeCompatators(dataType).map(comp => ({
|
||||||
value: comp.value, label: intl.get(comp.label_id),
|
value: comp.value, label: intl.get(comp.label),
|
||||||
})),
|
})),
|
||||||
[dataType]
|
[dataType]
|
||||||
);
|
);
|
||||||
306
client/src/components/AdvancedFilter/AdvancedFilterDropdown.tsx
Normal file
306
client/src/components/AdvancedFilter/AdvancedFilterDropdown.tsx
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
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,47 @@
|
|||||||
|
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,104 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
client/src/components/AdvancedFilter/components.tsx
Normal file
22
client/src/components/AdvancedFilter/components.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
const DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced filter auto-save.
|
||||||
|
*/
|
||||||
|
export function useAdvancedFilterAutoSubmit() {
|
||||||
|
const { submitForm, values } = useFormikContext();
|
||||||
|
const [isSubmit, setIsSubmit] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
const debouncedSubmit = React.useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
return submitForm().then(() => setIsSubmit(true));
|
||||||
|
}, DEBOUNCE_MS),
|
||||||
|
[submitForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => debouncedSubmit, [debouncedSubmit, values]);
|
||||||
|
}
|
||||||
84
client/src/components/AdvancedFilter/interfaces.ts
Normal file
84
client/src/components/AdvancedFilter/interfaces.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { ArrayHelpers } from 'formik';
|
||||||
|
|
||||||
|
export type IResourceFieldType = 'text' | 'number' | 'enumeration' | 'boolean';
|
||||||
|
|
||||||
|
export interface IResourceField {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
fieldType: IResourceFieldType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAdvancedFilterDropdown {
|
||||||
|
fields: IResourceField[];
|
||||||
|
defaultFieldKey: string;
|
||||||
|
defaultComparator?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAdvancedFilterDropdownFooter {
|
||||||
|
onClick?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterFieldsField {
|
||||||
|
fields: IResourceField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterRole {
|
||||||
|
fieldKey: string;
|
||||||
|
comparator: string;
|
||||||
|
condition: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAdvancedFilterContextProps {
|
||||||
|
initialCondition: IFilterRole;
|
||||||
|
fields: IResourceField[];
|
||||||
|
fieldsByKey: { [fieldKey: string]: IResourceField };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterConditionContextProps {
|
||||||
|
conditionIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAdvancedFilterProviderProps {
|
||||||
|
initialCondition: IFilterRole;
|
||||||
|
fields: IResourceField[];
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterConditionProviderProps {
|
||||||
|
conditionIndex: number;
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterDropdownFormikValues {
|
||||||
|
conditions: IFilterRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IAdvancedFilterDropdownConditionsProps = ArrayHelpers;
|
||||||
|
|
||||||
|
export interface IAdvancedFilterDropdownCondition {
|
||||||
|
conditionIndex: number;
|
||||||
|
onRemoveClick: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterOption {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAdvancedFilterValueField {
|
||||||
|
fieldType: string;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
options?: IFilterOption[];
|
||||||
|
onChange: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IFieldType {
|
||||||
|
TEXT = 'text',
|
||||||
|
NUMBER = 'number',
|
||||||
|
DATE = 'date',
|
||||||
|
ENUMERATION = 'enumeration',
|
||||||
|
BOOLEAN = 'boolean',
|
||||||
|
}
|
||||||
98
client/src/components/AdvancedFilter/utils.ts
Normal file
98
client/src/components/AdvancedFilter/utils.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export const BooleanCompatators = [
|
|
||||||
{ value: 'is', label_id: 'is' },
|
|
||||||
{ value: 'is_not', label_id: 'is_not' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TextCompatators = [
|
|
||||||
{ value: 'contain', label_id: 'contain' },
|
|
||||||
{ value: 'not_contain', label_id: 'not_contain' },
|
|
||||||
{ value: 'equals', label_id: 'equals' },
|
|
||||||
{ value: 'not_equal', label_id: 'not_equals' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DateCompatators = [
|
|
||||||
{ value: 'in', label_id: 'in' },
|
|
||||||
{ value: 'after', label_id: 'after' },
|
|
||||||
{ value: 'before', label_id: 'before' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const OptionsCompatators = [
|
|
||||||
{ value: 'is', label_id: 'is' },
|
|
||||||
{ value: 'is_not', label_id: 'is_not' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const NumberCampatators = [
|
|
||||||
{ value: 'equals', label_id: 'equals' },
|
|
||||||
{ value: 'not_equal', label_id: 'not_equal' },
|
|
||||||
{ value: 'bigger_than', label_id: 'bigger_than' },
|
|
||||||
{ value: 'bigger_or_equals', label_id: 'bigger_or_equals' },
|
|
||||||
{ value: 'smaller_than', label_id: 'smaller_than' },
|
|
||||||
{ value: 'smaller_or_equals', label_id: 'smaller_or_equals' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const getConditionTypeCompatators = (dataType) => {
|
|
||||||
return [
|
|
||||||
...(dataType === 'options'
|
|
||||||
? [...OptionsCompatators]
|
|
||||||
: dataType === 'date'
|
|
||||||
? [...DateCompatators]
|
|
||||||
: dataType === 'boolean'
|
|
||||||
? [...BooleanCompatators]
|
|
||||||
: dataType === 'number'
|
|
||||||
? [...NumberCampatators]
|
|
||||||
: [...TextCompatators]),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getConditionDefaultCompatator = (dataType) => {
|
|
||||||
const compatators = getConditionTypeCompatators(dataType);
|
|
||||||
return compatators[0];
|
|
||||||
};
|
|
||||||
@@ -6,8 +6,8 @@ import For from './Utils/For';
|
|||||||
import { FormattedMessage, FormattedHTMLMessage } from './FormattedMessage';
|
import { FormattedMessage, FormattedHTMLMessage } from './FormattedMessage';
|
||||||
import ListSelect from './ListSelect';
|
import ListSelect from './ListSelect';
|
||||||
import FinancialStatement from './FinancialStatement';
|
import FinancialStatement from './FinancialStatement';
|
||||||
import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField';
|
// import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField';
|
||||||
import DynamicFilterCompatatorField from './DynamicFilter/DynamicFilterCompatatorField';
|
// import DynamicFilterCompatatorField from './DynamicFilter/DynamicFilterCompatatorField';
|
||||||
import ErrorMessage from './ErrorMessage';
|
import ErrorMessage from './ErrorMessage';
|
||||||
import MODIFIER from './modifiers';
|
import MODIFIER from './modifiers';
|
||||||
import FieldHint from './FieldHint';
|
import FieldHint from './FieldHint';
|
||||||
@@ -78,8 +78,8 @@ export {
|
|||||||
Money,
|
Money,
|
||||||
ListSelect,
|
ListSelect,
|
||||||
FinancialStatement,
|
FinancialStatement,
|
||||||
DynamicFilterValueField,
|
// DynamicFilterValueField,
|
||||||
DynamicFilterCompatatorField,
|
// DynamicFilterCompatatorField,
|
||||||
MODIFIER,
|
MODIFIER,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
FieldHint,
|
FieldHint,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import intl from 'react-intl-universal';
|
|||||||
import { If, DashboardActionViewsList } from 'components';
|
import { If, DashboardActionViewsList } from 'components';
|
||||||
|
|
||||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||||
import FilterDropdown from 'components/FilterDropdown';
|
import AdvancedFilterDropdown from 'components/AdvancedFilter/AdvancedFilterDropdown.tsx';
|
||||||
|
|
||||||
import { useRefreshAccounts } from 'hooks/query/accounts';
|
import { useRefreshAccounts } from 'hooks/query/accounts';
|
||||||
import { useAccountsChartContext } from 'containers/Accounts/AccountsChartProvider';
|
import { useAccountsChartContext } from 'containers/Accounts/AccountsChartProvider';
|
||||||
@@ -30,6 +30,46 @@ import withAccountsTableActions from './withAccountsTableActions';
|
|||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
const FIELDS = [
|
||||||
|
{
|
||||||
|
name: 'Name',
|
||||||
|
key: 'name',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Account code',
|
||||||
|
key: 'code',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Balance',
|
||||||
|
key: 'balance',
|
||||||
|
fieldType: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Active',
|
||||||
|
key: 'active',
|
||||||
|
fieldType: 'boolean'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Created at',
|
||||||
|
key: 'created_at',
|
||||||
|
fieldType: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Root type',
|
||||||
|
key: 'root_type',
|
||||||
|
fieldType: 'enumeration',
|
||||||
|
options: [
|
||||||
|
{ key: 'asset', label: 'Asset' },
|
||||||
|
{ key: 'liability', label: 'Liability' },
|
||||||
|
{ key: 'equity', label: 'Equity' },
|
||||||
|
{ key: 'Income', label: 'Income' },
|
||||||
|
{ key: 'expense', label: 'Expense' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accounts actions bar.
|
* Accounts actions bar.
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +150,14 @@ function AccountsActionsBar({
|
|||||||
/>
|
/>
|
||||||
<Popover
|
<Popover
|
||||||
minimal={true}
|
minimal={true}
|
||||||
content={''}
|
content={
|
||||||
|
<AdvancedFilterDropdown
|
||||||
|
defaultFieldKey={'name'}
|
||||||
|
fields={FIELDS}
|
||||||
|
onFilterChange={(filterConditions) => {
|
||||||
|
console.log(filterConditions, 'XXX');
|
||||||
|
}} />
|
||||||
|
}
|
||||||
interactionKind={PopoverInteractionKind.CLICK}
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
position={Position.BOTTOM_LEFT}
|
position={Position.BOTTOM_LEFT}
|
||||||
canOutsideClickClose={true}
|
canOutsideClickClose={true}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { connect } from 'react-redux';
|
|||||||
import { If } from 'components';
|
import { If } from 'components';
|
||||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||||
import Icon from 'components/Icon';
|
import Icon from 'components/Icon';
|
||||||
import FilterDropdown from 'components/FilterDropdown';
|
|
||||||
|
|
||||||
import { useRefreshExchangeRate } from 'hooks/query/exchangeRates';
|
import { useRefreshExchangeRate } from 'hooks/query/exchangeRates';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
|||||||
Reference in New Issue
Block a user