mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
feat: remove SET_DASHBOARD_REQUEST_LOADING reducer.
feat: fix dropdown filter. feat: fix fetch resource data.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
22
client/src/components/Dialog/Dialog.js
Normal file
22
client/src/components/Dialog/Dialog.js
Normal 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);
|
||||
15
client/src/components/Dialog/DialogContent.js
Normal file
15
client/src/components/Dialog/DialogContent.js
Normal 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>
|
||||
);
|
||||
}
|
||||
18
client/src/components/Dialog/DialogSuspense.js
Normal file
18
client/src/components/Dialog/DialogSuspense.js
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
// );
|
||||
}
|
||||
|
||||
9
client/src/components/Forms/InputPrepend.js
Normal file
9
client/src/components/Forms/InputPrepend.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function InputPrepend({ children }) {
|
||||
return (
|
||||
<div class="input-prepend">
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
client/src/components/Forms/InputPrependOptions.js
Normal file
0
client/src/components/Forms/InputPrependOptions.js
Normal 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',
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -35,6 +35,9 @@ function AccountsActionsBar({
|
||||
// #withAccountsActions
|
||||
addAccountsTableQueries,
|
||||
|
||||
// #withAccounts
|
||||
accountsTableQuery,
|
||||
|
||||
selectedRows = [],
|
||||
onFilterChanged,
|
||||
onBulkDelete,
|
||||
@@ -42,7 +45,9 @@ function AccountsActionsBar({
|
||||
onBulkActivate,
|
||||
onBulkInactive,
|
||||
}) {
|
||||
const [filterCount, setFilterCount] = useState(0);
|
||||
const [filterCount, setFilterCount] = useState(
|
||||
accountsTableQuery?.filter_roles?.length || 0,
|
||||
);
|
||||
|
||||
const onClickNewAccount = () => {
|
||||
openDialog('account-form', {});
|
||||
@@ -54,9 +59,10 @@ function AccountsActionsBar({
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
initialConditions: accountsTableQuery.filter_roles,
|
||||
initialCondition: {
|
||||
fieldKey: 'name',
|
||||
compatator: 'contains',
|
||||
comparator: 'contains',
|
||||
value: '',
|
||||
},
|
||||
onFilterChange: (filterConditions) => {
|
||||
@@ -171,8 +177,9 @@ const withAccountsActionsBar = connect(mapStateToProps);
|
||||
export default compose(
|
||||
withAccountsActionsBar,
|
||||
withDialogActions,
|
||||
withAccounts(({ accountsViews }) => ({
|
||||
withAccounts(({ accountsViews, accountsTableQuery }) => ({
|
||||
accountsViews,
|
||||
accountsTableQuery,
|
||||
})),
|
||||
withResourceDetail(({ resourceFields }) => ({
|
||||
resourceFields,
|
||||
|
||||
@@ -51,11 +51,10 @@ function NormalCell({ cell }) {
|
||||
|
||||
function BalanceCell({ cell }) {
|
||||
const account = cell.row.original;
|
||||
const { balance = null } = account;
|
||||
|
||||
return balance ? (
|
||||
return (account.amount) ? (
|
||||
<span>
|
||||
<Money amount={balance.amount} currency={balance.currency_code} />
|
||||
<Money amount={account.amount} currency={'USD'} />
|
||||
</span>
|
||||
) : (
|
||||
<span class="placeholder">—</span>
|
||||
@@ -245,7 +244,7 @@ function AccountsDataTable({
|
||||
{
|
||||
id: 'balance',
|
||||
Header: formatMessage({ id: 'balance' }),
|
||||
accessor: 'balance',
|
||||
accessor: 'amount',
|
||||
Cell: BalanceCell,
|
||||
width: 150,
|
||||
},
|
||||
@@ -268,11 +267,7 @@ function AccountsDataTable({
|
||||
);
|
||||
|
||||
const selectionColumn = useMemo(
|
||||
() => ({
|
||||
minWidth: 40,
|
||||
width: 40,
|
||||
maxWidth: 40,
|
||||
}),
|
||||
() => ({ minWidth: 45, width: 45, maxWidth: 45 }),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ function CustomerForm({
|
||||
// #withDashboardActions
|
||||
changePageTitle,
|
||||
|
||||
//#withCustomers
|
||||
// #withCustomers
|
||||
customers,
|
||||
|
||||
//#withCustomerDetail
|
||||
// #withCustomerDetail
|
||||
customer,
|
||||
|
||||
//#withCustomersActions
|
||||
// #withCustomersActions
|
||||
requestSubmitCustomer,
|
||||
requestFetchCustomers,
|
||||
requestEditCustomer,
|
||||
@@ -47,7 +47,7 @@ function CustomerForm({
|
||||
requestSubmitMedia,
|
||||
requestDeleteMedia,
|
||||
|
||||
//#Props
|
||||
// #Props
|
||||
onFormSubmit,
|
||||
onCancelForm,
|
||||
}) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RadioGroup, Radio } from '@blueprintjs/core';
|
||||
export default function RadioCustomer(props) {
|
||||
const { onChange, ...rest } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
inline={true}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'utils';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccountDetail from 'containers/Accounts/withAccountDetail';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
dialogName: 'account-form',
|
||||
accountId:
|
||||
props.payload.action === 'edit' && props.payload.id
|
||||
? props.payload.id
|
||||
: null,
|
||||
});
|
||||
const AccountFormDialogConnect = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withDialogRedux(null, 'account-form'),
|
||||
AccountFormDialogConnect,
|
||||
withAccountsActions,
|
||||
withAccountDetail,
|
||||
withAccounts(({ accountsTypes, accountsList }) => ({
|
||||
accountsTypes,
|
||||
accounts: accountsList,
|
||||
})),
|
||||
withDialogActions,
|
||||
);
|
||||
@@ -1,405 +1,45 @@
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Intent,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { useFormik } from 'formik';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { pick, omit } from 'lodash';
|
||||
import { useQuery, queryCache } from 'react-query';
|
||||
import classNames from 'classnames';
|
||||
import Yup from 'services/yup';
|
||||
import {
|
||||
If,
|
||||
ErrorMessage,
|
||||
Dialog,
|
||||
AppToaster,
|
||||
FieldRequiredHint,
|
||||
Hint,
|
||||
AccountsSelectList,
|
||||
AccountsTypesSelect,
|
||||
} from 'components';
|
||||
import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container';
|
||||
import React, { lazy } from 'react';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import { Dialog, DialogSuspense } from 'components';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const AccountFormDialogContent = lazy(() => import('./AccountFormDialogContent'));
|
||||
|
||||
/**
|
||||
* Account form dialog.
|
||||
*/
|
||||
function AccountFormDialog({
|
||||
dialogName,
|
||||
payload = { action: 'new', id: null },
|
||||
payload = { action: '', id: null },
|
||||
isOpen,
|
||||
|
||||
// #withAccounts
|
||||
accountsTypes,
|
||||
accounts,
|
||||
|
||||
// #withAccountDetail
|
||||
account,
|
||||
|
||||
// #withAccountsActions
|
||||
requestFetchAccounts,
|
||||
requestFetchAccountTypes,
|
||||
requestFetchAccount,
|
||||
requestSubmitAccount,
|
||||
requestEditAccount,
|
||||
|
||||
// #withDialog
|
||||
closeDialog,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const validationSchema = Yup.object().shape({
|
||||
name: Yup.string()
|
||||
.required()
|
||||
.min(3)
|
||||
.max(255)
|
||||
.label(formatMessage({ id: 'account_name_' })),
|
||||
code: Yup.string().digits().min(3).max(6),
|
||||
account_type_id: Yup.number()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'account_type_id' })),
|
||||
description: Yup.string().min(3).max(512).nullable().trim(),
|
||||
parent_account_id: Yup.number().nullable(),
|
||||
});
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
account_type_id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const transformApiErrors = (errors) => {
|
||||
const fields = {};
|
||||
if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
|
||||
fields.code = formatMessage({ id: 'account_code_is_not_unique' });
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
// Formik
|
||||
const {
|
||||
errors,
|
||||
values,
|
||||
touched,
|
||||
setFieldValue,
|
||||
resetForm,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
getFieldProps,
|
||||
} = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
...(payload.action === 'edit' &&
|
||||
pick(account, Object.keys(initialValues))),
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: (values, { setSubmitting, setErrors }) => {
|
||||
const form = omit(values, ['subaccount']);
|
||||
const toastAccountName = values.code
|
||||
? `${values.code} - ${values.name}`
|
||||
: values.name;
|
||||
|
||||
const afterSubmit = () => {
|
||||
closeDialog(dialogName);
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
queryCache.invalidateQueries('accounts-list');
|
||||
};
|
||||
const afterErrors = (errors) => {
|
||||
const errorsTransformed = transformApiErrors(errors);
|
||||
setErrors({ ...errorsTransformed });
|
||||
setSubmitting(false);
|
||||
};
|
||||
if (payload.action === 'edit') {
|
||||
requestEditAccount(payload.id, form)
|
||||
.then((response) => {
|
||||
afterSubmit(response);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
{ id: 'service_has_been_successful_edited' },
|
||||
{
|
||||
name: toastAccountName,
|
||||
service: formatMessage({ id: 'account' }),
|
||||
},
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(afterErrors);
|
||||
} else {
|
||||
requestSubmitAccount({ form })
|
||||
.then((response) => {
|
||||
afterSubmit(response);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
{ id: 'service_has_been_successful_created' },
|
||||
{
|
||||
name: toastAccountName,
|
||||
service: formatMessage({ id: 'account' }),
|
||||
},
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
})
|
||||
.catch(afterErrors);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (values.parent_account_id) {
|
||||
setFieldValue('subaccount', true);
|
||||
}
|
||||
}, [values.parent_account_id]);
|
||||
|
||||
// Reset `parent account id` after change `account type`.
|
||||
useEffect(() => {
|
||||
setFieldValue('parent_account_id', null);
|
||||
}, [values.account_type_id]);
|
||||
|
||||
// Filtered accounts based on the given account type.
|
||||
const filteredAccounts = useMemo(
|
||||
() =>
|
||||
accounts.filter(
|
||||
(account) =>
|
||||
account.account_type_id === values.account_type_id ||
|
||||
!values.account_type_id,
|
||||
),
|
||||
[accounts, values.account_type_id],
|
||||
);
|
||||
|
||||
// Handles dialog close.
|
||||
const handleClose = useCallback(() => {
|
||||
closeDialog(dialogName);
|
||||
}, [closeDialog, dialogName]);
|
||||
|
||||
// Fetches accounts list.
|
||||
const fetchAccountsList = useQuery(
|
||||
'accounts-list',
|
||||
() => requestFetchAccounts(),
|
||||
{ enabled: false },
|
||||
);
|
||||
|
||||
// Fetches accounts types.
|
||||
const fetchAccountsTypes = useQuery(
|
||||
'accounts-types-list',
|
||||
async () => {
|
||||
await requestFetchAccountTypes();
|
||||
},
|
||||
{ enabled: false },
|
||||
);
|
||||
|
||||
// Fetch the given account id on edit mode.
|
||||
const fetchAccount = useQuery(
|
||||
['account', payload.id],
|
||||
(key, _id) => requestFetchAccount(_id),
|
||||
{ enabled: false },
|
||||
);
|
||||
|
||||
const isFetching =
|
||||
fetchAccountsList.isFetching ||
|
||||
fetchAccountsTypes.isFetching ||
|
||||
fetchAccount.isFetching;
|
||||
|
||||
// Fetch requests on dialog opening.
|
||||
const onDialogOpening = useCallback(() => {
|
||||
fetchAccountsList.refetch();
|
||||
fetchAccountsTypes.refetch();
|
||||
|
||||
if (payload.action === 'edit' && payload.id) {
|
||||
fetchAccount.refetch();
|
||||
}
|
||||
if (payload.action === 'new_child') {
|
||||
setFieldValue('parent_account_id', payload.parentAccountId);
|
||||
setFieldValue('account_type_id', payload.accountTypeId);
|
||||
}
|
||||
}, [payload, fetchAccount, fetchAccountsList, fetchAccountsTypes]);
|
||||
|
||||
// Handle account type change.
|
||||
const onChangeAccountType = useCallback(
|
||||
(accountType) => {
|
||||
setFieldValue('account_type_id', accountType.id);
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
// Handles change sub-account.
|
||||
const onChangeSubaccount = useCallback(
|
||||
(account) => {
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
// Handle dialog on closed.
|
||||
const onDialogClosed = useCallback(() => {
|
||||
resetForm();
|
||||
}, [resetForm]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={
|
||||
payload.action === 'edit' ? (
|
||||
<T id={'edit_account'} />
|
||||
) : (
|
||||
<T id={'new_account'} />
|
||||
)
|
||||
(payload.action === 'edit') ?
|
||||
(<T id={'edit_account'} />) :
|
||||
(<T id={'new_account'} />)
|
||||
}
|
||||
className={{
|
||||
'dialog--loading': isFetching,
|
||||
'dialog--account-form': true,
|
||||
}}
|
||||
className={'dialog--account-form'}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
onClosed={onDialogClosed}
|
||||
onOpening={onDialogOpening}
|
||||
isOpen={isOpen}
|
||||
isLoading={isFetching}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FormGroup
|
||||
label={<T id={'account_type'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames(
|
||||
'form-group--account-type',
|
||||
'form-group--select-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
inline={true}
|
||||
helperText={
|
||||
<ErrorMessage name="account_type_id" {...{ errors, touched }} />
|
||||
}
|
||||
intent={
|
||||
errors.account_type_id && touched.account_type_id && Intent.DANGER
|
||||
}
|
||||
>
|
||||
<AccountsTypesSelect
|
||||
accountsTypes={accountsTypes}
|
||||
selectedTypeId={values.account_type_id}
|
||||
defaultSelectText={<T id={'select_account_type'} />}
|
||||
onTypeSelected={onChangeAccountType}
|
||||
buttonProps={{ disabled: payload.action === 'edit' }}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={<T id={'account_name'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={'form-group--account-name'}
|
||||
intent={errors.name && touched.name && Intent.DANGER}
|
||||
helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={errors.name && touched.name && Intent.DANGER}
|
||||
{...getFieldProps('name')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={<T id={'account_code'} />}
|
||||
className={'form-group--account-code'}
|
||||
intent={errors.code && touched.code && Intent.DANGER}
|
||||
helperText={<ErrorMessage name="code" {...{ errors, touched }} />}
|
||||
inline={true}
|
||||
labelInfo={<Hint content={<T id="account_code_hint" />} />}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={errors.code && touched.code && Intent.DANGER}
|
||||
{...getFieldProps('code')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={' '}
|
||||
className={classNames('form-group--subaccount')}
|
||||
inline={true}
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={
|
||||
<>
|
||||
<T id={'sub_account'} />
|
||||
<Hint />
|
||||
</>
|
||||
}
|
||||
{...getFieldProps('subaccount')}
|
||||
checked={values.subaccount}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<If condition={values.subaccount}>
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames(
|
||||
'form-group--parent-account',
|
||||
'form-group--select-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
inline={true}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={filteredAccounts}
|
||||
onAccountSelected={onChangeSubaccount}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={values.parent_account_id}
|
||||
/>
|
||||
</FormGroup>
|
||||
</If>
|
||||
|
||||
<FormGroup
|
||||
label={<T id={'description'} />}
|
||||
className={'form-group--description'}
|
||||
intent={errors.description && Intent.DANGER}
|
||||
helperText={errors.description && errors.credential}
|
||||
inline={true}
|
||||
>
|
||||
<TextArea
|
||||
growVertically={true}
|
||||
height={280}
|
||||
{...getFieldProps('description')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={handleClose} style={{ minWidth: '75px' }}>
|
||||
<T id={'close'} />
|
||||
</Button>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
disabled={isSubmitting}
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
{payload.action === 'edit' ? (
|
||||
<T id={'edit'} />
|
||||
) : (
|
||||
<T id={'submit'} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<DialogSuspense>
|
||||
<AccountFormDialogContent
|
||||
dialogName={dialogName}
|
||||
accountId={payload.id}
|
||||
action={payload.action}
|
||||
parentAccountId={payload.parentAccountId}
|
||||
accountTypeId={payload.accountTypeId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountFormDialogContainer(AccountFormDialog);
|
||||
export default compose(
|
||||
withDialogRedux(),
|
||||
)(AccountFormDialog);
|
||||
400
client/src/containers/Dialogs/AccountFormDialogContent.js
Normal file
400
client/src/containers/Dialogs/AccountFormDialogContent.js
Normal file
@@ -0,0 +1,400 @@
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Intent,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { useFormik } from 'formik';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { pick, omit } from 'lodash';
|
||||
import { useQuery, queryCache } from 'react-query';
|
||||
import classNames from 'classnames';
|
||||
import Yup from 'services/yup';
|
||||
import {
|
||||
If,
|
||||
ErrorMessage,
|
||||
AppToaster,
|
||||
FieldRequiredHint,
|
||||
Hint,
|
||||
AccountsSelectList,
|
||||
AccountsTypesSelect,
|
||||
DialogContent,
|
||||
} from 'components';
|
||||
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccountDetail from 'containers/Accounts/withAccountDetail';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Account form dialog content.
|
||||
*/
|
||||
function AccountFormDialogContent({
|
||||
// #withAccounts
|
||||
accountsTypes,
|
||||
accounts,
|
||||
|
||||
// #withAccountDetail
|
||||
account,
|
||||
|
||||
// #withAccountsActions
|
||||
requestFetchAccounts,
|
||||
requestFetchAccountTypes,
|
||||
requestFetchAccount,
|
||||
requestSubmitAccount,
|
||||
requestEditAccount,
|
||||
|
||||
closeDialog,
|
||||
|
||||
// #ownProp
|
||||
accountId,
|
||||
action,
|
||||
dialogName,
|
||||
parentAccountId,
|
||||
accountTypeId,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const validationSchema = Yup.object().shape({
|
||||
name: Yup.string()
|
||||
.required()
|
||||
.min(3)
|
||||
.max(255)
|
||||
.label(formatMessage({ id: 'account_name_' })),
|
||||
code: Yup.string().digits().min(3).max(6),
|
||||
account_type_id: Yup.number()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'account_type_id' })),
|
||||
description: Yup.string().min(3).max(512).nullable().trim(),
|
||||
parent_account_id: Yup.number().nullable(),
|
||||
});
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
account_type_id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const transformApiErrors = (errors) => {
|
||||
const fields = {};
|
||||
if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
|
||||
fields.code = formatMessage({ id: 'account_code_is_not_unique' });
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
// Formik
|
||||
const {
|
||||
errors,
|
||||
values,
|
||||
touched,
|
||||
setFieldValue,
|
||||
resetForm,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
getFieldProps,
|
||||
} = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
...(accountId && pick(account, Object.keys(initialValues))),
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: (values, { setSubmitting, setErrors }) => {
|
||||
const form = omit(values, ['subaccount']);
|
||||
const toastAccountName = values.code
|
||||
? `${values.code} - ${values.name}`
|
||||
: values.name;
|
||||
|
||||
const afterSubmit = () => {
|
||||
closeDialog(dialogName);
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
queryCache.invalidateQueries('accounts-list');
|
||||
};
|
||||
const afterErrors = (errors) => {
|
||||
const errorsTransformed = transformApiErrors(errors);
|
||||
setErrors({ ...errorsTransformed });
|
||||
setSubmitting(false);
|
||||
};
|
||||
if (accountId) {
|
||||
requestEditAccount(accountId, form)
|
||||
.then((response) => {
|
||||
afterSubmit(response);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
{ id: 'service_has_been_successful_edited' },
|
||||
{
|
||||
name: toastAccountName,
|
||||
service: formatMessage({ id: 'account' }),
|
||||
},
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(afterErrors);
|
||||
} else {
|
||||
requestSubmitAccount({ form })
|
||||
.then((response) => {
|
||||
afterSubmit(response);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
{ id: 'service_has_been_successful_created' },
|
||||
{
|
||||
name: toastAccountName,
|
||||
service: formatMessage({ id: 'account' }),
|
||||
},
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
})
|
||||
.catch(afterErrors);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (values.parent_account_id) {
|
||||
setFieldValue('subaccount', true);
|
||||
}
|
||||
}, [values.parent_account_id]);
|
||||
|
||||
// Reset `parent account id` after change `account type`.
|
||||
useEffect(() => {
|
||||
setFieldValue('parent_account_id', null);
|
||||
}, [values.account_type_id]);
|
||||
|
||||
// Filtered accounts based on the given account type.
|
||||
const filteredAccounts = useMemo(
|
||||
() =>
|
||||
accounts.filter(
|
||||
(account) =>
|
||||
account.account_type_id === values.account_type_id ||
|
||||
!values.account_type_id,
|
||||
),
|
||||
[accounts, values.account_type_id],
|
||||
);
|
||||
|
||||
// Handles dialog close.
|
||||
const handleClose = useCallback(() => {
|
||||
closeDialog(dialogName);
|
||||
}, [closeDialog, dialogName]);
|
||||
|
||||
// Fetches accounts list.
|
||||
const fetchAccountsList = useQuery(
|
||||
'accounts-list',
|
||||
() => requestFetchAccounts(),
|
||||
);
|
||||
|
||||
// Fetches accounts types.
|
||||
const fetchAccountsTypes = useQuery(
|
||||
'accounts-types-list',
|
||||
async () => {
|
||||
await requestFetchAccountTypes();
|
||||
},
|
||||
);
|
||||
|
||||
// Fetch the given account id on edit mode.
|
||||
const fetchAccount = useQuery(
|
||||
['account', accountId],
|
||||
(key, _id) => requestFetchAccount(_id),
|
||||
{ enabled: accountId },
|
||||
);
|
||||
|
||||
const isFetching =
|
||||
fetchAccountsList.isFetching ||
|
||||
fetchAccountsTypes.isFetching ||
|
||||
fetchAccount.isFetching;
|
||||
|
||||
// Fetch requests on dialog opening.
|
||||
const onDialogOpening = useCallback(() => {
|
||||
fetchAccountsList.refetch();
|
||||
fetchAccountsTypes.refetch();
|
||||
|
||||
if (action === 'edit' && accountId) {
|
||||
fetchAccount.refetch();
|
||||
}
|
||||
if (action === 'new_child') {
|
||||
setFieldValue('parent_account_id', parentAccountId);
|
||||
setFieldValue('account_type_id', accountTypeId);
|
||||
}
|
||||
}, [
|
||||
parentAccountId,
|
||||
accountTypeId,
|
||||
fetchAccount,
|
||||
fetchAccountsList,
|
||||
fetchAccountsTypes,
|
||||
]);
|
||||
|
||||
// Handle account type change.
|
||||
const onChangeAccountType = useCallback(
|
||||
(accountType) => {
|
||||
setFieldValue('account_type_id', accountType.id);
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
// Handles change sub-account.
|
||||
const onChangeSubaccount = useCallback(
|
||||
(account) => {
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
// Handle dialog on closed.
|
||||
const onDialogClosed = useCallback(() => {
|
||||
resetForm();
|
||||
}, [resetForm]);
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isFetching}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FormGroup
|
||||
label={<T id={'account_type'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames(
|
||||
'form-group--account-type',
|
||||
'form-group--select-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
inline={true}
|
||||
helperText={
|
||||
<ErrorMessage name="account_type_id" {...{ errors, touched }} />
|
||||
}
|
||||
intent={
|
||||
errors.account_type_id && touched.account_type_id && Intent.DANGER
|
||||
}
|
||||
>
|
||||
<AccountsTypesSelect
|
||||
accountsTypes={accountsTypes}
|
||||
selectedTypeId={values.account_type_id}
|
||||
defaultSelectText={<T id={'select_account_type'} />}
|
||||
onTypeSelected={onChangeAccountType}
|
||||
buttonProps={{ disabled: action === 'edit' }}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={<T id={'account_name'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={'form-group--account-name'}
|
||||
intent={errors.name && touched.name && Intent.DANGER}
|
||||
helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={errors.name && touched.name && Intent.DANGER}
|
||||
{...getFieldProps('name')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={<T id={'account_code'} />}
|
||||
className={'form-group--account-code'}
|
||||
intent={errors.code && touched.code && Intent.DANGER}
|
||||
helperText={<ErrorMessage name="code" {...{ errors, touched }} />}
|
||||
inline={true}
|
||||
labelInfo={<Hint content={<T id="account_code_hint" />} />}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={errors.code && touched.code && Intent.DANGER}
|
||||
{...getFieldProps('code')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={' '}
|
||||
className={classNames('form-group--subaccount')}
|
||||
inline={true}
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={
|
||||
<>
|
||||
<T id={'sub_account'} />
|
||||
<Hint />
|
||||
</>
|
||||
}
|
||||
{...getFieldProps('subaccount')}
|
||||
checked={values.subaccount}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<If condition={values.subaccount}>
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames(
|
||||
'form-group--parent-account',
|
||||
'form-group--select-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
inline={true}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={filteredAccounts}
|
||||
onAccountSelected={onChangeSubaccount}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={values.parent_account_id}
|
||||
/>
|
||||
</FormGroup>
|
||||
</If>
|
||||
|
||||
<FormGroup
|
||||
label={<T id={'description'} />}
|
||||
className={'form-group--description'}
|
||||
intent={errors.description && Intent.DANGER}
|
||||
helperText={errors.description && errors.credential}
|
||||
inline={true}
|
||||
>
|
||||
<TextArea
|
||||
growVertically={true}
|
||||
height={280}
|
||||
{...getFieldProps('description')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={handleClose} style={{ minWidth: '75px' }}>
|
||||
<T id={'close'} />
|
||||
</Button>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
disabled={isSubmitting}
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
{action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAccountsActions,
|
||||
withAccountDetail,
|
||||
withAccounts(({ accountsTypes, accountsList }) => ({
|
||||
accountsTypes,
|
||||
accounts: accountsList,
|
||||
})),
|
||||
withDialogActions,
|
||||
)(AccountFormDialogContent);
|
||||
@@ -1,20 +1,26 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withSettings from 'containers/Settings/withSettings';
|
||||
import { compose } from 'utils';
|
||||
|
||||
function DashboardHomepage({ changePageTitle, name }) {
|
||||
|
||||
const DashboardHomepage = ({ changePageTitle }) => {
|
||||
useEffect(() => {
|
||||
changePageTitle('Craig’s Design and Landscaping Services')
|
||||
});
|
||||
changePageTitle(name)
|
||||
}, [name, changePageTitle]);
|
||||
|
||||
return (
|
||||
<div>asdasd</div>
|
||||
<DashboardInsider name="homepage">
|
||||
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
const mapActionsToProps = (dispatch) => ({
|
||||
changePageTitle: pageTitle => dispatch({
|
||||
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps)(DashboardHomepage);
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
withSettings(({ organizationSettings }) => ({
|
||||
name: organizationSettings.name,
|
||||
})),
|
||||
)(DashboardHomepage);
|
||||
@@ -1,18 +1,20 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
getResourceColumns,
|
||||
getResourceFieldsFactory,
|
||||
getResourceMetadata,
|
||||
getResourceData,
|
||||
} from 'store/resources/resources.reducer';
|
||||
getResourceFieldsFactory,
|
||||
getResourceDataFactory,
|
||||
} from 'store/resources/resources.selectors';
|
||||
|
||||
export default (mapState) => {
|
||||
const getResourceFields = getResourceFieldsFactory();
|
||||
const getResourceData = getResourceDataFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const { resourceName } = props;
|
||||
|
||||
const mapped = {
|
||||
resourceData: getResourceData(state, resourceName),
|
||||
resourceData: getResourceData(state, props),
|
||||
resourceFields: getResourceFields(state, props),
|
||||
resourceColumns: getResourceColumns(state, resourceName),
|
||||
resourceMetadata: getResourceMetadata(state, resourceName),
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
organizationSettings: state.settings.data.organization,
|
||||
manualJournalsSettings: state.settings.data.manual_journals,
|
||||
});
|
||||
export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
organizationSettings: state.settings.data.organization,
|
||||
manualJournalsSettings: state.settings.data.manual_journals,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
return connect(mapStateToProps);
|
||||
}
|
||||
|
||||
@@ -765,5 +765,9 @@ export default {
|
||||
something_wentwrong: 'Something went wrong.',
|
||||
new_password: 'New password',
|
||||
license_code_: 'License code',
|
||||
legal_organization_name: 'Legal Organization Name'
|
||||
legal_organization_name: 'Legal Organization Name',
|
||||
smaller_than: 'Smaller than',
|
||||
smaller_or_equals: 'Smaller or equals',
|
||||
bigger_than: 'Bigger than',
|
||||
bigger_or_equals: 'Bigger or equals',
|
||||
};
|
||||
|
||||
@@ -4,9 +4,6 @@ import t from 'store/types';
|
||||
export const submitEstimate = ({ form }) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.post('sales/estimates', form)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
|
||||
@@ -37,9 +37,6 @@ export const deleteInvoice = ({ id }) => {
|
||||
export const editInvoice = (id, form) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.post(`sales/invoices/${id}`, form)
|
||||
.then((response) => {
|
||||
resolve(response);
|
||||
|
||||
@@ -60,9 +60,6 @@ export const fetchAccountsTable = ({ query } = {}) => {
|
||||
type: t.ACCOUNTS_TABLE_LOADING,
|
||||
loading: true,
|
||||
});
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.get('accounts', { params: { ...pageQuery, ...query } })
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
@@ -105,9 +102,6 @@ export const fetchAccountsDataTable = ({ query }) => {
|
||||
export const submitAccount = ({ form }) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.post('accounts', form)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
@@ -136,9 +130,6 @@ export const submitAccount = ({ form }) => {
|
||||
export const editAccount = (id, form) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.post(`accounts/${id}`, form)
|
||||
.then((response) => {
|
||||
dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS });
|
||||
|
||||
@@ -10,17 +10,12 @@ export const submitCustomer = ({ form }) => {
|
||||
|
||||
ApiService.post('customers', form)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
const { data } = response;
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
});
|
||||
|
||||
reject(data?.errors);
|
||||
});
|
||||
});
|
||||
@@ -35,17 +30,12 @@ export const editCustomer = ({ form, id }) => {
|
||||
|
||||
ApiService.post(`customers/${id}`, form)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
const { data } = response;
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
});
|
||||
|
||||
reject(data?.errors);
|
||||
});
|
||||
});
|
||||
@@ -59,9 +49,6 @@ export const fetchCustomers = ({ query }) => {
|
||||
type: t.ITEMS_TABLE_LOADING,
|
||||
payload: { loading: true },
|
||||
});
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.get(`customers`, { params: { ...pageQuery, ...query } })
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
@@ -79,15 +66,9 @@ export const fetchCustomers = ({ query }) => {
|
||||
type: t.CUSTOMERS_TABLE_LOADING,
|
||||
payload: { loading: false },
|
||||
});
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
pageTitle: '',
|
||||
pageSubtitle: 'Hello World',
|
||||
pageSubtitle: '',
|
||||
preferencesPageTitle: '',
|
||||
sidebarExpended: true,
|
||||
dialogs: {},
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { createSelector } from "@reduxjs/toolkit";
|
||||
|
||||
const dialogByNameSelector = (dialogName) => (state) => state.dashboard.dialogs?.[dialogName];
|
||||
const dialogByNameSelector = (state, props) => state.dashboard.dialogs?.[props.dialogName];
|
||||
|
||||
export const isDialogOpenFactory = (dialogName) => createSelector(
|
||||
dialogByNameSelector(dialogName),
|
||||
export const isDialogOpenFactory = () => createSelector(
|
||||
dialogByNameSelector,
|
||||
(dialog) => {
|
||||
return dialog && dialog.isOpen;
|
||||
},
|
||||
);
|
||||
|
||||
export const getDialogPayloadFactory = (dialogName) => createSelector(
|
||||
dialogByNameSelector(dialogName),
|
||||
export const getDialogPayloadFactory = () => createSelector(
|
||||
dialogByNameSelector,
|
||||
(dialog) => {
|
||||
return { ...dialog?.payload };
|
||||
},
|
||||
|
||||
@@ -18,9 +18,6 @@ export const fetchItems = ({ query }) => {
|
||||
type: t.ITEMS_TABLE_LOADING,
|
||||
payload: { loading: true },
|
||||
});
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.get(`items`, { params: { ...pageQuery, ...query } })
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
|
||||
@@ -4,9 +4,6 @@ import t from 'store/types';
|
||||
export const submitReceipt = ({ form }) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
ApiService.post('sales/receipts', form)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
|
||||
@@ -20,14 +20,11 @@ export const fetchResourceColumns = ({ resourceSlug }) => {
|
||||
export const fetchResourceFields = ({ resourceSlug }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get(`resources/${resourceSlug}/fields`).then((response) => {
|
||||
// dispatch({
|
||||
// type: t.RESOURCE_FIELDS_SET,
|
||||
// fields: response.data.resource_fields,
|
||||
// resource_slug: resourceSlug,
|
||||
// });
|
||||
// dispatch({
|
||||
// type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
// });
|
||||
dispatch({
|
||||
type: t.RESOURCE_FIELDS_SET,
|
||||
fields: response.data.resource_fields,
|
||||
resource_slug: resourceSlug,
|
||||
});
|
||||
resolve(response);
|
||||
}).catch((error) => { reject(error); });
|
||||
});
|
||||
@@ -36,13 +33,13 @@ export const fetchResourceFields = ({ resourceSlug }) => {
|
||||
export const fetchResourceData = ({ resourceSlug }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get(`/resources/${resourceSlug}/data`).then((response) => {
|
||||
// dispatch({
|
||||
// type: t.RESOURCE_DATA_SET,
|
||||
// payload: {
|
||||
// data: response.data.data,
|
||||
// resource_key: resourceSlug,
|
||||
// },
|
||||
// });
|
||||
dispatch({
|
||||
type: t.RESOURCE_DATA_SET,
|
||||
payload: {
|
||||
data: response.data.resource_data,
|
||||
resourceKey: resourceSlug,
|
||||
},
|
||||
});
|
||||
resolve(response);
|
||||
}).catch(error => { reject(error); });
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import { createSelector } from 'reselect';
|
||||
import t from 'store/types';
|
||||
import { pickItemsFromIds } from 'store/selectors'
|
||||
|
||||
const initialState = {
|
||||
data: {},
|
||||
data: {
|
||||
resources: {},
|
||||
},
|
||||
fields: {},
|
||||
columns: {},
|
||||
resourceFields: {},
|
||||
@@ -42,79 +42,30 @@ export default createReducer(initialState, {
|
||||
|
||||
[t.RESOURCE_FIELDS_SET]: (state, action) => {
|
||||
const _fields = {};
|
||||
|
||||
|
||||
action.fields.forEach((field) => {
|
||||
_fields[field.id] = field;
|
||||
_fields[field.key] = field;
|
||||
});
|
||||
state.fields = {
|
||||
...state.fields,
|
||||
..._fields,
|
||||
};
|
||||
state.resourceFields[action.resource_slug] = action.fields.map(f => f.id);
|
||||
state.resourceFields[action.resource_slug] = action.fields.map(f => f.key);
|
||||
},
|
||||
|
||||
[t.RESOURCE_DATA_SET]: (state, action) => {
|
||||
const { data, resource_key: resourceKey } = action.payload;
|
||||
const dataMapped = {};
|
||||
const { data, resourceKey } = action.payload;
|
||||
const _data = {};
|
||||
|
||||
data.forEach((item) => { dataMapped[item.id] = item; })
|
||||
state.data[resourceKey] = dataMapped;
|
||||
data.forEach((item) => {
|
||||
_data[item.id] = item;
|
||||
});
|
||||
const order = data.map((item) => item.id);
|
||||
|
||||
state.data.resources[resourceKey] = {
|
||||
...(state.data.resources[resourceKey] || {}),
|
||||
data: _data,
|
||||
order,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const resourceFieldsIdsSelector = (state, props) => state.resources.resourceFields[props.resourceName];
|
||||
const resourceFieldsItemsSelector = (state) => state.resources.fields;
|
||||
|
||||
/**
|
||||
* Retrieve resource fields of the given resource slug.
|
||||
* @param {Object} state
|
||||
* @param {String} resourceSlug
|
||||
* @return {Array}
|
||||
*/
|
||||
export const getResourceFieldsFactory = () => createSelector(
|
||||
resourceFieldsIdsSelector,
|
||||
resourceFieldsItemsSelector,
|
||||
(fieldsIds, fieldsItems) => {
|
||||
return pickItemsFromIds(fieldsItems, fieldsIds);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve resource columns of the given resource slug.
|
||||
* @param {State} state
|
||||
* @param {String} resourceSlug -
|
||||
* @return {Array}
|
||||
*/
|
||||
export const getResourceColumns = (state, resourceSlug) => {
|
||||
const resourceIds = state.resources.resourceColumns[resourceSlug];
|
||||
const items = state.resources.columns;
|
||||
return pickItemsFromIds(items, resourceIds);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {Number} fieldId
|
||||
*/
|
||||
export const getResourceField = (state, fieldId) => {
|
||||
return state.resources.fields[fieldId];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {Number} columnId
|
||||
*/
|
||||
export const getResourceColumn = (state, columnId) => {
|
||||
return state.resources.columns[columnId];
|
||||
};
|
||||
|
||||
export const getResourceMetadata = (state, resourceSlug) => {
|
||||
return state.resources.metadata[resourceSlug];
|
||||
};
|
||||
|
||||
|
||||
export const getResourceData = (state, resourceSlug) => {
|
||||
return state.resources.data[resourceSlug] || {};
|
||||
};
|
||||
});
|
||||
77
client/src/store/resources/resources.selectors.js
Normal file
77
client/src/store/resources/resources.selectors.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { pickItemsFromIds } from 'store/selectors';
|
||||
|
||||
const resourceDataIdsSelector = (state, props) => {
|
||||
return state.resources.data.resources[props.resourceName]?.order;
|
||||
}
|
||||
const resourceDataSelector = (state, props) => {
|
||||
return state.resources.data.resources[props.resourceName]?.data;
|
||||
}
|
||||
|
||||
const resourceFieldsIdsSelector = (state, props) => state.resources.resourceFields[props.resourceName];
|
||||
const resourceFieldsItemsSelector = (state) => state.resources.fields;
|
||||
|
||||
/**
|
||||
* Retrieve resource fields of the given resource slug.
|
||||
* @param {Object} state
|
||||
* @param {String} resourceSlug
|
||||
* @return {Array}
|
||||
*/
|
||||
export const getResourceFieldsFactory = () => createSelector(
|
||||
resourceFieldsIdsSelector,
|
||||
resourceFieldsItemsSelector,
|
||||
(fieldsIds, fieldsItems) => {
|
||||
return pickItemsFromIds(fieldsItems, fieldsIds);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve resource data of the given resource name in component properties.
|
||||
* @return {Array}
|
||||
*/
|
||||
export const getResourceDataFactory = () => createSelector(
|
||||
resourceDataSelector,
|
||||
resourceDataIdsSelector,
|
||||
(resourceData, resourceDataIds) => {
|
||||
return pickItemsFromIds(resourceData, resourceDataIds);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve resource columns of the given resource slug.
|
||||
* @param {State} state
|
||||
* @param {String} resourceSlug -
|
||||
* @return {Array}
|
||||
*/
|
||||
export const getResourceColumns = (state, resourceSlug) => {
|
||||
const resourceIds = state.resources.resourceColumns[resourceSlug];
|
||||
const items = state.resources.columns;
|
||||
return pickItemsFromIds(items, resourceIds);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {Number} fieldId
|
||||
*/
|
||||
export const getResourceField = (state, fieldId) => {
|
||||
return state.resources.fields[fieldId];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {Number} columnId
|
||||
*/
|
||||
export const getResourceColumn = (state, columnId) => {
|
||||
return state.resources.columns[columnId];
|
||||
};
|
||||
|
||||
export const getResourceMetadata = (state, resourceSlug) => {
|
||||
return state.resources.metadata[resourceSlug];
|
||||
};
|
||||
|
||||
|
||||
export const getResourceData = (state, resourceSlug) => {
|
||||
return state.resources.data[resourceSlug] || {};
|
||||
};
|
||||
4
client/src/store/vendors/vendors.actions.js
vendored
4
client/src/store/vendors/vendors.actions.js
vendored
@@ -79,10 +79,6 @@ export const deleteVendor = ({ id }) => {
|
||||
export const submitVendor = ({ form }) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
});
|
||||
|
||||
ApiService.post('vendors', form)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
|
||||
@@ -6,4 +6,9 @@
|
||||
&-header{
|
||||
background: #ebf1f5;
|
||||
}
|
||||
|
||||
.bp3-spinner{
|
||||
padding-top: 10px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user