feat: WIP advanced filter.

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

View File

@@ -138,6 +138,7 @@
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.8",
"@types/yup": "^0.29.13",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"compression-webpack-plugin": "^6.1.0",
"http-proxy-middleware": "^1.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,24 +5,26 @@ import {
NavbarGroup,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import {
AdvancedFilterPopover,
DashboardFilterButton,
FormattedMessage as T,
} from 'components';
import { useRefreshJournals } from 'hooks/query/manualJournals';
import { useManualJournalsContext } from './ManualJournalsListProvider';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withManualJournalsActions from './withManualJournalsActions';
import withManualJournals from './withManualJournals';
import { If, DashboardActionViewsList } from 'components';
import withManualJournalsActions from './withManualJournalsActions';
import { compose } from 'utils';
/**
@@ -31,12 +33,15 @@ import { compose } from 'utils';
function ManualJournalActionsBar({
// #withManualJournalsActions
setManualJournalsTableState,
// #withManualJournals
manualJournalsFilterConditions,
}) {
// History context.
const history = useHistory();
// Manual journals context.
const { journalsViews } = useManualJournalsContext();
const { journalsViews, fields } = useManualJournalsContext();
// Manual journals refresh action.
const { refresh } = useRefreshJournals();
@@ -45,7 +50,6 @@ function ManualJournalActionsBar({
const onClickNewManualJournal = () => {
history.push('/make-journal-entry');
};
// Handle delete button click.
const handleBulkDelete = () => {};
@@ -53,11 +57,10 @@ function ManualJournalActionsBar({
const handleTabChange = (customView) => {
setManualJournalsTableState({ customViewId: customView.id || null });
};
// Handle click a refresh Journals
const handleRefreshBtnClick = () => {
refresh();
};
const handleRefreshBtnClick = () => { refresh(); };
console.log(manualJournalsFilterConditions, fields, 'XXX');
return (
<DashboardActionsBar>
@@ -75,20 +78,20 @@ function ManualJournalActionsBar({
text={<T id={'journal_entry'} />}
onClick={onClickNewManualJournal}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: manualJournalsFilterConditions,
defaultFieldKey: 'journal_number',
fields,
onFilterChange: (filterConditions) => {
setManualJournalsTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': false,
})}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
<DashboardFilterButton
conditionsCount={manualJournalsFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
@@ -130,4 +133,7 @@ function ManualJournalActionsBar({
export default compose(
withDialogActions,
withManualJournalsActions,
withManualJournals(({ manualJournalsTableState }) => ({
manualJournalsFilterConditions: manualJournalsTableState.filterRoles,
}))
)(ManualJournalActionsBar);

View File

@@ -1,27 +1,36 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useJournals } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceViews, useResourceMeta, useJournals } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const ManualJournalsContext = createContext();
function ManualJournalsListProvider({ query, ...props }) {
// Fetches accounts resource views and fields.
const { data: journalsViews, isLoading: isViewsLoading } = useResourceViews(
'manual_journals',
);
const { data: journalsViews, isLoading: isViewsLoading } =
useResourceViews('manual_journals');
// Fetches the manual journals transactions with pagination meta.
const {
data: { manualJournals, pagination, filterMeta },
isLoading: isManualJournalsLoading,
isFetching: isManualJournalsFetching
isFetching: isManualJournalsFetching,
} = useJournals(query, { keepPreviousData: true });
// Fetch the accounts resource fields.
const {
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('manual_journals');
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: manualJournals, pagination, filterMeta,
}) && !isManualJournalsFetching;
const isEmptyStatus =
isTableEmptyStatus({
data: manualJournals,
pagination,
filterMeta,
}) && !isManualJournalsFetching;
// Global state.
const state = {
@@ -29,15 +38,21 @@ function ManualJournalsListProvider({ query, ...props }) {
pagination,
journalsViews,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isManualJournalsLoading,
isManualJournalsFetching,
isViewsLoading,
isEmptyStatus
isEmptyStatus,
};
const isPageLoading =
isManualJournalsLoading || isViewsLoading || isResourceMetaLoading;
return (
<DashboardInsider loading={isViewsLoading} name={'manual-journals'}>
<DashboardInsider loading={isPageLoading} name={'manual-journals'}>
<ManualJournalsContext.Provider value={state} {...props} />
</DashboardInsider>
);

View File

@@ -6,20 +6,20 @@ import {
NavbarGroup,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
Intent,
Switch,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { If, DashboardActionViewsList } from 'components';
import {
AdvancedFilterPopover,
If,
DashboardActionViewsList,
DashboardFilterButton,
} from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import AdvancedFilterDropdown from 'components/AdvancedFilter/AdvancedFilterDropdown.tsx';
import { useRefreshAccounts } from 'hooks/query/accounts';
import { useAccountsChartContext } from 'containers/Accounts/AccountsChartProvider';
@@ -30,46 +30,6 @@ import withAccountsTableActions from './withAccountsTableActions';
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.
*/
@@ -80,6 +40,7 @@ function AccountsActionsBar({
// #withAccounts
accountsSelectedRows,
accountsInactiveMode,
accountsFilterConditions,
// #withAlertActions
openAlert,
@@ -90,7 +51,7 @@ function AccountsActionsBar({
// #ownProps
onFilterChanged,
}) {
const { resourceViews } = useAccountsChartContext();
const { resourceViews, fields } = useAccountsChartContext();
const onClickNewAccount = () => {
openDialog('account-form', {});
@@ -148,34 +109,22 @@ function AccountsActionsBar({
text={<T id={'new_account'} />}
onClick={onClickNewAccount}
/>
<Popover
minimal={true}
content={
<AdvancedFilterDropdown
defaultFieldKey={'name'}
fields={FIELDS}
onFilterChange={(filterConditions) => {
console.log(filterConditions, 'XXX');
}} />
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: accountsFilterConditions,
defaultFieldKey: 'name',
fields: fields,
onFilterChange: (filterConditions) => {
setAccountsTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': false,
})}
text={
true ? (
<T id={'filter'} />
) : (
intl.get('count_filters_applied', { count: 0 })
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
<DashboardFilterButton
conditionsCount={accountsFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<NavbarDivider />
<If condition={!isEmpty(accountsSelectedRows)}>
<Button
@@ -237,6 +186,7 @@ export default compose(
withAccounts(({ accountsSelectedRows, accountsTableState }) => ({
accountsSelectedRows,
accountsInactiveMode: accountsTableState.inactiveMode,
accountsFilterConditions: accountsTableState.filterRoles,
})),
withAccountsTableActions,
)(AccountsActionsBar);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import 'style/pages/Accounts/List.scss';
import { DashboardPageContent, DashboardContentTable } from 'components';
@@ -14,6 +14,7 @@ import withAccounts from 'containers/Accounts/withAccounts';
import { compose } from 'utils';
import { transformAccountsStateToQuery } from './utils';
import withAccountsTableActions from './withAccountsTableActions';
/**
* Accounts chart list.
@@ -21,7 +22,22 @@ import { transformAccountsStateToQuery } from './utils';
function AccountsChart({
// #withAccounts
accountsTableState,
// #withAccountsActions
setAccountsTableState,
}) {
// Resets the accounts table state once the page unmount.
useEffect(
() => () => {
setAccountsTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setAccountsTableState],
);
return (
<AccountsChartProvider
query={transformAccountsStateToQuery(accountsTableState)}
@@ -43,4 +59,5 @@ function AccountsChart({
export default compose(
withAccounts(({ accountsTableState }) => ({ accountsTableState })),
withAccountsTableActions,
)(AccountsChart);

View File

@@ -1,6 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useAccounts } from 'hooks/query';
import { useResourceViews, useResourceMeta, useAccounts } from 'hooks/query';
import { getFieldsFromResourceMeta } from 'utils';
const AccountsChartContext = createContext();
@@ -9,41 +10,42 @@ const AccountsChartContext = createContext();
*/
function AccountsChartProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: resourceViews, isLoading: isViewsLoading } = useResourceViews(
'accounts',
);
const { data: resourceViews, isLoading: isViewsLoading } =
useResourceViews('accounts');
// Fetch the accounts resource fields.
const {
data: resourceFields,
isLoading: isFieldsLoading,
} = useResourceFields('accounts');
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('accounts');
// Fetch accounts list according to the given custom view id.
const {
data: accounts,
isFetching: isAccountsFetching,
isLoading: isAccountsLoading
} = useAccounts(
query,
{ keepPreviousData: true }
);
isLoading: isAccountsLoading,
} = useAccounts(query, { keepPreviousData: true });
// Provider payload.
const provider = {
accounts,
resourceFields,
resourceMeta,
resourceViews,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isAccountsLoading,
isAccountsFetching,
isFieldsLoading,
isResourceMetaFetching,
isResourceMetaLoading,
isViewsLoading,
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceMetaLoading}
name={'accounts-chart'}
>
<AccountsChartContext.Provider value={provider} {...props} />

View File

@@ -1,23 +1,24 @@
import React, { useCallback } from 'react';
import React from 'react';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
Switch,
Alignment,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, Icon, DashboardActionViewsList } from 'components';
import {
If,
Icon,
DashboardActionViewsList,
AdvancedFilterPopover,
DashboardFilterButton,
} from 'components';
import { useCustomersListContext } from './CustomersListProvider';
import { useRefreshCustomers } from 'hooks/query/customers';
@@ -34,6 +35,7 @@ import { compose } from 'utils';
function CustomerActionsBar({
// #withCustomers
customersSelectedRows = [],
customersFilterConditions,
// #withCustomersActions
setCustomersTableState,
@@ -46,7 +48,7 @@ function CustomerActionsBar({
const history = useHistory();
// Customers list context.
const { customersViews } = useCustomersListContext();
const { customersViews, fields } = useCustomersListContext();
// Customers refresh action.
const { refresh } = useRefreshCustomers();
@@ -72,9 +74,7 @@ function CustomerActionsBar({
};
// Handle click a refresh customers
const handleRefreshBtnClick = () => {
refresh();
};
const handleRefreshBtnClick = () => { refresh(); };
return (
<DashboardActionsBar>
@@ -93,17 +93,21 @@ function CustomerActionsBar({
onClick={onClickNewCustomer}
/>
<NavbarDivider />
<Popover
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: customersFilterConditions,
defaultFieldKey: 'display_name',
fields: fields,
onFilterChange: (filterConditions) => {
setCustomersTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={`${intl.get('filter')}`}
icon={<Icon icon="filter-16" iconSize={16} />}
<DashboardFilterButton
conditionsCount={customersFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={customersSelectedRows.length}>
<Button
@@ -146,6 +150,7 @@ export default compose(
withCustomers(({ customersSelectedRows, customersTableState }) => ({
customersSelectedRows,
accountsInactiveMode: customersTableState.inactiveMode,
customersFilterConditions: customersTableState.filterRoles,
})),
withAlertActions,
)(CustomerActionsBar);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import 'style/pages/Customers/List.scss';
@@ -11,6 +11,8 @@ import CustomersAlerts from 'containers/Customers/CustomersAlerts';
import { CustomersListProvider } from './CustomersListProvider';
import withCustomers from './withCustomers';
import withCustomersActions from './withCustomersActions';
import { compose } from 'utils';
/**
@@ -19,7 +21,22 @@ import { compose } from 'utils';
function CustomersList({
// #withCustomers
customersTableState,
// #withCustomersActions
setCustomersTableState
}) {
// Resets the accounts table state once the page unmount.
useEffect(
() => () => {
setCustomersTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setCustomersTableState],
);
return (
<CustomersListProvider tableState={customersTableState}>
<CustomersActionsBar />
@@ -38,4 +55,5 @@ function CustomersList({
export default compose(
withCustomers(({ customersTableState }) => ({ customersTableState })),
withCustomersActions
)(CustomersList);

View File

@@ -1,8 +1,8 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useCustomers } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceMeta, useResourceViews, useCustomers } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
import { transformCustomersStateToQuery } from './utils';
const CustomersListContext = createContext();
@@ -12,10 +12,15 @@ function CustomersListProvider({ tableState, ...props }) {
const tableQuery = transformCustomersStateToQuery(tableState);
// Fetch customers resource views and fields.
const { data: customersViews, isLoading: isViewsLoading } =
useResourceViews('customers');
// Fetch the customers resource fields.
const {
data: customersViews,
isLoading: isCustomersViewsLoading,
} = useResourceViews('customers');
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('customers');
// Fetches customers data with pagination meta.
const {
@@ -25,16 +30,26 @@ function CustomersListProvider({ tableState, ...props }) {
} = useCustomers(tableQuery, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: customers, pagination, filterMeta,
}) && !isCustomersFetching && !tableState.inactiveMode;
const isEmptyStatus =
isTableEmptyStatus({
data: customers,
pagination,
filterMeta,
}) &&
!isCustomersFetching &&
!tableState.inactiveMode;
const state = {
customersViews,
customers,
pagination,
isCustomersViewsLoading,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
resourceMeta,
isResourceMetaLoading,
isResourceMetaFetching,
isViewsLoading,
isCustomersLoading,
isCustomersFetching,
@@ -42,7 +57,10 @@ function CustomersListProvider({ tableState, ...props }) {
};
return (
<DashboardInsider loading={isCustomersViewsLoading} name={'customers-list'}>
<DashboardInsider
loading={isViewsLoading || isResourceMetaLoading || isCustomersLoading}
name={'customers-list'}
>
<CustomersListContext.Provider value={state} {...props} />
</DashboardInsider>
);

View File

@@ -1,28 +1,31 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { If, DashboardActionViewsList } from 'components';
import {
If,
DashboardActionViewsList,
DashboardFilterButton,
AdvancedFilterPopover,
} from 'components';
import { useRefreshExpenses } from 'hooks/query/expenses';
import { useExpensesListContext } from './ExpensesListProvider';
import withExpensesActions from './withExpensesActions';
import withExpenses from './withExpenses';
import { compose } from 'utils';
@@ -32,14 +35,15 @@ import { compose } from 'utils';
function ExpensesActionsBar({
//#withExpensesActions
setExpensesTableState,
}) {
const [filterCount, setFilterCount] = useState(0);
// #withExpenses
expensesFilterConditions
}) {
// History context.
const history = useHistory();
// Expenses list context.
const { expensesViews } = useExpensesListContext();
const { expensesViews, fields } = useExpensesListContext();
// Expenses refresh action.
const { refresh } = useRefreshExpenses();
@@ -79,20 +83,20 @@ function ExpensesActionsBar({
text={<T id={'new_expense'} />}
onClick={onClickNewExpense}
/>
<Popover
minimal={true}
content={''}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: expensesFilterConditions,
defaultFieldKey: 'reference_no',
fields: fields,
onFilterChange: (filterConditions) => {
setExpensesTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': filterCount > 0,
})}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
<DashboardFilterButton
conditionsCount={expensesFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
@@ -134,4 +138,7 @@ function ExpensesActionsBar({
export default compose(
withDialogActions,
withExpensesActions,
withExpenses(({ expensesTableState }) => ({
expensesFilterConditions: expensesTableState.filterRoles,
}))
)(ExpensesActionsBar);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import 'style/pages/Expense/List.scss';
@@ -10,6 +10,7 @@ import ExpenseDataTable from './ExpenseDataTable';
import ExpensesAlerts from '../ExpensesAlerts';
import withExpenses from './withExpenses';
import withExpensesActions from './withExpensesActions';
import { compose, transformTableStateToQuery } from 'utils';
import { ExpensesListProvider } from './ExpensesListProvider';
@@ -20,7 +21,22 @@ import { ExpensesListProvider } from './ExpensesListProvider';
function ExpensesList({
// #withExpenses
expensesTableState,
// #withExpensesActions
setExpensesTableState,
}) {
// Resets the accounts table state once the page unmount.
useEffect(
() => () => {
setExpensesTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setExpensesTableState],
);
return (
<ExpensesListProvider
query={transformTableStateToQuery(expensesTableState)}
@@ -42,4 +58,5 @@ function ExpensesList({
export default compose(
withExpenses(({ expensesTableState }) => ({ expensesTableState })),
withExpensesActions,
)(ExpensesList);

View File

@@ -1,7 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useExpenses, useResourceViews } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useExpenses, useResourceMeta, useResourceViews } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const ExpensesListContext = createContext();
@@ -10,9 +10,8 @@ const ExpensesListContext = createContext();
*/
function ExpensesListProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: expensesViews, isLoading: isViewsLoading } = useResourceViews(
'expenses',
);
const { data: expensesViews, isLoading: isViewsLoading } =
useResourceViews('expenses');
// Fetches the expenses with pagination meta.
const {
@@ -21,10 +20,20 @@ function ExpensesListProvider({ query, ...props }) {
isFetching: isExpensesFetching,
} = useExpenses(query, { keepPreviousData: true });
// Fetch the expenses resource fields.
const {
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('expenses');
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: expenses, pagination, filterMeta,
}) && !isExpensesFetching;
const isEmptyStatus =
isTableEmptyStatus({
data: expenses,
pagination,
filterMeta,
}) && !isExpensesFetching;
// Provider payload.
const provider = {
@@ -32,16 +41,21 @@ function ExpensesListProvider({ query, ...props }) {
expenses,
pagination,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
resourceMeta,
isResourceMetaLoading,
isResourceMetaFetching,
isViewsLoading,
isExpensesLoading,
isExpensesFetching,
isEmptyStatus
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isExpensesLoading}
loading={isViewsLoading || isExpensesLoading || isResourceMetaLoading}
name={'expenses'}
>
<ExpensesListContext.Provider value={provider} {...props} />
@@ -49,7 +63,6 @@ function ExpensesListProvider({ query, ...props }) {
);
}
const useExpensesListContext = () =>
React.useContext(ExpensesListContext);
const useExpensesListContext = () => React.useContext(ExpensesListContext);
export { ExpensesListProvider, useExpensesListContext };

View File

@@ -10,6 +10,5 @@ export default (mapState) => {
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,24 +1,23 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import {
Popover,
NavbarGroup,
NavbarDivider,
PopoverInteractionKind,
Position,
Button,
Classes,
Intent,
Switch,
Alignment,
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import Icon from 'components/Icon';
import { If, DashboardActionViewsList } from 'components';
import {
If,
DashboardActionViewsList,
AdvancedFilterPopover,
DashboardFilterButton,
} from 'components';
import { useItemsListContext } from './ItemsListProvider';
import { useRefreshItems } from 'hooks/query/items';
@@ -35,6 +34,7 @@ import { compose } from 'utils';
function ItemsActionsBar({
// #withItems
itemsSelectedRows,
itemsFilterRoles,
// #withItemActions
setItemsTableState,
@@ -44,7 +44,7 @@ function ItemsActionsBar({
openAlert,
}) {
// Items list context.
const { itemsViews } = useItemsListContext();
const { itemsViews, fields } = useItemsListContext();
// Items refresh action.
const { refresh } = useRefreshItems();
@@ -93,19 +93,21 @@ function ItemsActionsBar({
text={<T id={'new_item'} />}
onClick={onClickNewItem}
/>
<NavbarDivider />
<Popover
content={''}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: itemsFilterRoles,
defaultFieldKey: 'name',
fields: fields,
onFilterChange: (filterConditions) => {
setItemsTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={`${intl.get('filter')}`}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<DashboardFilterButton conditionsCount={itemsFilterRoles.length} />
</AdvancedFilterPopover>
<NavbarDivider />
<If condition={itemsSelectedRows.length}>
<Button
@@ -149,6 +151,7 @@ export default compose(
withItems(({ itemsSelectedRows, itemsTableState }) => ({
itemsSelectedRows,
itemsInactiveMode: itemsTableState.inactiveMode,
itemsFilterRoles: itemsTableState.filterRoles,
})),
withItemsActions,
withAlertActions,

View File

@@ -1,62 +1,74 @@
import React, { createContext } from 'react';
import { transformTableQueryToParams, isTableEmptyStatus } from 'utils';
import {
getFieldsFromResourceMeta,
transformTableQueryToParams,
isTableEmptyStatus,
} from 'utils';
import { transformItemsTableState } from './utils';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useItems } from 'hooks/query';
import { useResourceViews, useResourceMeta, useItems } from 'hooks/query';
const ItemsContext = createContext();
/**
* Items list provider.
*/
function ItemsListProvider({
tableState,
...props
}) {
function ItemsListProvider({ tableState, ...props }) {
const tableQuery = transformItemsTableState(tableState);
// Fetch accounts resource views and fields.
const { data: itemsViews, isLoading: isViewsLoading } = useResourceViews(
'items',
);
const { data: itemsViews, isLoading: isViewsLoading } =
useResourceViews('items');
// Fetch the accounts resource fields.
const { data: itemsFields, isLoading: isFieldsLoading } = useResourceFields(
'items',
);
const {
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('items');
// Handle fetching the items table based on the given query.
const {
data: { items, pagination, filterMeta },
isFetching: isItemsFetching,
isLoading: isItemsLoading,
} = useItems({
...transformTableQueryToParams(tableQuery)
}, { keepPreviousData: true });
} = useItems(
{
...transformTableQueryToParams(tableQuery),
},
{ keepPreviousData: true },
);
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: items, pagination, filterMeta,
}) && !isItemsFetching && !tableState.inactiveMode;
const isEmptyStatus =
isTableEmptyStatus({
data: items,
pagination,
filterMeta,
}) &&
!isItemsFetching &&
!tableState.inactiveMode;
const state = {
itemsViews,
itemsFields,
items,
pagination,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isViewsLoading,
isItemsLoading,
isItemsFetching: isItemsFetching,
isResourceLoading,
isResourceFetching,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isFieldsLoading}
name={'items-list'}
>
<DashboardInsider loading={isItemsLoading || isResourceLoading} name={'items-list'}>
<ItemsContext.Provider value={state} {...props} />
</DashboardInsider>
);

View File

@@ -1,4 +1,6 @@
import React from 'react';
import * as R from 'ramda';
import 'style/pages/ItemsCategories/List.scss';
import { DashboardContentTable, DashboardPageContent } from 'components';
@@ -8,12 +10,17 @@ import ItemsCategoryActionsBar from './ItemsCategoryActionsBar';
import { ItemsCategoriesProvider } from './ItemsCategoriesProvider';
import ItemCategoriesTable from './ItemCategoriesTable';
import withItemsCategories from './withItemCategories';
/**
* Item categories list.
*/
export default function ItemCategoryList() {
function ItemCategoryList({
// #withItemsCategories
itemsCategoriesTableState
}) {
return (
<ItemsCategoriesProvider>
<ItemsCategoriesProvider tableState={itemsCategoriesTableState}>
<ItemsCategoryActionsBar />
<DashboardPageContent>
@@ -25,3 +32,9 @@ export default function ItemCategoryList() {
</ItemsCategoriesProvider>
);
}
export default R.compose(
withItemsCategories(({ itemsCategoriesTableState }) => ({
itemsCategoriesTableState,
})),
)(ItemCategoryList);

View File

@@ -1,31 +1,50 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useItemsCategories } from 'hooks/query';
import { useItemsCategories, useResourceMeta } from 'hooks/query';
import { transformTableStateToQuery, getFieldsFromResourceMeta } from 'utils';
const ItemsCategoriesContext = createContext();
/**
* Items categories provider.
*/
function ItemsCategoriesProvider({ query, ...props }) {
function ItemsCategoriesProvider({ tableState, ...props }) {
// Transformes the table state to query.
const query = transformTableStateToQuery(tableState);
// Items categories list.
const {
data: { itemsCategories, pagination },
isFetching: isCategoriesFetching,
isLoading: isCategoriesLoading,
} = useItemsCategories();
} = useItemsCategories(query, { keepPreviousData: true });
// Fetch the accounts resource fields.
const {
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('item_category');
const state = {
isCategoriesFetching,
isCategoriesLoading,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
resourceMeta,
isResourceLoading,
isResourceFetching,
itemsCategories,
pagination,
query,
};
return (
<DashboardInsider name={'items-categories-list'}>
<DashboardInsider
isLoading={isResourceLoading}
name={'items-categories-list'}
>
<ItemsCategoriesContext.Provider value={state} {...props} />
</DashboardInsider>
);
@@ -34,7 +53,4 @@ function ItemsCategoriesProvider({ query, ...props }) {
const useItemsCategoriesContext = () =>
React.useContext(ItemsCategoriesContext);
export {
ItemsCategoriesProvider,
useItemsCategoriesContext,
};
export { ItemsCategoriesProvider, useItemsCategoriesContext };

View File

@@ -5,21 +5,20 @@ import {
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { If, Icon } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { AdvancedFilterPopover, DashboardFilterButton } from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
// import withItemCategories from './withItemCategories';
import withItemCategories from './withItemCategories';
import withItemCategoriesActions from './withItemCategoriesActions';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
import { useItemsCategoriesContext } from './ItemsCategoriesProvider';
/**
* Items categories actions bar.
@@ -27,6 +26,10 @@ import { compose } from 'utils';
function ItemsCategoryActionsBar({
// #withItemCategories
itemCategoriesSelectedRows = [],
categoriesFilterConditions,
//
setItemsCategoriesTableState,
// #withDialog
openDialog,
@@ -34,6 +37,8 @@ function ItemsCategoryActionsBar({
// #withAlertActions
openAlert,
}) {
const { fields } = useItemsCategoriesContext();
const onClickNewCategory = () => {
openDialog('item-category-form', {});
};
@@ -44,7 +49,9 @@ function ItemsCategoryActionsBar({
itemCategoriesIds: itemCategoriesSelectedRows,
});
};
console.log(fields, categoriesFilterConditions, 'XXXX');
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -56,25 +63,20 @@ function ItemsCategoryActionsBar({
/>
<NavbarDivider />
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: categoriesFilterConditions,
defaultFieldKey: 'name',
fields: fields,
onFilterChange: (filterConditions) => {
setItemsCategoriesTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
true ? (
<T id={'filter'} />
) : (
`${0} filters applied`
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
<DashboardFilterButton
conditionsCount={categoriesFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={itemCategoriesSelectedRows.length}>
<Button
@@ -103,8 +105,12 @@ function ItemsCategoryActionsBar({
export default compose(
withDialogActions,
// withItemCategories(({ itemCategoriesSelectedRows }) => ({
// itemCategoriesSelectedRows,
// })),
withItemCategories(
({ itemCategoriesSelectedRows, itemsCategoriesTableState }) => ({
itemCategoriesSelectedRows,
categoriesFilterConditions: itemsCategoriesTableState.filterRoles,
}),
),
withAlertActions,
withItemCategoriesActions
)(ItemsCategoryActionsBar);

View File

@@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import {
getItemsCategoriesTableStateFactory,
} from 'store/itemCategories/itemsCategories.selectors';
} from 'store/itemCategories/ItemsCategories.selectors';
export default (mapState) => {
const getItemsCategoriesTableState = getItemsCategoriesTableStateFactory();

View File

@@ -3,24 +3,25 @@ import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, DashboardActionViewsList } from 'components';
import {
If,
DashboardActionViewsList,
DashboardFilterButton,
AdvancedFilterPopover,
} from 'components';
import withBillsActions from './withBillsActions';
import withBills from './withBills';
import { useBillsListContext } from './BillsListProvider';
import { useRefreshBills } from 'hooks/query/bills';
import { compose } from 'utils';
@@ -31,6 +32,9 @@ import { compose } from 'utils';
function BillActionsBar({
// #withBillActions
setBillsTableState,
// #withBills
billsConditionsRoles
}) {
const history = useHistory();
@@ -38,9 +42,7 @@ function BillActionsBar({
const { refresh } = useRefreshBills();
// Bills list context.
const { billsViews } = useBillsListContext();
const [filterCount] = useState(0);
const { billsViews, fields } = useBillsListContext();
// Handle click a new bill.
const handleClickNewBill = () => {
@@ -53,11 +55,8 @@ function BillActionsBar({
customViewId: customView.id || null,
});
};
// Handle click a refresh bills
const handleRefreshBtnClick = () => {
refresh();
};
const handleRefreshBtnClick = () => { refresh(); };
return (
<DashboardActionsBar>
@@ -74,24 +73,21 @@ function BillActionsBar({
text={<T id={'new_bill'} />}
onClick={handleClickNewBill}
/>
<Popover
minimal={true}
content={[]}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: billsConditionsRoles,
defaultFieldKey: 'bill_number',
fields: fields,
onFilterChange: (filterConditions) => {
setBillsTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL)}
text={
true ? (
<T id={'filter'} />
) : (
`${filterCount} ${intl.get('filters_applied')}`
)
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
<DashboardFilterButton
conditionsCount={billsConditionsRoles.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
@@ -128,4 +124,9 @@ function BillActionsBar({
);
}
export default compose(withBillsActions)(BillActionsBar);
export default compose(
withBillsActions,
withBills(({ billsTableState }) => ({
billsConditionsRoles: billsTableState.filterRoles
}))
)(BillActionsBar);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
import 'style/pages/Bills/List.scss';
@@ -11,6 +11,7 @@ import BillsViewsTabs from './BillsViewsTabs';
import BillsTable from './BillsTable';
import withBills from './withBills';
import withBillsActions from './withBillsActions';
import { transformTableStateToQuery, compose } from 'utils';
@@ -20,7 +21,22 @@ import { transformTableStateToQuery, compose } from 'utils';
function BillsList({
// #withBills
billsTableState,
// #withBillsActions
setBillsTableState
}) {
// Resets the accounts table state once the page unmount.
useEffect(
() => () => {
setBillsTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setBillsTableState],
);
return (
<BillsListProvider query={transformTableStateToQuery(billsTableState)}>
<BillsActionsBar />
@@ -40,4 +56,5 @@ function BillsList({
export default compose(
withBills(({ billsTableState }) => ({ billsTableState })),
withBillsActions
)(BillsList);

View File

@@ -1,7 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useBills } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceViews, useResourceMeta, useBills } from 'hooks/query';
import { getFieldsFromResourceMeta, isTableEmptyStatus } from 'utils';
const BillsListContext = createContext();
@@ -10,15 +10,15 @@ const BillsListContext = createContext();
*/
function BillsListProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: billsViews, isLoading: isViewsLoading } = useResourceViews(
'bills',
);
const { data: billsViews, isLoading: isViewsLoading } =
useResourceViews('bills');
// Fetch the accounts resource fields.
const {
data: billsFields,
isLoading: isFieldsLoading,
} = useResourceFields('bills');
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('bills');
// Fetch accounts list according to the given custom view id.
const {
@@ -28,27 +28,33 @@ function BillsListProvider({ query, ...props }) {
} = useBills(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: bills, pagination, filterMeta,
}) && !isBillsFetching;
const isEmptyStatus =
isTableEmptyStatus({
data: bills,
pagination,
filterMeta,
}) && !isBillsFetching;
// Provider payload.
const provider = {
bills,
pagination,
billsFields,
billsViews,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isResourceLoading,
isResourceFetching,
isBillsLoading,
isBillsFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceLoading}
name={'bills'}
>
<BillsListContext.Provider value={provider} {...props} />

View File

@@ -3,26 +3,29 @@ import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, DashboardActionViewsList } from 'components';
import {
If,
DashboardActionViewsList,
DashboardFilterButton,
AdvancedFilterPopover,
} from 'components';
import withPaymentMade from './withPaymentMade';
import withPaymentMadeActions from './withPaymentMadeActions';
import { usePaymentMadesListContext } from './PaymentMadesListProvider';
import { useRefreshPaymentMades } from 'hooks/query/paymentMades';
import { compose } from 'utils';
/**
@@ -31,11 +34,14 @@ import { compose } from 'utils';
function PaymentMadeActionsBar({
// #withPaymentMadesActions
setPaymentMadesTableState,
// #withPaymentMades
paymentMadesFilterConditions
}) {
const history = useHistory();
// Payment receives list context.
const { paymentMadesViews } = usePaymentMadesListContext();
const { paymentMadesViews, fields } = usePaymentMadesListContext();
// Handle new payment made button click.
const handleClickNewPaymentMade = () => {
@@ -69,20 +75,21 @@ function PaymentMadeActionsBar({
text={<T id={'new_payment_made'} />}
onClick={handleClickNewPaymentMade}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: paymentMadesFilterConditions,
defaultFieldKey: 'payment_number',
fields: fields,
onFilterChange: (filterConditions) => {
setPaymentMadesTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL)}
text={
true ? <T id={'filter'} /> : `${0} ${intl.get('filters_applied')}`
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
<DashboardFilterButton
conditionsCount={paymentMadesFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
@@ -119,4 +126,9 @@ function PaymentMadeActionsBar({
);
}
export default compose(withPaymentMadeActions)(PaymentMadeActionsBar);
export default compose(
withPaymentMadeActions,
withPaymentMade(({ paymentMadesTableState }) => ({
paymentMadesFilterConditions: paymentMadesTableState.filterRoles,
})),
)(PaymentMadeActionsBar);

View File

@@ -10,6 +10,7 @@ import { PaymentMadesListProvider } from './PaymentMadesListProvider';
import PaymentMadeViewTabs from './PaymentMadeViewTabs';
import withPaymentMades from './withPaymentMade';
import withPaymentMadeActions from './withPaymentMadeActions';
import { compose, transformTableStateToQuery } from 'utils';
@@ -19,7 +20,22 @@ import { compose, transformTableStateToQuery } from 'utils';
function PaymentMadeList({
// #withPaymentMades
paymentMadesTableState,
// #withPaymentMadeActions
setPaymentMadesTableState
}) {
// Resets the invoices table state once the page unmount.
React.useEffect(
() => () => {
setPaymentMadesTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setPaymentMadesTableState],
);
return (
<PaymentMadesListProvider
query={transformTableStateToQuery(paymentMadesTableState)}
@@ -43,4 +59,5 @@ export default compose(
withPaymentMades(({ paymentMadesTableState }) => ({
paymentMadesTableState,
})),
withPaymentMadeActions
)(PaymentMadeList);

View File

@@ -2,10 +2,10 @@ import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useResourceViews,
useResourceFields,
usePaymentMades,
useResourceMeta
} from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const PaymentMadesListContext = createContext();
@@ -21,9 +21,10 @@ function PaymentMadesListProvider({ query, ...props }) {
// Fetch the accounts resource fields.
const {
data: paymentMadesFields,
isLoading: isFieldsLoading,
} = useResourceFields('bill_payments');
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('bill_payments');
// Fetch accounts list according to the given custom view id.
const {
@@ -45,19 +46,22 @@ function PaymentMadesListProvider({ query, ...props }) {
paymentMades,
pagination,
filterMeta,
paymentMadesFields,
paymentMadesViews,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
resourceMeta,
isResourceMetaLoading,
isResourceMetaFetching,
isPaymentsLoading,
isPaymentsFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceMetaLoading}
name={'payment-mades-list'}
>
<PaymentMadesListContext.Provider value={provider} {...props} />

View File

@@ -9,20 +9,20 @@ const PaymentMadesContext = createContext();
*/
function PaymentMadesProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: paymentsViews, isFetching: isViewsLoading } = useResourceViews(
const { data: paymentsViews, isLoading: isViewsLoading } = useResourceViews(
'bill_payments',
);
// Fetch the accounts resource fields.
const {
data: paymentsFields,
isFetching: isFieldsLoading,
isLoading: isFieldsLoading,
} = useResourceFields('bill_payments');
// Fetch accounts list according to the given custom view id.
const {
data: { paymentMades, pagination },
isFetching: isPaymentsLoading,
isLoading: isPaymentsLoading,
} = usePaymentMades(query);
// Provider payload.
@@ -39,7 +39,7 @@ function PaymentMadesProvider({ query, ...props }) {
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isFieldsLoading || isPaymentsLoading}
name={'payment_made'}
>
<PaymentMadesContext.Provider value={provider} {...props} />

View File

@@ -1,25 +1,27 @@
import React, { useState } from 'react';
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { If, DashboardActionViewsList } from 'components';
import {
AdvancedFilterPopover,
If,
DashboardActionViewsList,
DashboardFilterButton,
} from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withEstimatesActions from './withEstimatesActions';
import withEstimates from './withEstimates';
import { useEstimatesListContext } from './EstimatesListProvider';
import { useRefreshEstimates } from 'hooks/query/estimates';
@@ -31,13 +33,14 @@ import { compose } from 'utils';
function EstimateActionsBar({
// #withEstimateActions
setEstimatesTableState,
// #withEstimates
estimatesFilterRoles
}) {
const history = useHistory();
const [filterCount, setFilterCount] = useState(0);
// Estimates list context.
const { estimatesViews } = useEstimatesListContext();
const { estimatesViews, fields } = useEstimatesListContext();
// Handle click a new sale estimate.
const onClickNewEstimate = () => {
@@ -55,9 +58,7 @@ function EstimateActionsBar({
};
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => {
refresh();
};
const handleRefreshBtnClick = () => { refresh(); };
return (
<DashboardActionsBar>
@@ -74,23 +75,21 @@ function EstimateActionsBar({
text={<T id={'new_estimate'} />}
onClick={onClickNewEstimate}
/>
<Popover
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: estimatesFilterRoles,
defaultFieldKey: 'estimate_number',
fields: fields,
onFilterChange: (filterConditions) => {
setEstimatesTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${intl.get('filters_applied')}`
)
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
<DashboardFilterButton
conditionsCount={estimatesFilterRoles.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
@@ -128,4 +127,9 @@ function EstimateActionsBar({
);
}
export default compose(withEstimatesActions)(EstimateActionsBar);
export default compose(
withEstimatesActions,
withEstimates(({ estimatesTableState }) => ({
estimatesFilterRoles: estimatesTableState.filterRoles,
})),
)(EstimateActionsBar);

View File

@@ -9,6 +9,7 @@ import EstimatesViewTabs from './EstimatesViewTabs';
import EstimatesDataTable from './EstimatesDataTable';
import withEstimates from './withEstimates';
import withEstimatesActions from './withEstimatesActions';
import { EstimatesListProvider } from './EstimatesListProvider';
import { compose, transformTableStateToQuery } from 'utils';
@@ -19,7 +20,22 @@ import { compose, transformTableStateToQuery } from 'utils';
function EstimatesList({
// #withEstimate
estimatesTableState,
// #withEstimatesActions
setEstimatesTableState
}) {
// Resets the estimates table state once the page unmount.
React.useEffect(
() => () => {
setEstimatesTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setEstimatesTableState],
);
return (
<EstimatesListProvider
query={transformTableStateToQuery(estimatesTableState)}
@@ -41,4 +57,5 @@ function EstimatesList({
export default compose(
withEstimates(({ estimatesTableState }) => ({ estimatesTableState })),
withEstimatesActions
)(EstimatesList);

View File

@@ -1,7 +1,9 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useEstimates } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceViews, useResourceMeta, useEstimates } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const EstimatesListContext = createContext();
@@ -10,17 +12,17 @@ const EstimatesListContext = createContext();
*/
function EstimatesListProvider({ query, ...props }) {
// Fetches estimates resource views and fields.
const { data: estimatesViews, isLoading: isViewsLoading } = useResourceViews(
'sale_estimates',
);
const { data: estimatesViews, isLoading: isViewsLoading } =
useResourceViews('sale_estimates');
// Fetches the estimates resource fields.
const {
data: estimatesFields,
isLoading: isFieldsLoading,
} = useResourceFields('sale_estimates');
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('sale_estimates');
// Fetch estimates list according to the given custom view id.
// Fetches estimates list according to the given custom view id.
const {
data: { estimates, pagination, filterMeta },
isLoading: isEstimatesLoading,
@@ -39,12 +41,15 @@ function EstimatesListProvider({ query, ...props }) {
const provider = {
estimates,
pagination,
estimatesFields,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
estimatesViews,
isResourceLoading,
isResourceFetching,
isEstimatesLoading,
isEstimatesFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus,
@@ -52,7 +57,7 @@ function EstimatesListProvider({ query, ...props }) {
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceLoading}
name={'sale_estimate'}
>
<EstimatesListContext.Provider value={provider} {...props} />

View File

@@ -1,21 +1,19 @@
import React, { useState } from 'react';
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import {
FormattedMessage as T,
AdvancedFilterPopover,
DashboardFilterButton,
} from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -23,7 +21,9 @@ import { If, DashboardActionViewsList } from 'components';
import { useRefreshInvoices } from 'hooks/query/invoices';
import { useInvoicesListContext } from './InvoicesListProvider';
import withInvoiceActions from './withInvoiceActions';
import withInvoices from './withInvoices';
import { compose } from 'utils';
@@ -33,13 +33,14 @@ import { compose } from 'utils';
function InvoiceActionsBar({
// #withInvoiceActions
setInvoicesTableState,
// #withInvoices
invoicesFilterRoles,
}) {
const history = useHistory();
const [filterCount, setFilterCount] = useState(0);
// Sale invoices list context.
const { invoicesViews } = useInvoicesListContext();
const { invoicesViews, invoicesFields } = useInvoicesListContext();
// Handle new invoice button click.
const handleClickNewInvoice = () => {
@@ -74,31 +75,27 @@ function InvoiceActionsBar({
text={<T id={'new_invoice'} />}
onClick={handleClickNewInvoice}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: invoicesFilterRoles,
defaultFieldKey: 'invoice_no',
fields: invoicesFields,
onFilterChange: (filterConditions) => {
setInvoicesTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL)}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${intl.get('filters_applied')}`
)
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
/>
</Popover>
<DashboardFilterButton conditionsCount={invoicesFilterRoles.length} />
</AdvancedFilterPopover>
<NavbarDivider />
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'trash-16'} iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
// onClick={handleBulkDelete}
/>
</If>
<Button
@@ -128,4 +125,9 @@ function InvoiceActionsBar({
);
}
export default compose(withInvoiceActions)(InvoiceActionsBar);
export default compose(
withInvoiceActions,
withInvoices(({ invoicesTableState }) => ({
invoicesFilterRoles: invoicesTableState.filterRoles,
})),
)(InvoiceActionsBar);

View File

@@ -11,6 +11,7 @@ import InvoicesDataTable from './InvoicesDataTable';
import InvoicesAlerts from '../InvoicesAlerts';
import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { transformTableStateToQuery, compose } from 'utils';
@@ -21,7 +22,22 @@ import { transformTableStateToQuery, compose } from 'utils';
function InvoicesList({
// #withInvoice
invoicesTableState,
// #withInvoicesActions
setInvoicesTableState
}) {
// Resets the invoices table state once the page unmount.
React.useEffect(
() => () => {
setInvoicesTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setInvoicesTableState],
);
return (
<InvoicesListProvider
query={transformTableStateToQuery(invoicesTableState)}
@@ -43,5 +59,6 @@ function InvoicesList({
export default compose(
withInvoices(({ invoicesTableState }) => ({ invoicesTableState })),
withInvoiceActions,
withAlertsActions,
)(InvoicesList);

View File

@@ -1,7 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useInvoices } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceViews, useResourceMeta, useInvoices } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const InvoicesListContext = createContext();
@@ -10,15 +10,15 @@ const InvoicesListContext = createContext();
*/
function InvoicesListProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: invoicesViews, isLoading: isViewsLoading } = useResourceViews(
'sale_invoices',
);
const { data: invoicesViews, isLoading: isViewsLoading } =
useResourceViews('sale_invoices');
// Fetch the accounts resource fields.
const {
data: invoicesFields,
isLoading: isFieldsLoading,
} = useResourceFields('sale_invoices');
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('sale_invoices');
// Fetch accounts list according to the given custom view id.
const {
@@ -39,20 +39,22 @@ function InvoicesListProvider({ query, ...props }) {
const provider = {
invoices,
pagination,
invoicesFields,
invoicesFields: getFieldsFromResourceMeta(resourceMeta.fields),
invoicesViews,
isInvoicesLoading,
isInvoicesFetching,
isFieldsLoading,
isResourceFetching,
isResourceLoading,
isViewsLoading,
isEmptyStatus
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceLoading}
name={'sales-invoices-list'}
>
<InvoicesListContext.Provider value={provider} {...props} />

View File

@@ -2,10 +2,10 @@ import React, { createContext, useContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useResourceViews,
useResourceFields,
useResourceMeta,
usePaymentReceives,
} from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const PaymentReceivesListContext = createContext();
@@ -21,9 +21,10 @@ function PaymentReceivesListProvider({ query, ...props }) {
// Fetch the payment receives resource fields.
const {
data: paymentReceivesFields,
isFetching: isFieldsLoading,
} = useResourceFields('payment_receives');
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('payment_receives');
// Fetch payment receives list according to the given custom view id.
const {
@@ -44,19 +45,23 @@ function PaymentReceivesListProvider({ query, ...props }) {
const state = {
paymentReceives,
pagination,
paymentReceivesFields,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
paymentReceivesViews,
isPaymentReceivesLoading,
isPaymentReceivesFetching,
isFieldsLoading,
isResourceFetching,
isResourceLoading,
isViewsLoading,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceLoading}
name={'payment-receives-list'}
>
<PaymentReceivesListContext.Provider value={state} {...props} />

View File

@@ -3,18 +3,18 @@ import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import {
DashboardFilterButton,
AdvancedFilterPopover,
FormattedMessage as T,
} from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -33,12 +33,15 @@ import { useRefreshPaymentReceive } from 'hooks/query/paymentReceives';
function PaymentReceiveActionsBar({
// #withPaymentReceivesActions
setPaymentReceivesTableState,
// #withPaymentReceives
paymentFilterConditions
}) {
// History context.
const history = useHistory();
// Payment receives list context.
const { paymentReceivesViews } = usePaymentReceivesListContext();
const { paymentReceivesViews, fields } = usePaymentReceivesListContext();
// Handle new payment button click.
const handleClickNewPaymentReceive = () => {
@@ -58,6 +61,8 @@ function PaymentReceiveActionsBar({
refresh();
};
console.log(fields, 'fields');
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -73,18 +78,21 @@ function PaymentReceiveActionsBar({
text={<T id={'new_payment_receive'} />}
onClick={handleClickNewPaymentReceive}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: paymentFilterConditions,
defaultFieldKey: 'payment_receive_no',
fields: fields,
onFilterChange: (filterConditions) => {
setPaymentReceivesTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL)}
text={<T id={'filter'} />}
icon={<Icon icon={'filter-16'} iconSize={16} />}
<DashboardFilterButton
conditionsCount={paymentFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
@@ -125,5 +133,6 @@ export default compose(
withPaymentReceivesActions,
withPaymentReceives(({ paymentReceivesTableState }) => ({
paymentReceivesTableState,
paymentFilterConditions: paymentReceivesTableState.filterRoles,
})),
)(PaymentReceiveActionsBar);

View File

@@ -10,6 +10,7 @@ import PaymentReceiveViewTabs from './PaymentReceiveViewTabs';
import PaymentReceivesTable from './PaymentReceivesTable';
import withPaymentReceives from './withPaymentReceives';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import { compose, transformTableStateToQuery } from 'utils';
@@ -19,7 +20,22 @@ import { compose, transformTableStateToQuery } from 'utils';
function PaymentReceiveList({
// #withPaymentReceives
paymentReceivesTableState,
// #withPaymentReceivesActions
setPaymentReceivesTableState
}) {
// Resets the payment receives table state once the page unmount.
React.useEffect(
() => () => {
setPaymentReceivesTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setPaymentReceivesTableState],
);
return (
<PaymentReceivesListProvider
query={transformTableStateToQuery(paymentReceivesTableState)}
@@ -43,4 +59,5 @@ export default compose(
withPaymentReceives(({ paymentReceivesTableState }) => ({
paymentReceivesTableState,
})),
withPaymentReceivesActions,
)(PaymentReceiveList);

View File

@@ -2,9 +2,10 @@ import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useResourceViews,
useResourceFields,
useResourceMeta,
usePaymentReceives,
} from 'hooks/query';
import { getFieldsFromResourceMeta } from 'utils';
const PaymentReceivesListContext = createContext();
@@ -20,9 +21,10 @@ function PaymentReceivesListProvider({ query, ...props }) {
// Fetch the accounts resource fields.
const {
data: paymentReceivesFields,
isFetching: isFieldsLoading,
} = useResourceFields('payment_receives');
data: resourceMeta,
isFetching: isResourceFetching,
isLoading: isResourceLoading,
} = useResourceMeta('payment_receives');
// Fetch accounts list according to the given custom view id.
const {
@@ -35,18 +37,21 @@ function PaymentReceivesListProvider({ query, ...props }) {
const provider = {
paymentReceives,
paymentReceivesViews,
paymentReceivesFields,
pagination,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isViewsLoading,
isFieldsLoading,
isResourceFetching,
isResourceLoading,
isPaymentReceivesLoading,
isPaymentReceivesFetching
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
loading={isViewsLoading || isResourceLoading}
name={'payment_receives'}
>
<PaymentReceivesListContext.Provider value={provider} {...props} />

View File

@@ -3,24 +3,25 @@ import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
Alignment,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import {
AdvancedFilterPopover,
DashboardFilterButton,
FormattedMessage as T,
} from 'components';
import { If, DashboardActionViewsList } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withReceiptsActions from './withReceiptsActions';
import withReceipts from './withReceipts';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { useRefreshReceipts } from 'hooks/query/receipts';
import { compose } from 'utils';
@@ -31,13 +32,14 @@ import { compose } from 'utils';
function ReceiptActionsBar({
// #withReceiptsActions
setReceiptsTableState,
// #withReceipts
receiptsFilterConditions,
}) {
const history = useHistory();
const [filterCount, setFilterCount] = useState(0);
// Sale receipts list context.
const { receiptsViews } = useReceiptsListContext();
const { receiptsViews, fields } = useReceiptsListContext();
// Handle new receipt button click.
const onClickNewReceipt = () => {
@@ -54,9 +56,9 @@ function ReceiptActionsBar({
};
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => {
refresh();
};
const handleRefreshBtnClick = () => { refresh(); };
console.log(receiptsFilterConditions, fields, 'XXX');
return (
<DashboardActionsBar>
@@ -74,24 +76,21 @@ function ReceiptActionsBar({
text={<T id={'new_receipt'} />}
onClick={onClickNewReceipt}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: receiptsFilterConditions,
defaultFieldKey: 'reference_no',
fields: fields,
onFilterChange: (filterConditions) => {
setReceiptsTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${intl.get('filters_applied')}`
)
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
<DashboardFilterButton
conditionsCount={receiptsFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
@@ -128,4 +127,9 @@ function ReceiptActionsBar({
);
}
export default compose(withReceiptsActions)(ReceiptActionsBar);
export default compose(
withReceiptsActions,
withReceipts(({ receiptTableState }) => ({
receiptsFilterConditions: receiptTableState.filterRoles,
})),
)(ReceiptActionsBar);

View File

@@ -9,6 +9,7 @@ import ReceiptsAlerts from '../ReceiptsAlerts';
import ReceiptsTable from './ReceiptsTable';
import withReceipts from './withReceipts';
import withReceiptsActions from './withReceiptsActions';
import { ReceiptsListProvider } from './ReceiptsListProvider';
import { transformTableStateToQuery, compose } from 'utils';
@@ -19,7 +20,22 @@ import { transformTableStateToQuery, compose } from 'utils';
function ReceiptsList({
// #withReceipts
receiptTableState,
// #withReceiptsActions
setReceiptsTableState,
}) {
// Resets the receipts table state once the page unmount.
React.useEffect(
() => () => {
setReceiptsTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setReceiptsTableState],
);
return (
<ReceiptsListProvider query={transformTableStateToQuery(receiptTableState)}>
<DashboardPageContent>
@@ -43,4 +59,5 @@ export default compose(
withReceipts(({ receiptTableState }) => ({
receiptTableState,
})),
withReceiptsActions,
)(ReceiptsList);

View File

@@ -1,29 +1,30 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useReceipts } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceMeta, useResourceViews, useReceipts } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
const ReceiptsListContext = createContext();
// Receipts list provider.
function ReceiptsListProvider({ query, ...props }) {
// Fetch receipts resource views and fields.
const { data: receiptsViews, isLoading: isViewsLoading } = useResourceViews(
'sale_receipt',
);
const { data: receiptsViews, isLoading: isViewsLoading } =
useResourceViews('sale_receipt');
// Fetches the sale receipts resource fields.
// const {
// data: receiptsFields,
// isFetching: isFieldsLoading,
// } = useResourceFields('sale_receipt');
const {
data: resourceMeta,
isFetching: isResourceFetching,
isLoading: isResourceLoading,
} = useResourceMeta('sale_receipt');
const {
data: { receipts, pagination, filterMeta },
isLoading: isReceiptsLoading,
isFetching: isReceiptsFetching,
} = useReceipts(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
@@ -35,20 +36,22 @@ function ReceiptsListProvider({ query, ...props }) {
const provider = {
receipts,
pagination,
// receiptsFields,
receiptsViews,
isViewsLoading,
// isFieldsLoading,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isResourceFetching,
isResourceLoading,
isReceiptsLoading,
isReceiptsFetching,
isEmptyStatus
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading}
name={'sales_receipts'}
>
<DashboardInsider loading={isViewsLoading} name={'sales_receipts'}>
<ReceiptsListContext.Provider value={provider} {...props} />
</DashboardInsider>
);

View File

@@ -5,19 +5,19 @@ import {
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
Switch,
Alignment,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import Icon from 'components/Icon';
import { If, DashboardActionViewsList } from 'components';
import {
If,
DashboardActionViewsList,
DashboardFilterButton,
AdvancedFilterPopover,
} from 'components';
import { useRefreshVendors } from 'hooks/query/vendors';
import { useVendorsListContext } from './VendorsListProvider';
@@ -32,6 +32,9 @@ import { compose } from 'utils';
* Vendors actions bar.
*/
function VendorActionsBar({
// #withVendors
vendorsFilterConditions,
// #withVendorActions
setVendorsTableState,
vendorsInactiveMode,
@@ -39,7 +42,7 @@ function VendorActionsBar({
const history = useHistory();
// Vendors list context.
const { vendorsViews } = useVendorsListContext();
const { vendorsViews, fields } = useVendorsListContext();
// Handles new vendor button click.
const onClickNewVendor = () => {
@@ -81,19 +84,21 @@ function VendorActionsBar({
onClick={onClickNewVendor}
/>
<NavbarDivider />
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
<AdvancedFilterPopover
advancedFilterProps={{
conditions: vendorsFilterConditions,
defaultFieldKey: 'display_name',
fields: fields,
onFilterChange: (filterConditions) => {
setVendorsTableState({ filterRoles: filterConditions });
},
}}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
true ? <T id={'filter'} /> : `${9} ${intl.get('filters_applied')}`
}
icon={<Icon icon="filter-16" iconSize={16} />}
<DashboardFilterButton
conditionsCount={vendorsFilterConditions.length}
/>
</Popover>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
@@ -133,5 +138,6 @@ export default compose(
withVendorsActions,
withVendors(({ vendorsTableState }) => ({
vendorsInactiveMode: vendorsTableState.inactiveMode,
vendorsFilterConditions: vendorsTableState.filterRoles,
})),
)(VendorActionsBar);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import 'style/pages/Vendors/List.scss';
@@ -11,6 +11,7 @@ import VendorsAlerts from '../VendorsAlerts';
import VendorsTable from './VendorsTable';
import withVendors from './withVendors';
import withVendorsActions from './withVendorsActions';
import { compose } from 'utils';
@@ -20,7 +21,22 @@ import { compose } from 'utils';
function VendorsList({
// #withVendors
vendorsTableState,
// #withVendorsActions
setVendorsTableState
}) {
// Resets the vendors table state once the page unmount.
useEffect(
() => () => {
setVendorsTableState({
filterRoles: [],
viewSlug: '',
pageIndex: 0,
});
},
[setVendorsTableState],
);
return (
<VendorsListProvider tableState={vendorsTableState}>
<VendorActionsBar />
@@ -40,4 +56,5 @@ function VendorsList({
export default compose(
withVendors(({ vendorsTableState }) => ({ vendorsTableState })),
withVendorsActions
)(VendorsList);

View File

@@ -1,8 +1,8 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useVendors } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
import { useResourceMeta, useResourceViews, useVendors } from 'hooks/query';
import { isTableEmptyStatus, getFieldsFromResourceMeta } from 'utils';
import { transformVendorsStateToQuery } from './utils';
const VendorsListContext = createContext();
@@ -22,6 +22,13 @@ function VendorsListProvider({ tableState, ...props }) {
const { data: vendorsViews, isLoading: isVendorsViewsLoading } =
useResourceViews('vendors');
// Fetch the customers resource fields.
const {
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('customers');
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
@@ -37,15 +44,23 @@ function VendorsListProvider({ tableState, ...props }) {
pagination,
vendorsViews,
isVendorsLoading,
isVendorsFetching,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
resourceMeta,
isResourceMetaLoading,
isResourceMetaFetching,
isVendorsViewsLoading,
isVendorsLoading,
isVendorsFetching,
isEmptyStatus,
};
return (
<DashboardInsider loading={isVendorsViewsLoading} name={'vendors-list'}>
<DashboardInsider
loading={isResourceMetaLoading || isVendorsLoading}
name={'vendors-list'}
>
<VendorsListContext.Provider value={provider} {...props} />
</DashboardInsider>
);

View File

@@ -14,33 +14,21 @@ export function useResourceViews(resourceSlug) {
},
);
}
/**
* Retrieve the resource columns.
* @param {string} resourceSlug - Resource slug.
*/
export function useResourceColumns(resourceSlug) {
return useRequestQuery(
['RESOURCE_COLUMNS', resourceSlug],
{ method: 'get', url: `resources/${resourceSlug}/columns` },
{
defaultData: [],
},
);
}
/**
* Retrieve the resource fields.
* Retrieve the resource meta.
* @param {string} resourceSlug - Resource slug.
*/
export function useResourceFields(resourceSlug, props) {
export function useResourceMeta(resourceSlug, props) {
return useRequestQuery(
['RESOURCE_FIELDS', resourceSlug],
{ method: 'get', url: `resources/${resourceSlug}/fields` },
['RESOURCE_META', resourceSlug],
{ method: 'get', url: `resources/${resourceSlug}/meta` },
{
select: (res) => res.data.resource_fields,
defaultData: [],
select: (res) => res.data.resource_meta,
defaultData: {
fields: {},
},
},
props
props,
);
}
}

View File

@@ -6,10 +6,12 @@ import {
useSetGlobalErrors,
useAuthToken,
} from './state';
import { useAppIntlContext } from '../components/AppIntlProvider';
export default function useApiRequest() {
const setGlobalErrors = useSetGlobalErrors();
const { setLogout } = useAuthActions();
const { currentLocale } = useAppIntlContext();
// Authentication token.
const token = useAuthToken();
@@ -24,7 +26,7 @@ export default function useApiRequest() {
// Request interceptors.
instance.interceptors.request.use(
(request) => {
const locale = 'ar';
const locale = currentLocale;
if (token) {
request.headers.common['X-Access-Token'] = token;

View File

@@ -435,7 +435,7 @@
"once_delete_these_customers_you_will_not_able_restore_them": "Once you delete these customers, you won't be able to retrieve them later. Are you sure you want to delete them?",
"after": "After",
"before": "Before",
"count_filters_applied": "{count} filters applied",
"count_filters_applied": "{count} Filters applied",
"is": "Is",
"is_not": "Is Not",
"create_a_new_view": "Create a new view",
@@ -1194,5 +1194,8 @@
"are_sure_to_activate_this_contact": "Are you sure you want to activate this contact ? You will be able to inactivate it later",
"publish_adjustment": "Publish adjustment",
"inactivate_customer": "Inactivate customer",
"activate_customer": "Activate customer"
"activate_customer": "Activate customer",
"filter.all_filters_must_match": "Atleast one filter must match",
"filter.atleast_one_filter_must_match": "Atleast one filter must match",
"filter.when": "When"
}

View File

@@ -8,6 +8,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: []
},
};
@@ -15,7 +16,7 @@ const STORAGE_KEY = 'bigcapital:bills';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -10,6 +10,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: [],
},
};
@@ -17,7 +18,7 @@ const STORAGE_KEY = 'bigcapital:estimates';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -10,6 +10,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: []
},
};
@@ -17,7 +18,7 @@ const STORAGE_KEY = 'bigcapital:invoices';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -10,6 +10,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: [],
sortBy: [],
},
};

View File

@@ -10,6 +10,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: [],
},
};
@@ -17,7 +18,7 @@ const STORAGE_KEY = 'bigcapital:paymentReceives';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -5,14 +5,18 @@ import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {},
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: [],
},
};
const STORAGE_KEY = 'bigcapital:accounts';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -8,6 +8,7 @@ const initialState = {
pageSize: 12,
pageIndex: 0,
inactiveMode: false,
filterRoles: []
},
};
@@ -20,7 +21,7 @@ const STORAGE_KEY = 'bigcapital:estimates';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
},
reducerInstance,

View File

@@ -8,6 +8,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: [],
},
};
@@ -15,7 +16,7 @@ const STORAGE_KEY = 'bigcapital:expenses';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -8,14 +8,16 @@ import t from 'store/types';
// Initial state.
const initialState = {
tableState: {},
tableState: {
filterRoles: []
},
};
const STORAGE_KEY = 'bigcapital:itemCategories';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -2,6 +2,9 @@ import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
const initialState = {
tableState: {
filterRoles: [],
},
categories: {},
loading: false,
};

View File

@@ -8,7 +8,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filters: [],
filterRoles: [],
inactiveMode: false,
},
selectedRows: [],

View File

@@ -8,6 +8,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: [],
},
};
@@ -15,7 +16,7 @@ const STORAGE_KEY = 'bigcapital:manualJournals';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -10,6 +10,7 @@ const initialState = {
tableState: {
pageSize: 12,
pageIndex: 0,
filterRoles: []
},
};
@@ -17,7 +18,7 @@ const STORAGE_KEY = 'bigcapital:receipts';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -9,6 +9,7 @@ const initialState = {
pageSize: 12,
pageIndex: 0,
inactiveMode: false,
filterRoles: [],
},
};
@@ -16,7 +17,7 @@ const STORAGE_KEY = 'bigcapital:vendors';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
whitelist: [],
storage,
};

View File

@@ -7,6 +7,10 @@
padding-right: 12px;
}
.bp3-button:not([class*='bp3-intent-']) {
color: #33304a;
}
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
color: #555555;
box-shadow: 0 0 0 transparent;

View File

@@ -1,121 +1,115 @@
.filter-dropdown{
width: 550px;
width: 600px;
&__body{
padding: 12px;
&__form{
}
&__conditions-wrap{
}
&__conditions{
padding: 6px 0
}
&__condition{
display: flex;
padding: 6px 12px;
&:not(:first-of-type) {
padding-top: 8px;
border-top: 1px solid #e6e6e6;
margin-top: 8px;
}
}
.bp3-form-group{
padding-right: 4px;
margin-bottom: 0;
&:not(:last-of-type) {
padding-right: 8px;
}
.bp3-html-select select,
.bp3-select select{
padding: 0 15px 0 4px;
&:after{
margin-right: 8px;
}
}
.bp3-input{
padding: 0 6px;
}
.bp3-html-select select,
.bp3-select select,
.bp3-input-group .bp3-input{
height: 32px;
border-radius: 3px;
border-color: #dbd8d8;
}
.form-group--select-list .bp3-popover-target .bp3-button{
padding-left: 6px;
}
.bp3-html-select::after,
.form-group--select-list .bp3-button::after{
border-top-color: #aaa;
margin-right: 8px;
margin-right: 6px;
border-top-color: #c5c5c5;
}
.bp3-html-select,
.bp3-input,
.form-group--select-list .bp3-button{
&:not(:focus){
border-color: #d3d9de;
}
}
.form-group--select-list .bp3-button{
padding-right: 20px;
}
.bp3-form-group{
margin-bottom: 0;
padding-right: 10px;
.bp3-control.bp3-checkbox{
margin-left: 20px;
}
.bp3-popover-wrapper{
width: 100%;
}
.bp3-button{
text-overflow: ellipsis;
overflow: visible;
white-space: nowrap;
display: block;
overflow: hidden;
}
}
.form-group--condition{
width: 65px;
input[disabled] {
background: transparent;
border: 0;
}
}
.form-group--comparator{
width: 120px;
}
.form-group--fieldKey{
width: 145px;
}
.form-group--value{
width: 220px;
}
.button--remove{
margin-left: -6px;
.bp3-icon{
color: #929aa0;
}
}
}
&__footer{
border-top: 1px solid #e8e8e8;
padding: 5px 10px;
}
.form-group{
&--condition{
width: 65px;
min-width: 65px;
}
&--field{
width: 40%;
}
&--comparator{
min-width: 100px;
width: 100px;
max-width: 100px;
}
&--value{
width: 50%;
.bp3-control{
display: inline-block;
margin-left: 1.8rem;
}
}
padding: 10px 12px;
margin-top: -6px;
}
}
.list-select--filter-dropdown{
.bp3-popover{
.bp3-button:not([class*="bp3-intent-"]):not(.bp3-minimal),
.bp3-button:not([class*="bp3-intent-"]):not(.bp3-minimal){
&,
&:hover{
background-color: #E6EFFB;
border: 0;
border-radius: 3px;
}
&:after{
border-top-color: #afb9d0;
}
&,
& .bp3-popover-content{
border-radius: 5px;
}
}
.popover--list-select-filter-dropdown{
.bp3-popover-content{
max-width: 200px;
.bp3-menu-item{
.text-hint{
font-size: 11px;
line-height: 1.3;
display: block;
color: #5c7080;
}
.bp3-menu{
max-height: 250px;
overflow: auto;
}
.bp3-input-group{
margin: 8px 8px 0;
padding-bottom: 4px;
.bp3-input:not(:first-child){
padding-left: 10px;
}
.bp3-icon-search{
display: none;
}
}
}

View File

@@ -512,6 +512,10 @@ export function getPagesCountFromPaginationMeta(pagination) {
return Math.ceil(total / pageSize);
}
function transformFilterRoles(filterRoles) {
return JSON.stringify(filterRoles);
}
/**
* Transformes the table state to url query.
*/
@@ -521,6 +525,13 @@ export function transformTableStateToQuery(tableState) {
const query = {
pageSize,
page: pageIndex + 1,
...(tableState.filterRoles
? {
stringified_filter_roles: transformFilterRoles(
tableState.filterRoles,
),
}
: {}),
...(viewSlug ? { viewSlug } : {}),
...(Array.isArray(sortBy) && sortBy.length > 0
? {
@@ -691,10 +702,14 @@ export function nestedArrayToflatten(
parseItem = (a, level) => a,
level = 1,
) {
const parseObject = (obj) => parseItem({
..._.omit(obj, [property]),
level,
}, level);
const parseObject = (obj) =>
parseItem(
{
..._.omit(obj, [property]),
level,
},
level,
);
return collection.reduce((items, currentValue, index) => {
let localItems = [...items];
@@ -713,3 +728,27 @@ export function nestedArrayToflatten(
return localItems;
}, []);
}
export function getFieldsFromResourceMeta(resourceFields) {
const fields = Object.keys(resourceFields)
.map((fieldKey) => {
const field = resourceFields[fieldKey];
return {
...transformToCamelCase(field),
key: fieldKey,
};
})
.filter((field) => field.filterable !== false);
return _.orderBy(fields, ['label']);
}
export function getFilterableFieldsFromFields(fields) {
return fields.filter((field) => field.filterable !== false);
}
export const RESORUCE_TYPE = {
ACCOUNTS: 'account',
ITEMS: 'items',
}

View File

@@ -16,7 +16,7 @@ TENANT_DB_CLIENT=mysql
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
TENANT_DB_HOST=127.0.0.1
TENANT_DB_PASSWORD=root
TEANNT_DB_USER=root
TENANT_DB_USER=root
TENANT_DB_CHARSET=utf8
TENANT_MIGRATIONS_DIR=src/database/migrations
TENANT_SEEDS_DIR=src/database/seeds/core

View File

@@ -1,7 +1,6 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param, query } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import { param } from 'express-validator';
import BaseController from './BaseController';
import { ServiceError } from 'exceptions';
import ResourceService from 'services/Resource/ResourceService';
@@ -19,32 +18,15 @@ export default class ResourceController extends BaseController {
router.get(
'/:resource_model/meta',
[...this.resourceModelParamSchema],
[
param('resource_model').exists().trim().escape()
],
this.asyncMiddleware(this.resourceMeta.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/fields',
[...this.resourceModelParamSchema],
this.validationResult,
asyncMiddleware(this.resourceFields.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/data',
[...this.resourceModelParamSchema],
this.validationResult,
asyncMiddleware(this.resourceData.bind(this)),
this.handleServiceErrors
);
return router;
}
get resourceModelParamSchema() {
return [param('resource_model').exists().trim().escape()];
}
/**
* Retrieve resource model meta.
* @param {Request} req -
@@ -52,7 +34,7 @@ export default class ResourceController extends BaseController {
* @param {NextFunction} next -
* @returns {Response}
*/
private resourceMeta = (
public resourceMeta = (
req: Request,
res: Response,
next: NextFunction
@@ -73,56 +55,6 @@ export default class ResourceController extends BaseController {
}
};
/**
* Retrieve resource fields of the given resource.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
resourceFields(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
try {
// const resourceFields = this.resourcesService.getResourceFields(
// tenantId,
// resourceModel
// );
return res.status(200).send({
resource_fields: [],
});
} catch (error) {
next(error);
}
}
/**
* Retrieve resource data of the give resource based on the given query.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async resourceData(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
const filter = req.query;
try {
const resourceData = await this.resourcesService.getResourceData(
tenantId,
resourceModel,
filter
);
return res.status(200).send({
resource_data: this.transfromToResponse(resourceData),
});
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param {Error} error

View File

@@ -23,6 +23,7 @@ export default {
column: 'slug',
fieldType: 'text',
columnable: false,
filterable: false,
},
code: {
name: 'Account code',
@@ -76,6 +77,7 @@ export default {
name: 'Currency',
column: 'currency_code',
fieldType: 'text',
filterable: false,
},
created_at: {
name: 'Created at',

View File

@@ -48,12 +48,12 @@ export default {
fieldType: 'enumeration',
columnable: true,
options: [
{ name: 'Paid', key: 'paid' },
{ name: 'Partially paid', key: 'partially-paid' },
{ name: 'Overdue', key: 'overdue' },
{ name: 'Unpaid', key: 'unpaid' },
{ name: 'Opened', key: 'opened' },
{ name: 'Draft', key: 'draft' },
{ label: 'Paid', key: 'paid' },
{ label: 'Partially paid', key: 'partially-paid' },
{ label: 'Overdue', key: 'overdue' },
{ label: 'Unpaid', key: 'unpaid' },
{ label: 'Opened', key: 'opened' },
{ label: 'Draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,

View File

@@ -43,6 +43,7 @@ export default {
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'Balance',
@@ -61,11 +62,13 @@ export default {
fieldType: 'date',
},
currency_code: {
name: 'Curreny',
column: 'currency_code',
fieldType: 'text',
},
status: {
label: 'Status',
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },

View File

@@ -48,8 +48,8 @@ export default {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'published', name: 'Published' },
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'Published' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,

View File

@@ -6,23 +6,23 @@ export default {
},
fields: {
name: {
label: 'Name',
name: 'Name',
column: 'name',
fieldType: 'text',
},
description: {
label: 'Description',
name: 'Description',
column: 'description',
fieldType: 'text',
},
count: {
label: 'Count',
name: 'Count',
column: 'count',
fieldType: 'number',
virtualColumn: true,
},
created_at: {
label: 'Created at',
name: 'Created at',
column: 'created_at',
columnType: 'date',
},

View File

@@ -6,43 +6,47 @@ export default {
},
fields: {
'date': {
label: 'Date',
name: 'Date',
column: 'date',
fieldType: 'date',
},
'journal_number': {
label: 'Journal number',
name: 'Journal number',
column: 'journal_number',
fieldType: 'text',
},
'reference': {
label: 'Reference No.',
name: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'journal_type': {
label: 'Journal type',
name: 'Journal type',
column: 'journal_type',
fieldType: 'text',
},
'amount': {
label: 'Amount',
name: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
'description': {
label: 'Description',
name: 'Description',
column: 'description',
fieldType: 'text',
},
'status': {
label: 'Status',
name: 'Status',
column: 'status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'published' }
],
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
label: 'Created at',
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},

View File

@@ -26,8 +26,8 @@ export default (Model) =>
* @param {string} key
* @returns
*/
public static getMeta(key: string) {
return get(this.meta, key);
public static getMeta(key?: string) {
return key ? get(this.meta, key): this.meta;
}
/**

View File

@@ -55,11 +55,11 @@ export default {
name: 'Status',
fieldType: 'enumeration',
options: [
{ name: 'Delivered', key: 'delivered' },
{ name: 'Rejected', key: 'rejected' },
{ name: 'Approved', key: 'approved' },
{ name: 'Delivered', key: 'delivered' },
{ name: 'Draft', key: 'draft' },
{ label: 'Delivered', key: 'delivered' },
{ label: 'Rejected', key: 'rejected' },
{ label: 'Approved', key: 'approved' },
{ label: 'Delivered', key: 'delivered' },
{ label: 'Draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,

View File

@@ -64,15 +64,14 @@ export default {
},
status: {
name: 'Status',
columnable: true,
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'delivered', name: 'Delivered' },
{ key: 'unpaid', name: 'Unpaid' },
{ key: 'overdue', name: 'Overdue' },
{ key: 'partially-paid', name: 'Partially paid' },
{ key: 'paid', name: 'Paid' },
{ key: 'draft', label: 'Draft' },
{ key: 'delivered', label: 'Delivered' },
{ key: 'unpaid', label: 'Unpaid' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'partially-paid', label: 'Partially paid' },
{ key: 'paid', label: 'Paid' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
@@ -97,4 +96,4 @@ function StatusFieldFilterQuery(query, role) {
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}
}

View File

@@ -22,8 +22,8 @@ export default {
relationEntityKey: 'slug',
},
'customer': {
name: 'Customer',
column: 'customer_id',
label: 'Customer',
name: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
@@ -67,8 +67,8 @@ export default {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'closed', name: 'Closed' },
{ key: 'draft', label: 'Draft' },
{ key: 'closed', label: 'Closed' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,

View File

@@ -71,7 +71,8 @@ export default {
fieldType: 'text',
},
'status': {
label: 'Status',
name: 'Status',
type: 'enumeration',
options: [
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },

View File

@@ -3,9 +3,6 @@ import { camelCase, upperFirst } from 'lodash';
import pluralize from 'pluralize';
import { buildFilter } from 'objection-filter';
import { IModel, IModelMeta } from 'interfaces';
import {
getModelFields,
} from 'lib/ViewRolesBuilder'
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
@@ -20,62 +17,12 @@ export default class ResourceService {
/**
* Transform resource to model name.
* @param {string} resourceName
* @param {string} resourceName
*/
private resourceToModelName(resourceName: string): string {
return upperFirst(camelCase(pluralize.singular(resourceName)));
}
/**
* Retrieve model fields.
* @param {number} tenantId
* @param {IModel} Model
*/
private getModelFields(tenantId: number, Model: IModel) {
const { __ } = this.tenancy.i18n(tenantId);
const fields = getModelFields(Model);
return fields.map((field) => ({
label: __(field.label),
key: field.key,
dataType: field.columnType,
fieldType: field.fieldType,
...(field.options) ? {
options: field.options.map((option) => ({
...option, label: __(option.label),
})),
} : {},
...(field.optionsResource) ? {
optionsResource: field.optionsResource,
optionsKey: field.optionsKey,
optionsLabel: field.optionsLabel,
} : {},
}));
}
/**
* Should model be resource-able or throw service error.
* @param {IModel} model
*/
private shouldModelBeResourceable(model: IModel) {
if (!model.resourceable) {
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
}
}
/**
* Retrieve resource fields from resource model name.
* @param {string} resourceName
*/
public getResourceFields(tenantId: number, modelName: string) {
const resourceModel = this.getResourceModel(tenantId, modelName);
this.shouldModelBeResourceable(resourceModel);
return this.getModelFields(tenantId, resourceModel);
}
/**
* Retrieve resource model object.
* @param {number} tenantId -
@@ -91,29 +38,18 @@ export default class ResourceService {
return Models[modelName];
}
/**
* Retrieve resource data from the storage based on the given query.
* @param {number} tenantId
* @param {string} modelName
*/
public async getResourceData(tenantId: number, modelName: string, filter: any) {
const resourceModel = this.getResourceModel(tenantId, modelName);
this.shouldModelBeResourceable(resourceModel);
return buildFilter(resourceModel).build(filter);
}
/**
* Retrieve the resource meta.
* @param {number} tenantId
* @param {string} modelName
* @param {number} tenantId
* @param {string} modelName
* @returns {IModelMeta}
*/
public getResourceMeta(tenantId: number, modelName: string): IModelMeta {
public getResourceMeta(
tenantId: number,
modelName: string,
metakey?: string
): IModelMeta {
const resourceModel = this.getResourceModel(tenantId, modelName);
const settings = resourceModel.meta();
return settings;
return resourceModel.getMeta(metakey);
}
}
}