feat: remove SET_DASHBOARD_REQUEST_LOADING reducer.

feat: fix dropdown filter.
feat: fix fetch resource data.
This commit is contained in:
Ahmed Bouhuolia
2020-10-20 19:58:24 +02:00
parent 00ba1bb75e
commit 322af97d77
51 changed files with 1160 additions and 1009 deletions

View File

@@ -1,13 +0,0 @@
import React from 'react';
import {Dialog, Spinner, Classes} from '@blueprintjs/core';
export default function DialogComponent(props) {
const loadingContent = (
<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>
);
return (
<Dialog {...props}>
{props.isLoading ? loadingContent : props.children}
</Dialog>
);
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Dialog } from '@blueprintjs/core';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function DialogComponent(props) {
const { name, children, closeDialog, onClose } = props;
const handleClose = (event) => {
closeDialog(name)
onClose && onClose(event);
};
return (
<Dialog {...props} onClose={handleClose}>
{ children }
</Dialog>
);
}
export default compose(
withDialogActions,
)(DialogComponent);

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Spinner, Classes } from '@blueprintjs/core';
export default function DialogContent(props) {
const { isLoading, children } = props;
const loadingContent = (
<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>
);
return (
<div>
{isLoading ? loadingContent : children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import React, { Suspense } from 'react';
import { Classes, Spinner } from '@blueprintjs/core';
function LoadingContent() {
return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>);
}
export default function DialogSuspense({
children
}) {
return (
<Suspense fallback={<LoadingContent /> }>
<div className={'dialog__suspense-wrapper'}>
{ children }
</div>
</Suspense>
);
};

View File

@@ -1,17 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import {
isDialogOpenFactory,
getDialogPayloadFactory,
} from 'store/dashboard/dashboard.selectors';
export default (mapState, dialogName) => {
const isDialogOpen = isDialogOpenFactory(dialogName);
const getDialogPayload = getDialogPayloadFactory(dialogName);
export default (mapState) => {
const isDialogOpen = isDialogOpenFactory();
const getDialogPayload = getDialogPayloadFactory();
const mapStateToProps = (state, props) => {
const mapped = {
dialogName,
isOpen: isDialogOpen(state, props),
payload: getDialogPayload(state, props),
};

View File

@@ -1,20 +1,17 @@
import React from 'react';
import React, { lazy } from 'react';
import AccountFormDialog from 'containers/Dialogs/AccountFormDialog';
import UserFormDialog from 'containers/Dialogs/UserFormDialog';
import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
import CurrencyDialog from 'containers/Dialogs/CurrencyDialog';
import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog';
// import UserFormDialog from 'containers/Dialogs/UserFormDialog';
// import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
// import CurrencyDialog from 'containers/Dialogs/CurrencyDialog';
// import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
// import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog';
export default function DialogsContainer() {
return (
<div>
<ExchangeRateDialog />
{/* <InviteUserDialog /> */}
<CurrencyDialog />
<ItemCategoryDialog />
<AccountFormDialog />
{/* <UserFormDialog /> */}
<AccountFormDialog dialogName={'account-form'} />
</div>
);
}

View File

@@ -1,5 +1,6 @@
export const BooleanCompatators = [
{ value: 'is', label_id: 'is' },
{ value: 'is_not', label_id: 'is_not' },
];
export const TextCompatators = [
@@ -20,6 +21,15 @@ export const OptionsCompatators = [
{ value: 'is_not', label_id: 'is_not' },
];
export const NumberCampatators = [
{ value: 'equals', label_id: 'equals' },
{ value: 'not_equal', label_id: 'not_equal' },
{ value: 'bigger_than', label_id: 'bigger_than' },
{ value: 'bigger_or_equals', label_id: 'bigger_or_equals' },
{ value: 'smaller_than', label_id: 'smaller_than' },
{ value: 'smaller_or_equals', label_id: 'smaller_or_equals' },
]
export const getConditionTypeCompatators = (dataType) => {
return [
...(dataType === 'options'
@@ -27,7 +37,9 @@ export const getConditionTypeCompatators = (dataType) => {
: dataType === 'date'
? [...DateCompatators]
: dataType === 'boolean'
? [...BooleanCompatators]
? [...BooleanCompatators]
: dataType === 'number'
? [...NumberCampatators]
: [...TextCompatators]),
];
};

View File

@@ -14,72 +14,80 @@ import { FormattedMessage as T, useIntl } from 'react-intl';
import { debounce } from 'lodash';
import moment from 'moment';
import { If, Choose, ListSelect, MODIFIER } from 'components';
import { Choose, ListSelect, MODIFIER } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withResourceActions from 'containers/Resources/withResourcesActions';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator,
} from './DynamicFilterCompatators';
import { compose, momentFormatter } from 'utils';
/**
* Dynamic filter fields.
*/
function DynamicFilterValueField({
dataType,
value,
initialValue,
error,
// fieldkey,
// resourceKey,
// #withResourceDetail
resourceName,
resourceData,
resourceData = [],
requestResourceData,
// #ownProps
fieldType,
fieldName,
value,
initialValue,
error,
optionsResource,
optionsKey = 'key',
optionsLabel = 'label',
options,
onChange,
rosourceKey,
inputDebounceWait = 500,
inputDebounceWait = 250,
}) {
const { formatMessage } = useIntl();
const [localValue, setLocalValue] = useState();
const fetchResourceData = useQuery(
['resource-data', resourceName && resourceName],
(k, resName) => requestResourceData(resName),
{ manual: true },
);
// 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.name} key={item.id} onClick={handleClick} />;
return (<MenuItem
text={item[optionsLabel]}
key={item[optionsKey]}
onClick={handleClick}
/>);
};
// Handle list button click.
const handleBtnClick = () => {
fetchResourceData.refetch({});
};
const listOptions = useMemo(() => Object.values(resourceData), [
resourceData,
const listOptions = useMemo(() => [
...(resourceData || []),
...(options || []),
], [
resourceData, options,
]);
// Filters accounts types items.
const filterItems = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedTitle = item.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
@@ -89,16 +97,16 @@ function DynamicFilterValueField({
}
};
// Handle list item selected.
const onItemSelect = (item) => {
onChange && onChange(item);
onChange && onChange(item[optionsKey]);
};
const handleInputChangeThrottled = useRef(
debounce((value) => {
onChange && onChange(value);
}, inputDebounceWait),
debounce((value) => { onChange && onChange(value); }, inputDebounceWait),
);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
@@ -108,12 +116,14 @@ function DynamicFilterValueField({
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);
@@ -126,7 +136,7 @@ function DynamicFilterValueField({
return (
<FormGroup className={'form-group--value'}>
<Choose>
<Choose.When condition={dataType === 'options'}>
<Choose.When condition={fieldType === 'options'}>
<ListSelect
className={classNames(
'list-select--filter-dropdown',
@@ -146,14 +156,16 @@ function DynamicFilterValueField({
}}
onItemSelect={onItemSelect}
selectedItem={value}
selectedItemProp={'id'}
defaultText={<T id={'select_account_type'} />}
labelProp={'name'}
buttonProps={{ onClick: handleBtnClick }}
selectedItemProp={optionsKey}
defaultText={`Select an option`}
labelProp={optionsLabel}
buttonProps={{
onClick: handleBtnClick
}}
/>
</Choose.When>
<Choose.When condition={dataType === 'date'}>
<Choose.When condition={fieldType === 'date'}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={transformDateValue(localValue)}
@@ -167,7 +179,7 @@ function DynamicFilterValueField({
/>
</Choose.When>
<Choose.When condition={dataType === 'boolean'}>
<Choose.When condition={fieldType === 'checkbox'}>
<Checkbox value={localValue} onChange={handleCheckboxChange} />
</Choose.When>
@@ -184,7 +196,7 @@ function DynamicFilterValueField({
}
const mapStateToProps = (state, props) => ({
resourceName: props.dataResource,
resourceName: props.optionsResource,
});
const withResourceFilterValueField = connect(mapStateToProps);

View File

@@ -1,5 +1,5 @@
// @flow
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
// @flow
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import {
FormGroup,
Classes,
@@ -10,7 +10,6 @@ import {
import { useFormik } from 'formik';
import { isEqual, last } from 'lodash';
import { usePrevious } from 'react-use';
import { debounce } from 'lodash';
import Icon from 'components/Icon';
import { checkRequiredProperties, uniqueMultiProps } from 'utils';
import { FormattedMessage as T, useIntl } from 'react-intl';
@@ -22,7 +21,7 @@ import Toaster from 'components/AppToaster';
import moment from 'moment';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator
getConditionDefaultCompatator,
} from './DynamicFilter/DynamicFilterCompatators';
let limitToast;
@@ -39,224 +38,238 @@ type InitialCondition = {
export default function FilterDropdown({
fields,
onFilterChange,
refetchDebounceWait = 10,
initialCondition,
initialConditions,
}) {
const { formatMessage } = useIntl();
// Fields key -> metadata table.
const fieldsKeyMapped = useMemo(() =>
new Map(fields.map((field) => [field.key, field])),
[fields]
);
// Conditions options.
const conditionalsOptions = useMemo(
() => [
{ value: '&&', label: formatMessage({ id: 'and' }) },
{ value: '||', label: formatMessage({ id: 'or' }) },
],
[formatMessage],
);
// 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: formatMessage({ id: 'you_reached_conditions_limit' }),
intent: Intent.WARNING,
},
limitToast,
);
} else {
setFieldValue('conditions', [
...values.conditions,
defaultFilterCondition
]);
}
}, [values, setFieldValue, formatMessage, 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>
);
// const { formatMessage } = useIntl();
// const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field]));
// const conditionalsItems = useMemo(
// () => [
// { value: 'and', label: formatMessage({ id: 'and' }) },
// { value: 'or', label: formatMessage({ id: 'or' }) },
// ],
// [formatMessage],
// );
// const resourceFields = useMemo(
// () => [
// ...fields.map((field) => ({
// value: field.key,
// label: field.label_name,
// })),
// ],
// [fields],
// );
// const defaultFilterCondition = useMemo(
// () => ({
// condition: 'and',
// field_key: initialCondition.fieldKey,
// comparator: initialCondition.comparator,
// value: initialCondition.value,
// }),
// [fields],
// );
// const { setFieldValue, getFieldProps, values } = useFormik({
// enableReinitialize: true,
// initialValues: {
// conditions: [defaultFilterCondition],
// },
// });
// const onClickNewFilter = useCallback(() => {
// if (values.conditions.length >= 12) {
// limitToast = Toaster.show(
// {
// message: formatMessage({ id: 'you_reached_conditions_limit' }),
// intent: Intent.WARNING,
// },
// limitToast,
// );
// } else {
// setFieldValue('conditions', [
// ...values.conditions,
// last(values.conditions),
// ]);
// }
// }, [values, defaultFilterCondition, setFieldValue]);
// const filteredFilterConditions = useMemo(() => {
// const requiredProps = ['field_key', 'condition', 'comparator', 'value'];
// const conditions = values.conditions.filter(
// (condition) => !checkRequiredProperties(condition, requiredProps),
// );
// return uniqueMultiProps(conditions, requiredProps);
// }, [values.conditions]);
// const prevConditions = usePrevious(filteredFilterConditions);
// const onFilterChangeThrottled = useRef(
// debounce((conditions) => {
// onFilterChange && onFilterChange(conditions);
// }, refetchDebounceWait),
// );
// useEffect(() => {
// if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
// onFilterChange && onFilterChange(filteredFilterConditions);
// }
// }, [filteredFilterConditions, prevConditions]);
// // 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 === 'field_key') {
// const currentField = fieldsKeyMapped.get(
// values.conditions[index].field_key,
// );
// const nextField = fieldsKeyMapped.get(e.currentTarget.value);
// if (currentField.data_type !== nextField.data_type) {
// setFieldValue(`conditions[${index}].value`, '');
// }
// const comparatorsObs = getConditionTypeCompatators(nextField.data_type);
// const currentCompatator = values.conditions[index].comparator;
// if (!currentCompatator || comparatorsObs.map(c => c.value).indexOf(currentCompatator) === -1) {
// const defaultCompatator = getConditionDefaultCompatator(nextField.data_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.field_key);
// return {
// ...fieldProps(name, index),
// dataType: field.data_type,
// };
// };
// // Value field props.
// const valueFieldProps = (name, index) => {
// const condition = values.conditions[index];
// const field = fieldsKeyMapped.get(condition.field_key);
// return {
// ...fieldProps(name, index),
// dataType: field.data_type,
// resourceKey: field.resource_key,
// options: field.options,
// dataResource: field.data_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={conditionalsItems}
// className={Classes.FILL}
// disabled={index > 1}
// {...fieldProps('condition', index)}
// />
// </FormGroup>
// <FormGroup className={'form-group--field'}>
// <HTMLSelect
// options={resourceFields}
// value={1}
// className={Classes.FILL}
// {...fieldProps('field_key', 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

@@ -0,0 +1,9 @@
import React from 'react';
export default function InputPrepend({ children }) {
return (
<div class="input-prepend">
{ children }
</div>
);
}

View File

@@ -5,7 +5,7 @@ const Bar = ({ progress, animationDuration }) => (
<div
style={{
background: '#79b8ff',
height: 3,
height: 4,
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: 'fixed',

View File

@@ -15,13 +15,15 @@ import Pagination from './Pagination';
import DashboardViewsTabs from './Dashboard/DashboardViewsTabs';
import CurrenciesSelectList from './CurrenciesSelectList';
import FieldRequiredHint from './FieldRequiredHint';
import Dialog from './Dialog';
import AppToaster from './AppToaster';
import DataTable from './DataTable';
import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect';
import LoadingIndicator from './LoadingIndicator';
import DashboardActionViewsList from './Dashboard/DashboardActionViewsList';
import Dialog from './Dialog/Dialog';
import DialogContent from './Dialog/DialogContent';
import DialogSuspense from './Dialog/DialogSuspense';
const Hint = FieldHint;
export {
@@ -43,11 +45,13 @@ export {
DashboardViewsTabs,
CurrenciesSelectList,
FieldRequiredHint,
Dialog,
AppToaster,
DataTable,
AccountsSelectList,
AccountsTypesSelect,
LoadingIndicator,
DashboardActionViewsList,
AppToaster,
Dialog,
DialogContent,
DialogSuspense
};