mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat: fix accounts issue.
This commit is contained in:
@@ -8,15 +8,10 @@ import DialogsContainer from 'components/DialogsContainer';
|
||||
import PreferencesContent from 'components/Preferences/PreferencesContent';
|
||||
import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
|
||||
import Search from 'containers/GeneralSearch/Search';
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function Dashboard({ sidebarExpended }) {
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className={classNames('dashboard', {
|
||||
'has-mini-sidebar': !sidebarExpended,
|
||||
})}>
|
||||
<div className={classNames('dashboard')}>
|
||||
<Switch>
|
||||
<Route path="/preferences">
|
||||
<Sidebar />
|
||||
@@ -35,9 +30,3 @@ function Dashboard({ sidebarExpended }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDashboard(({ sidebarExpended }) => ({
|
||||
sidebarExpended,
|
||||
})),
|
||||
)(Dashboard);
|
||||
|
||||
79
client/src/components/Dashboard/DashboardViewsTabs.js
Normal file
79
client/src/components/Dashboard/DashboardViewsTabs.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
|
||||
import { If, Icon } from 'components';
|
||||
|
||||
export default function DashboardViewsTabs({
|
||||
tabs,
|
||||
allTab = true,
|
||||
newViewTab = true,
|
||||
onNewViewTabClick,
|
||||
onChange,
|
||||
onTabClick,
|
||||
}) {
|
||||
const [currentView, setCurrentView] = useState(0);
|
||||
|
||||
const handleClickNewView = () => {
|
||||
onNewViewTabClick && onNewViewTabClick();
|
||||
};
|
||||
|
||||
const handleTabClick = (viewId) => {
|
||||
onTabClick && onTabClick(viewId);
|
||||
};
|
||||
|
||||
const mappedTabs = useMemo(
|
||||
() => tabs.map((tab) => ({ ...tab, onTabClick: handleTabClick })),
|
||||
[tabs],
|
||||
);
|
||||
|
||||
const handleViewLinkClick = () => {
|
||||
onNewViewTabClick && onNewViewTabClick();
|
||||
};
|
||||
|
||||
const handleTabsChange = (viewId) => {
|
||||
setCurrentView(viewId);
|
||||
onChange && onChange(viewId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
id="navbar"
|
||||
large={true}
|
||||
selectedTabId={currentView}
|
||||
className="tabs--dashboard-views"
|
||||
onChange={handleTabsChange}
|
||||
>
|
||||
{allTab && (
|
||||
<Tab id={0} title={<T id={'all'} />} onClick={handleViewLinkClick} />
|
||||
)}
|
||||
{mappedTabs.map((tab) => (
|
||||
<Tab id={tab.id} title={tab.name} onClick={handleTabClick} />
|
||||
))}
|
||||
|
||||
<If condition={newViewTab}>
|
||||
<Tooltip
|
||||
content={<T id={'create_a_new_view'} />}
|
||||
position={Position.RIGHT}
|
||||
>
|
||||
<Button
|
||||
className="button--new-view"
|
||||
icon={<Icon icon="plus" />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
</If>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardViewsTabs.propTypes = {
|
||||
tabs: PropTypes.array.isRequired,
|
||||
allTab: PropTypes.bool,
|
||||
newViewTab: PropTypes.bool,
|
||||
|
||||
onNewViewTabClick: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onTabClick: PropTypes.func,
|
||||
};
|
||||
@@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { isDialogOpen, getDialogPayload } from 'store/dashboard/dashboard.selectors';
|
||||
|
||||
export default (Dialog) => {
|
||||
|
||||
function DialogReduxConnect(props) {
|
||||
return (<Dialog {...props} />);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const dialogs = state.dashboard.dialogs;
|
||||
|
||||
if (dialogs && dialogs.hasOwnProperty['name'] && dialogs[props.name]) {
|
||||
const { isOpen, payload } = dialogs[props.name];
|
||||
return { isOpen, payload };
|
||||
}
|
||||
return {
|
||||
isOpen: isDialogOpen(state, props),
|
||||
payload: getDialogPayload(state, props),
|
||||
};
|
||||
};
|
||||
|
||||
return connect(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { HTMLSelect, Classes } from '@blueprintjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { getConditionTypeCompatators } from './DynamicFilterCompatators';
|
||||
|
||||
export default function DynamicFilterCompatatorField({
|
||||
dataType,
|
||||
...restProps
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const options = useMemo(
|
||||
() => getConditionTypeCompatators(dataType).map(comp => ({
|
||||
value: comp.value, label: formatMessage({ id: comp.label_id }),
|
||||
})),
|
||||
[dataType]
|
||||
);
|
||||
|
||||
return (
|
||||
<HTMLSelect
|
||||
options={options}
|
||||
className={Classes.FILL}
|
||||
{...{ ...restProps }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export const BooleanCompatators = [
|
||||
{ value: 'is', label_id: 'is' },
|
||||
];
|
||||
|
||||
export const TextCompatators = [
|
||||
{ value: 'contain', label_id: 'contain' },
|
||||
{ value: 'not_contain', label_id: 'not_contain' },
|
||||
{ value: 'equals', label_id: 'equals' },
|
||||
{ value: 'not_equal', label_id: 'not_equals' },
|
||||
];
|
||||
|
||||
export const DateCompatators = [
|
||||
{ value: 'in', label_id: 'in' },
|
||||
{ value: 'after', label_id: 'after' },
|
||||
{ value: 'before', label_id: 'before' },
|
||||
];
|
||||
|
||||
export const OptionsCompatators = [
|
||||
{ value: 'is', label_id: 'is' },
|
||||
{ value: 'is_not', label_id: 'is_not' },
|
||||
];
|
||||
|
||||
export const getConditionTypeCompatators = (dataType) => {
|
||||
return [
|
||||
...(dataType === 'options'
|
||||
? [...OptionsCompatators]
|
||||
: dataType === 'date'
|
||||
? [...DateCompatators]
|
||||
: dataType === 'boolean'
|
||||
? [...BooleanCompatators]
|
||||
: [...TextCompatators]),
|
||||
];
|
||||
};
|
||||
|
||||
export const getConditionDefaultCompatator = (dataType) => {
|
||||
const compatators = getConditionTypeCompatators(dataType);
|
||||
return compatators[0];
|
||||
};
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { useMemo, useRef, useEffect, useState } from 'react';
|
||||
import { FormGroup, MenuItem, InputGroup, Position, Spinner } from '@blueprintjs/core';
|
||||
import {
|
||||
FormGroup,
|
||||
MenuItem,
|
||||
InputGroup,
|
||||
Position,
|
||||
Checkbox,
|
||||
} from '@blueprintjs/core';
|
||||
import { connect } from 'react-redux';
|
||||
import { useQuery } from 'react-query';
|
||||
import { DateInput } from '@blueprintjs/datetime';
|
||||
@@ -13,35 +19,42 @@ import { If, 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({
|
||||
fieldMeta,
|
||||
dataType,
|
||||
value,
|
||||
|
||||
initialValue,
|
||||
error,
|
||||
// fieldkey,
|
||||
// resourceKey,
|
||||
|
||||
// #withResourceDetail
|
||||
resourceName,
|
||||
resourceData,
|
||||
|
||||
requestResourceData,
|
||||
|
||||
onChange,
|
||||
rosourceKey,
|
||||
|
||||
inputDebounceWait = 500,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [localValue, setLocalValue] = useState();
|
||||
|
||||
|
||||
const fetchResourceData = useQuery(
|
||||
['resource-data', resourceName],
|
||||
() => requestResourceData(resourceName),
|
||||
resourceName && ['resource-data', resourceName],
|
||||
(k, resName) => requestResourceData(resName),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
@@ -57,7 +70,7 @@ function DynamicFilterValueField({
|
||||
};
|
||||
|
||||
const handleBtnClick = () => {
|
||||
fetchResourceData.refetch({ force: true });
|
||||
fetchResourceData.refetch({});
|
||||
};
|
||||
|
||||
const listOptions = useMemo(() => Object.values(resourceData), [
|
||||
@@ -85,75 +98,85 @@ function DynamicFilterValueField({
|
||||
onChange && onChange(value);
|
||||
}, inputDebounceWait),
|
||||
);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setLocalValue(e.currentTarget.value);
|
||||
if (e.currentTarget.type === 'checkbox') {
|
||||
setLocalValue(e.currentTarget.checked);
|
||||
} else {
|
||||
setLocalValue(e.currentTarget.value);
|
||||
}
|
||||
handleInputChangeThrottled.current(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (e) => {
|
||||
const value = !!e.currentTarget.checked;
|
||||
setLocalValue(value);
|
||||
onChange && onChange(value);
|
||||
}
|
||||
|
||||
const handleDateChange = (date) => {
|
||||
setLocalValue(date);
|
||||
onChange && onChange(date);
|
||||
};
|
||||
|
||||
const transformDateValue = (value) => {
|
||||
return moment(value || new Date()).toDate();
|
||||
return value ? moment(value || new Date()).toDate() : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup className={'form-group--value'}>
|
||||
<Choose>
|
||||
<Choose.When condition={true}>
|
||||
<Spinner size={18} />
|
||||
<Choose.When condition={dataType === 'options'}>
|
||||
<ListSelect
|
||||
className={classNames(
|
||||
'list-select--filter-dropdown',
|
||||
'form-group--select-list',
|
||||
MODIFIER.SELECT_LIST_FILL_POPOVER,
|
||||
MODIFIER.SELECT_LIST_FILL_BUTTON,
|
||||
)}
|
||||
items={listOptions}
|
||||
itemRenderer={menuItem}
|
||||
loading={fetchResourceData.isFetching}
|
||||
itemPredicate={filterItems}
|
||||
popoverProps={{
|
||||
inline: true,
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
popoverClassName: 'popover--list-select-filter-dropdown',
|
||||
}}
|
||||
onItemSelect={onItemSelect}
|
||||
selectedItem={value}
|
||||
selectedItemProp={'id'}
|
||||
defaultText={<T id={'select_account_type'} />}
|
||||
labelProp={'name'}
|
||||
buttonProps={{ onClick: handleBtnClick }}
|
||||
/>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={dataType === 'date'}>
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
value={transformDateValue(localValue)}
|
||||
onChange={handleDateChange}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
position: Position.BOTTOM,
|
||||
}}
|
||||
shortcuts={true}
|
||||
placeholder={'Select date'}
|
||||
/>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={dataType === 'boolean'}>
|
||||
<Checkbox value={localValue} onChange={handleCheckboxChange} />
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<Choose>
|
||||
<Choose.When condition={fieldMeta.data_type === 'options'}>
|
||||
<ListSelect
|
||||
className={classNames(
|
||||
'list-select--filter-dropdown',
|
||||
'form-group--select-list',
|
||||
MODIFIER.SELECT_LIST_FILL_POPOVER,
|
||||
MODIFIER.SELECT_LIST_FILL_BUTTON,
|
||||
)}
|
||||
items={listOptions}
|
||||
itemRenderer={menuItem}
|
||||
loading={fetchResourceData.isFetching}
|
||||
itemPredicate={filterItems}
|
||||
popoverProps={{
|
||||
inline: true,
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
popoverClassName: 'popover--list-select-filter-dropdown',
|
||||
}}
|
||||
onItemSelect={onItemSelect}
|
||||
selectedItem={value}
|
||||
selectedItemProp={'id'}
|
||||
defaultText={<T id={'select_account_type'} />}
|
||||
labelProp={'name'}
|
||||
buttonProps={{ onClick: handleBtnClick }}
|
||||
/>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={fieldMeta.data_type === 'date'}>
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
value={transformDateValue(localValue)}
|
||||
onChange={handleDateChange}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
position: Position.BOTTOM,
|
||||
}}
|
||||
/>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<InputGroup
|
||||
placeholder={formatMessage({ id: 'value' })}
|
||||
onChange={handleInputChange}
|
||||
value={localValue}
|
||||
/>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
<InputGroup
|
||||
placeholder={formatMessage({ id: 'value' })}
|
||||
onChange={handleInputChange}
|
||||
value={localValue}
|
||||
/>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
</FormGroup>
|
||||
@@ -161,7 +184,7 @@ function DynamicFilterValueField({
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: props.fieldMeta.resource_key || 'account_type',
|
||||
resourceName: props.dataResource,
|
||||
});
|
||||
|
||||
const withResourceFilterValueField = connect(mapStateToProps);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import {
|
||||
FormGroup,
|
||||
@@ -7,25 +8,39 @@ import {
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { useFormik } from 'formik';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, last } from 'lodash';
|
||||
import { usePrevious } from 'react-use';
|
||||
import { debounce } from 'lodash';
|
||||
import Icon from 'components/Icon';
|
||||
import { checkRequiredProperties } from 'utils';
|
||||
import { checkRequiredProperties, uniqueMultiProps } from 'utils';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { DynamicFilterValueField } from 'components';
|
||||
import {
|
||||
DynamicFilterValueField,
|
||||
DynamicFilterCompatatorField,
|
||||
} from 'components';
|
||||
import Toaster from 'components/AppToaster';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
getConditionTypeCompatators,
|
||||
getConditionDefaultCompatator
|
||||
} from './DynamicFilter/DynamicFilterCompatators';
|
||||
|
||||
let limitToast;
|
||||
|
||||
type InitialCondition = {
|
||||
fieldKey: string,
|
||||
comparator: string,
|
||||
value: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter popover content.
|
||||
*/
|
||||
export default function FilterDropdown({
|
||||
fields,
|
||||
onFilterChange,
|
||||
refetchDebounceWait = 250,
|
||||
refetchDebounceWait = 10,
|
||||
initialCondition,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field]));
|
||||
@@ -37,6 +52,7 @@ export default function FilterDropdown({
|
||||
],
|
||||
[formatMessage],
|
||||
);
|
||||
|
||||
const resourceFields = useMemo(
|
||||
() => [
|
||||
...fields.map((field) => ({
|
||||
@@ -46,23 +62,13 @@ export default function FilterDropdown({
|
||||
],
|
||||
[fields],
|
||||
);
|
||||
const compatatorsItems = useMemo(
|
||||
() => [
|
||||
{ value: '', label: formatMessage({ id: 'comparator' }) },
|
||||
{ value: 'equals', label: formatMessage({ id: 'equals' }) },
|
||||
{ value: 'not_equal', label: formatMessage({ id: 'not_equal' }) },
|
||||
{ value: 'contain', label: formatMessage({ id: 'contain' }) },
|
||||
{ value: 'not_contain', label: formatMessage({ id: 'not_contain' }) },
|
||||
],
|
||||
[formatMessage],
|
||||
);
|
||||
|
||||
const defaultFilterCondition = useMemo(
|
||||
() => ({
|
||||
condition: 'and',
|
||||
field_key: fields.length > 0 ? fields[0].key : '',
|
||||
compatator: 'equals',
|
||||
value: '',
|
||||
field_key: initialCondition.fieldKey,
|
||||
comparator: initialCondition.comparator,
|
||||
value: initialCondition.value,
|
||||
}),
|
||||
[fields],
|
||||
);
|
||||
@@ -86,17 +92,17 @@ export default function FilterDropdown({
|
||||
} else {
|
||||
setFieldValue('conditions', [
|
||||
...values.conditions,
|
||||
defaultFilterCondition,
|
||||
last(values.conditions),
|
||||
]);
|
||||
}
|
||||
}, [values, defaultFilterCondition, setFieldValue]);
|
||||
|
||||
const filteredFilterConditions = useMemo(() => {
|
||||
const requiredProps = ['field_key', 'condition', 'compatator', 'value'];
|
||||
|
||||
return values.conditions.filter(
|
||||
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);
|
||||
@@ -109,7 +115,7 @@ export default function FilterDropdown({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
|
||||
onFilterChangeThrottled.current(filteredFilterConditions);
|
||||
onFilterChange && onFilterChange(filteredFilterConditions);
|
||||
}
|
||||
}, [filteredFilterConditions, prevConditions]);
|
||||
|
||||
@@ -124,7 +130,7 @@ export default function FilterDropdown({
|
||||
setFieldValue('conditions', [...conditions]);
|
||||
};
|
||||
|
||||
// transform dynamic value field.
|
||||
// Transform dynamic value field.
|
||||
const transformValueField = (value) => {
|
||||
if (value instanceof Date) {
|
||||
return moment(value).format('YYYY-MM-DD');
|
||||
@@ -145,27 +151,52 @@ export default function FilterDropdown({
|
||||
const currentField = fieldsKeyMapped.get(
|
||||
values.conditions[index].field_key,
|
||||
);
|
||||
const prevField = fieldsKeyMapped.get(e.currentTarget.value);
|
||||
const nextField = fieldsKeyMapped.get(e.currentTarget.value);
|
||||
|
||||
if (currentField.data_type !== prevField.data_type) {
|
||||
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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Value field props.
|
||||
const valueFieldProps = (name, index) => ({
|
||||
...fieldProps(name, index),
|
||||
onChange: (value) => {
|
||||
const transformedValue = transformValueField(value);
|
||||
setFieldValue(`conditions[${index}].${name}`, transformedValue);
|
||||
},
|
||||
});
|
||||
// Compatator field props.
|
||||
const comparatorFieldProps = (name, index) => {
|
||||
const condition = values.conditions[index];
|
||||
const field = fieldsKeyMapped.get(condition.field_key);
|
||||
|
||||
console.log(values.conditions, 'XX');
|
||||
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">
|
||||
@@ -190,18 +221,15 @@ export default function FilterDropdown({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className={'form-group--compatator'}>
|
||||
<HTMLSelect
|
||||
options={compatatorsItems}
|
||||
<FormGroup className={'form-group--comparator'}>
|
||||
<DynamicFilterCompatatorField
|
||||
className={Classes.FILL}
|
||||
{...fieldProps('compatator', index)}
|
||||
{...comparatorFieldProps('comparator', index)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<DynamicFilterValueField
|
||||
fieldMeta={fieldsKeyMapped.get(condition.field_key)}
|
||||
{...valueFieldProps('value', index)}
|
||||
/>
|
||||
<DynamicFilterValueField {...valueFieldProps('value', index)} />
|
||||
|
||||
<Button
|
||||
icon={<Icon icon="times" iconSize={14} />}
|
||||
minimal={true}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Scrollbar } from 'react-scrollbars-custom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -17,6 +17,10 @@ function SidebarContainer({
|
||||
// #withDashboard
|
||||
sidebarExpended,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle('has-mini-sidebar', !sidebarExpended);
|
||||
}, [sidebarExpended]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('sidebar', {
|
||||
|
||||
@@ -6,11 +6,13 @@ import For from './Utils/For';
|
||||
import ListSelect from './ListSelect';
|
||||
import FinancialStatement from './FinancialStatement';
|
||||
import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField';
|
||||
import DynamicFilterCompatatorField from './DynamicFilter/DynamicFilterCompatatorField';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import MODIFIER from './modifiers';
|
||||
import FieldHint from './FieldHint';
|
||||
import MenuItemLabel from './MenuItemLabel';
|
||||
import Pagination from './Pagination';
|
||||
import DashboardViewsTabs from './Dashboard/DashboardViewsTabs';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
@@ -23,11 +25,13 @@ export {
|
||||
FinancialStatement,
|
||||
Choose,
|
||||
DynamicFilterValueField,
|
||||
DynamicFilterCompatatorField,
|
||||
MODIFIER,
|
||||
ErrorMessage,
|
||||
FieldHint,
|
||||
Hint,
|
||||
MenuItemLabel,
|
||||
Pagination,
|
||||
DashboardViewsTabs,
|
||||
// For,
|
||||
};
|
||||
@@ -10,11 +10,12 @@ import {
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Intent
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
@@ -28,11 +29,9 @@ import withManualJournalsActions from 'containers/Accounting/withManualJournalsA
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
|
||||
function ManualJournalActionsBar({
|
||||
// #withResourceDetail
|
||||
resourceName = 'manual_journal',
|
||||
resourceName = 'manual_journals',
|
||||
resourceFields,
|
||||
|
||||
// #withManualJournals
|
||||
@@ -43,12 +42,12 @@ function ManualJournalActionsBar({
|
||||
|
||||
onFilterChanged,
|
||||
selectedRows,
|
||||
onBulkDelete
|
||||
onBulkDelete,
|
||||
}) {
|
||||
const { path } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const viewsMenuItems = manualJournalsViews.map(view => {
|
||||
const viewsMenuItems = manualJournalsViews.map((view) => {
|
||||
return (
|
||||
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
|
||||
);
|
||||
@@ -60,18 +59,25 @@ function ManualJournalActionsBar({
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
onFilterChange: filterConditions => {
|
||||
initialCondition: {
|
||||
fieldKey: 'journal_number',
|
||||
compatator: 'contains',
|
||||
value: '',
|
||||
},
|
||||
onFilterChange: (filterConditions) => {
|
||||
addManualJournalsTableQueries({
|
||||
filter_roles: filterConditions || ''
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
}
|
||||
},
|
||||
});
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
|
||||
selectedRows,
|
||||
]);
|
||||
|
||||
// Handle delete button click.
|
||||
const handleBulkDelete = useCallback(() => {
|
||||
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
|
||||
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
|
||||
}, [onBulkDelete, selectedRows]);
|
||||
|
||||
return (
|
||||
@@ -85,8 +91,8 @@ function ManualJournalActionsBar({
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--table-views')}
|
||||
icon={<Icon icon='table-16' iconSize={16} />}
|
||||
text={<T id={'table_views'}/>}
|
||||
icon={<Icon icon="table-16" iconSize={16} />}
|
||||
text={<T id={'table_views'} />}
|
||||
rightIcon={'caret-down'}
|
||||
/>
|
||||
</Popover>
|
||||
@@ -95,27 +101,28 @@ function ManualJournalActionsBar({
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='plus' />}
|
||||
text={<T id={'new_journal'}/>}
|
||||
icon={<Icon icon="plus" />}
|
||||
text={<T id={'new_journal'} />}
|
||||
onClick={onClickNewManualJournal}
|
||||
/>
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--filter')}
|
||||
text='Filter'
|
||||
icon={<Icon icon='filter-16' iconSize={16} />}
|
||||
text="Filter"
|
||||
icon={<Icon icon="filter-16" iconSize={16} />}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<If condition={hasSelectedRows}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='trash-16' iconSize={16} />}
|
||||
text={<T id={'delete'}/>}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
text={<T id={'delete'} />}
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleBulkDelete}
|
||||
/>
|
||||
@@ -123,20 +130,27 @@ function ManualJournalActionsBar({
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-import-16' iconSize={16} />}
|
||||
text={<T id={'import'}/>}
|
||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||
text={<T id={'import'} />}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-export-16' iconSize={16} />}
|
||||
text={<T id={'export'}/>}
|
||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
||||
text={<T id={'export'} />}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: 'manual_journals',
|
||||
});
|
||||
|
||||
const withManualJournalsActionsBar = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withManualJournalsActionsBar,
|
||||
withDialogActions,
|
||||
withResourceDetail(({ resourceFields }) => ({
|
||||
resourceFields,
|
||||
|
||||
@@ -17,6 +17,7 @@ import withManualJournals from 'containers/Accounting/withManualJournals';
|
||||
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
|
||||
import withViewsActions from 'containers/Views/withViewsActions';
|
||||
import withRouteActions from 'containers/Router/withRouteActions';
|
||||
import withResourceActions from 'containers/Resources/withResourcesActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
@@ -30,6 +31,9 @@ function ManualJournalsTable({
|
||||
// #withViewsActions
|
||||
requestFetchResourceViews,
|
||||
|
||||
// #withResourceActions
|
||||
requestFetchResourceFields,
|
||||
|
||||
// #withManualJournals
|
||||
manualJournalsTableQuery,
|
||||
|
||||
@@ -50,10 +54,15 @@ function ManualJournalsTable({
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const fetchViews = useQuery('journals-resource-views', () => {
|
||||
const fetchViews = useQuery('manual-journals-resource-views', () => {
|
||||
return requestFetchResourceViews('manual_journals');
|
||||
});
|
||||
|
||||
const fetchResourceFields = useQuery(
|
||||
'manual-journals-resource-fields',
|
||||
() => requestFetchResourceFields('manual_journals'),
|
||||
);
|
||||
|
||||
const fetchManualJournals = useQuery(
|
||||
['manual-journals-table', manualJournalsTableQuery],
|
||||
(key, q) => requestFetchManualJournalsTable(q),
|
||||
@@ -108,8 +117,8 @@ function ManualJournalsTable({
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
{ id: 'the_journals_has_been_successfully_deleted', },
|
||||
{ count: selectedRowsCount, },
|
||||
{ id: 'the_journals_has_been_successfully_deleted' },
|
||||
{ count: selectedRowsCount },
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
@@ -189,7 +198,7 @@ function ManualJournalsTable({
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={fetchViews.isFetching}
|
||||
loading={fetchViews.isFetching || fetchResourceFields.isFetching}
|
||||
name={'manual-journals'}
|
||||
>
|
||||
<ManualJournalsActionsBar
|
||||
@@ -265,6 +274,7 @@ export default compose(
|
||||
withDashboardActions,
|
||||
withManualJournalsActions,
|
||||
withViewsActions,
|
||||
withResourceActions,
|
||||
withManualJournals(({ manualJournalsTableQuery }) => ({
|
||||
manualJournalsTableQuery,
|
||||
})),
|
||||
|
||||
@@ -28,7 +28,7 @@ export default (mapState) => {
|
||||
manualJournalsTableQuery.page,
|
||||
),
|
||||
manualJournalsTableQuery,
|
||||
manualJournalsViews: getResourceViews(state, 'manual_journals'),
|
||||
manualJournalsViews: getResourceViews(state, props, 'manual_journals'),
|
||||
manualJournalsItems: state.manualJournals.items,
|
||||
|
||||
manualJournalsPagination: state.manualJournals.paginationMeta,
|
||||
|
||||
@@ -64,6 +64,11 @@ function AccountsActionsBar({
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
initialCondition: {
|
||||
fieldKey: 'name',
|
||||
compatator: 'contains',
|
||||
value: '',
|
||||
},
|
||||
onFilterChange: (filterConditions) => {
|
||||
setFilterCount(filterConditions.length || 0);
|
||||
addAccountsTableQueries({
|
||||
@@ -125,7 +130,7 @@ function AccountsActionsBar({
|
||||
filterCount <= 0 ? (
|
||||
<T id={'filter'} />
|
||||
) : (
|
||||
`${filterCount} filters applied`
|
||||
<T id={'count_filters_applied'} values={{ count: filterCount }} />
|
||||
)
|
||||
}
|
||||
icon={<Icon icon="filter-16" iconSize={16} />}
|
||||
|
||||
@@ -333,6 +333,7 @@ function AccountsChart({
|
||||
<AccountsViewsTabs onViewChanged={handleViewChanged} />
|
||||
|
||||
<AccountsDataTable
|
||||
loading={fetchAccountsHook.isFetching}
|
||||
onDeleteAccount={handleDeleteAccount}
|
||||
onInactiveAccount={handleInactiveAccount}
|
||||
onActivateAccount={handleActivateAccount}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { withRouter } from 'react-router';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
|
||||
import Icon from 'components/Icon';
|
||||
@@ -24,6 +25,7 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withCurrentView from 'containers/Views/withCurrentView';
|
||||
|
||||
import { If } from 'components';
|
||||
|
||||
@@ -35,6 +37,8 @@ function AccountsDataTable({
|
||||
// #withDialog.
|
||||
openDialog,
|
||||
|
||||
currentViewId,
|
||||
|
||||
// own properties
|
||||
loading,
|
||||
onFetchData,
|
||||
@@ -43,14 +47,18 @@ function AccountsDataTable({
|
||||
onInactiveAccount,
|
||||
onActivateAccount,
|
||||
}) {
|
||||
const [initialMount, setInitialMount] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(false);
|
||||
}, [currentViewId]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!accountsLoading) {
|
||||
setInitialMount(true);
|
||||
setIsMounted(true);
|
||||
}
|
||||
}, [accountsLoading, setInitialMount]);
|
||||
}, [accountsLoading, setIsMounted]);
|
||||
|
||||
const handleEditAccount = useCallback(
|
||||
(account) => () => {
|
||||
@@ -132,21 +140,21 @@ function AccountsDataTable({
|
||||
);
|
||||
},
|
||||
className: 'account_name',
|
||||
width: 300,
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
Header: formatMessage({ id: 'code' }),
|
||||
accessor: 'code',
|
||||
className: 'code',
|
||||
width: 100,
|
||||
width: 125,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
Header: formatMessage({ id: 'type' }),
|
||||
accessor: 'type.name',
|
||||
className: 'type',
|
||||
width: 120,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
id: 'normal',
|
||||
@@ -168,7 +176,7 @@ function AccountsDataTable({
|
||||
);
|
||||
},
|
||||
className: 'normal',
|
||||
width: 75,
|
||||
width: 115,
|
||||
},
|
||||
{
|
||||
id: 'balance',
|
||||
@@ -207,9 +215,9 @@ function AccountsDataTable({
|
||||
|
||||
const selectionColumn = useMemo(
|
||||
() => ({
|
||||
minWidth: 50,
|
||||
width: 50,
|
||||
maxWidth: 50,
|
||||
minWidth: 40,
|
||||
width: 40,
|
||||
maxWidth: 40,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@@ -227,7 +235,7 @@ function AccountsDataTable({
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<LoadingIndicator loading={loading && !isMounted} mount={false}>
|
||||
<DataTable
|
||||
noInitialFetch={true}
|
||||
columns={columns}
|
||||
@@ -239,7 +247,7 @@ function AccountsDataTable({
|
||||
treeGraph={true}
|
||||
sticky={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
loading={accountsLoading && !initialMount}
|
||||
loading={accountsLoading && !isMounted}
|
||||
spinnerProps={{ size: 30 }}
|
||||
rowContextMenu={rowContextMenu}
|
||||
/>
|
||||
@@ -248,6 +256,8 @@ function AccountsDataTable({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
withCurrentView,
|
||||
withDialogActions,
|
||||
withDashboardActions,
|
||||
withAccountsActions,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
@@ -7,14 +7,15 @@ import {
|
||||
NavbarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams, withRouter } from 'react-router-dom';
|
||||
import Icon from 'components/Icon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pick, debounce } from 'lodash';
|
||||
import Icon from 'components/Icon';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import {useUpdateEffect} from 'hooks';
|
||||
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
import { DashboardViewsTabs } from 'components';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
|
||||
@@ -48,7 +49,7 @@ function AccountsViewsTabs({
|
||||
useEffect(() => {
|
||||
changeAccountsCurrentView(customViewId || -1);
|
||||
setTopbarEditView(customViewId);
|
||||
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
|
||||
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
|
||||
|
||||
addAccountsTableQueries({
|
||||
custom_view_id: customViewId,
|
||||
@@ -57,7 +58,7 @@ function AccountsViewsTabs({
|
||||
return () => {
|
||||
setTopbarEditView(null);
|
||||
changePageSubtitle('');
|
||||
changeAccountsCurrentView(null)
|
||||
changeAccountsCurrentView(null);
|
||||
};
|
||||
}, [customViewId]);
|
||||
|
||||
@@ -71,52 +72,37 @@ function AccountsViewsTabs({
|
||||
history.push('/custom_views/accounts/new');
|
||||
};
|
||||
|
||||
// Handle view tab link click.
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
const tabs = accountsViews.map((view) => ({
|
||||
...pick(view, ['name', 'id']),
|
||||
}));
|
||||
|
||||
const debounceChangeHistory = useRef(
|
||||
debounce((toUrl) => {
|
||||
history.push(toUrl);
|
||||
}, 250),
|
||||
);
|
||||
|
||||
const handleTabsChange = (viewId) => {
|
||||
const toPath = viewId ? `${viewId}/custom_view` : '';
|
||||
debounceChangeHistory.current(`/accounts/${toPath}`);
|
||||
setTopbarEditView(viewId);
|
||||
};
|
||||
|
||||
const tabs = accountsViews.map((view) => {
|
||||
const baseUrl = '/accounts';
|
||||
|
||||
const link = (
|
||||
<Link
|
||||
to={`${baseUrl}/${view.id}/custom_view`}
|
||||
onClick={handleViewLinkClick}
|
||||
>{ view.name }</Link>
|
||||
);
|
||||
return <Tab id={`custom_view_${view.id}`} title={link} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<Navbar className='navbar--dashboard-views'>
|
||||
<Navbar className="navbar--dashboard-views">
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<Tabs
|
||||
id='navbar'
|
||||
large={true}
|
||||
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
|
||||
className='tabs--dashboard-views'
|
||||
>
|
||||
<Tab
|
||||
id={'all'}
|
||||
title={<Link to={`/accounts`}><T id={'all'}/></Link>}
|
||||
onClick={handleViewLinkClick}
|
||||
/>
|
||||
{ tabs }
|
||||
<Button
|
||||
className='button--new-view'
|
||||
icon={<Icon icon='plus' />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tabs>
|
||||
<DashboardViewsTabs
|
||||
baseUrl={'/accounts'}
|
||||
tabs={tabs}
|
||||
onNewViewTabClick={handleClickNewView}
|
||||
onChange={handleTabsChange}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
// Mapping view id from matched route params.
|
||||
viewId: ownProps.match.params.custom_view_id,
|
||||
});
|
||||
|
||||
@@ -130,5 +116,5 @@ export default compose(
|
||||
accountsViews,
|
||||
})),
|
||||
withAccountsTableActions,
|
||||
withViewDetail
|
||||
withViewDetail,
|
||||
)(AccountsViewsTabs);
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
accountsViews: getResourceViews(state, 'accounts'),
|
||||
accounts: getAccountsItems(state, state.accounts.currentViewId),
|
||||
accountsViews: getResourceViews(state, props, 'accounts'),
|
||||
accounts: getAccountsItems(state, props),
|
||||
accountsTypes: state.accounts.accountsTypes,
|
||||
|
||||
accountsTableQuery: state.accounts.tableQuery,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getCurrenciesList } from 'store/currencies/currencies.selector';
|
||||
|
||||
export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
currencies: state.currencies.data,
|
||||
currenciesList: Object.values(state.currencies.data),
|
||||
currenciesList: getCurrenciesList(state, props),
|
||||
currenciesLoading: state.currencies.loading,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
|
||||
const mapped = {
|
||||
customersViews: getResourceViews(state, 'customers'),
|
||||
customersViews: getResourceViews(state, props, 'customers'),
|
||||
customersItems: Object.values(state.customers.items),
|
||||
customers: getCustomersItems(state, state.customers.currentViewId),
|
||||
customersLoading: state.customers.loading,
|
||||
|
||||
@@ -6,7 +6,7 @@ export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
expenses: getExpensesItems(state, state.expenses.currentViewId),
|
||||
expensesViews: getResourceViews(state, 'expenses'),
|
||||
expensesViews: getResourceViews(state, props, 'expenses'),
|
||||
expensesItems: state.expenses.items,
|
||||
expensesTableQuery: state.expenses.tableQuery,
|
||||
expensesLoading: state.expenses.loading,
|
||||
|
||||
@@ -11,7 +11,7 @@ export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const viewPages = getViewPages(state.items.views, state.items.currentViewId);
|
||||
const mapped = {
|
||||
itemsViews: getResourceViews(state, 'items'),
|
||||
itemsViews: getResourceViews(state, props, 'items'),
|
||||
itemsCurrentPage: getCurrentPageResults(
|
||||
state.items.items,
|
||||
viewPages,
|
||||
|
||||
@@ -11,7 +11,7 @@ export default (mapState) => {
|
||||
const { resourceName } = props;
|
||||
const mapped = {
|
||||
resourceData: getResourceData(state, resourceName),
|
||||
resourceFields: getResourceFields(state, resourceName),
|
||||
resourceFields: getResourceFields(state, props),
|
||||
resourceColumns: getResourceColumns(state, resourceName),
|
||||
resourceMetadata: getResourceMetadata(state, resourceName),
|
||||
};
|
||||
|
||||
7
client/src/containers/Views/withCurrentView.js
Normal file
7
client/src/containers/Views/withCurrentView.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
currentViewId: props.match.params.custom_view_id,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
const { viewId } = props;
|
||||
|
||||
return {
|
||||
viewMeta: getViewMeta(state, viewId),
|
||||
viewItem: getViewItem(state, viewId),
|
||||
viewMeta: getViewMeta(state, props),
|
||||
viewItem: getViewItem(state, props),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -501,5 +501,13 @@ export default {
|
||||
once_delete_this_customer_you_will_able_to_restore_it: `Once you delete this customer, you won\'t be able to restore it later. Are you sure you want to delete this cusomter?`,
|
||||
once_delete_these_customers_you_will_not_able_restore_them:
|
||||
"Once you delete these customers, you won't be able to retrieve them later. Are you sure you want to delete them?",
|
||||
financial_accounting: 'Financial accounting'
|
||||
financial_accounting: 'Financial accounting',
|
||||
after: 'After',
|
||||
before: 'Before',
|
||||
count_filters_applied: '{count} filters applied',
|
||||
is: 'Is',
|
||||
is_not: 'Is Not',
|
||||
create_a_new_view: 'Create a new view',
|
||||
in: 'In',
|
||||
not_equals: 'Not Equals',
|
||||
};
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { pickItemsFromIds } from 'store/selectors';
|
||||
|
||||
export const getAccountsItems = (state, viewId) => {
|
||||
const accountsView = state.accounts.views[viewId || -1];
|
||||
const accountsItems = state.accounts.items;
|
||||
const accountsViewsSelector = (state) => state.accounts.views;
|
||||
const accountsDataSelector = (state) => state.accounts.items;
|
||||
const accountsCurrentViewSelector = (state) => state.accounts.currentViewId;
|
||||
|
||||
return typeof accountsView === 'object'
|
||||
? pickItemsFromIds(accountsItems, accountsView.ids) || []
|
||||
: [];
|
||||
};
|
||||
export const getAccountsItems = createSelector(
|
||||
accountsViewsSelector,
|
||||
accountsDataSelector,
|
||||
accountsCurrentViewSelector,
|
||||
(accountsViews, accountsItems, viewId) => {
|
||||
const accountsView = accountsViews[viewId || -1];
|
||||
|
||||
return typeof accountsView === 'object'
|
||||
? pickItemsFromIds(accountsItems, accountsView.ids) || []
|
||||
: [];
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
// @flow
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const currenciesItemsSelector = state => state.currencies.data;
|
||||
|
||||
export const getCurrenciesList = createSelector(
|
||||
currenciesItemsSelector,
|
||||
(currencies) => {
|
||||
return Object.values(currencies);
|
||||
}
|
||||
);
|
||||
|
||||
export const getCurrencyById = (currencies: Object, id: Integer) => {
|
||||
return Object.values(currencies).find(c => c.id == id) || null;
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import {pickItemsFromIds} from 'store/selectors';
|
||||
import {getResourceColumn } from 'store/resources/resources.reducer';
|
||||
import { createSelector } from 'reselect';
|
||||
import { pickItemsFromIds } from 'store/selectors';
|
||||
import { getResourceColumn } from 'store/resources/resources.reducer';
|
||||
|
||||
export const getResourceViews = (state, resourceName) => {
|
||||
const resourceViewsIds = state.views.resourceViews[resourceName] || [];
|
||||
return pickItemsFromIds(state.views.views, resourceViewsIds);
|
||||
};
|
||||
const resourceViewsIdsSelector = (state, props, resourceName) =>
|
||||
state.views.resourceViews[resourceName] || [];
|
||||
|
||||
const viewsSelector = (state) => state.views.views;
|
||||
const viewByIdSelector = (state, props) => state.views.viewsMeta[props.viewId] || {};
|
||||
|
||||
export const getResourceViews = createSelector(
|
||||
resourceViewsIdsSelector,
|
||||
viewsSelector,
|
||||
(resourceViewsIds, views) => {
|
||||
return pickItemsFromIds(views, resourceViewsIds);
|
||||
},
|
||||
);
|
||||
|
||||
export const getViewMeta = (state, viewId) => {
|
||||
const view = { ...state.views.viewsMeta[viewId] } || {};
|
||||
|
||||
|
||||
if (view.columns) {
|
||||
view.columns = view.columns.map((column) => {
|
||||
return {
|
||||
@@ -24,6 +34,7 @@ export const getViewItem = (state, viewId) => {
|
||||
};
|
||||
|
||||
export const getViewPages = (resourceViews, viewId) => {
|
||||
return (typeof resourceViews[viewId] === 'undefined') ?
|
||||
{} : resourceViews[viewId].pages;
|
||||
};
|
||||
return typeof resourceViews[viewId] === 'undefined'
|
||||
? {}
|
||||
: resourceViews[viewId].pages;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { pickItemsFromIds } from 'store/selectors';
|
||||
export const getCustomersItems = (state, viewId) => {
|
||||
|
||||
const customersView = state.customers.views[viewId || -1];
|
||||
const customersItems = state.customers.items;
|
||||
const customersViewsSelector = state => state.customers.views;
|
||||
const customersItemsSelector = state => state.customers.items;
|
||||
const customersCurrentViewSelector = state => state.customers.currentViewId;
|
||||
|
||||
return typeof customersView === 'object'
|
||||
? pickItemsFromIds(customersItems, customersView.ids) || []
|
||||
: [];
|
||||
};
|
||||
export const getCustomersItems = createSelector(
|
||||
customersViewsSelector,
|
||||
customersItemsSelector,
|
||||
customersCurrentViewSelector,
|
||||
(customersViews, customersItems, currentViewId) => {
|
||||
const customersView = customersViews[currentViewId || -1];
|
||||
|
||||
return (typeof customersView === 'object')
|
||||
? pickItemsFromIds(customersItems, customersView.ids) || []
|
||||
: [];
|
||||
},
|
||||
);
|
||||
18
client/src/store/dashboard/dashboard.selectors.js
Normal file
18
client/src/store/dashboard/dashboard.selectors.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createSelector } from "@reduxjs/toolkit";
|
||||
|
||||
|
||||
const dialogByNameSelector = (state, props) => state.dashboard.dialogs[props.name];
|
||||
|
||||
export const isDialogOpen = createSelector(
|
||||
dialogByNameSelector,
|
||||
(dialog) => {
|
||||
return dialog && dialog.isOpen;
|
||||
},
|
||||
);
|
||||
|
||||
export const getDialogPayload = createSelector(
|
||||
dialogByNameSelector,
|
||||
(dialog) => {
|
||||
return dialog?.payload;
|
||||
},
|
||||
);
|
||||
@@ -1,10 +1,19 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { pickItemsFromIds } from 'store/selectors';
|
||||
|
||||
export const getExpensesItems = (state, viewId) => {
|
||||
const accountsView = state.expenses.views[viewId || -1];
|
||||
const accountsItems = state.expenses.items;
|
||||
const expensesViewsSelector = state => state.expenses.views;
|
||||
const expensesItemsSelector = state => state.expenses.items;
|
||||
const expensesCurrentViewSelector = state => state.expenses.currentViewId;
|
||||
|
||||
return typeof accountsView === 'object'
|
||||
? pickItemsFromIds(accountsItems, accountsView.ids) || []
|
||||
: [];
|
||||
};
|
||||
export const getExpensesItems = createSelector(
|
||||
expensesViewsSelector,
|
||||
expensesItemsSelector,
|
||||
expensesCurrentViewSelector,
|
||||
(expensesViews, expensesItems, currentViewId) => {
|
||||
const expensesView = expensesViews[currentViewId || -1];
|
||||
|
||||
return (typeof expensesView === 'object')
|
||||
? (pickItemsFromIds(expensesItems, expensesView.ids) || [])
|
||||
: [];
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import { createSelector } from 'reselect';
|
||||
import t from 'store/types';
|
||||
import { pickItemsFromIds } from 'store/selectors'
|
||||
|
||||
@@ -61,17 +62,23 @@ export default createReducer(initialState, {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
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 getResourceFields = (state, resourceSlug) => {
|
||||
const resourceIds = state.resources.resourceFields[resourceSlug];
|
||||
const items = state.resources.fields;
|
||||
return pickItemsFromIds(items, resourceIds);
|
||||
};
|
||||
export const getResourceFields = createSelector(
|
||||
resourceFieldsIdsSelector,
|
||||
resourceFieldsItemsSelector,
|
||||
(fieldsIds, fieldsItems) => {
|
||||
return pickItemsFromIds(fieldsItems, fieldsIds);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve resource columns of the given resource slug.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
overflow-x: hidden;
|
||||
|
||||
.th{
|
||||
padding: 0.8rem 0.5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
background: #F8FAFA;
|
||||
font-size: 14px;
|
||||
color: #444;
|
||||
@@ -41,12 +41,12 @@
|
||||
&--desc{
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 6px solid #888;
|
||||
border-bottom: 6px solid #666;
|
||||
}
|
||||
&--asc{
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 6px solid #888;
|
||||
border-top: 6px solid #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,7 @@
|
||||
.tr .td{
|
||||
border-bottom: 1px solid #E8E8E8;
|
||||
align-items: center;
|
||||
color: #252833;
|
||||
|
||||
.placeholder{
|
||||
color: #999;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
body{
|
||||
color: #333;
|
||||
color: #1f3255;
|
||||
}
|
||||
|
||||
.#{$ns}-heading{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
.bigcapital-datatable{
|
||||
.normal{
|
||||
.#{$ns}-icon{
|
||||
color: #aaa;
|
||||
color: #838d9b;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,6 @@
|
||||
border-bottom-color: #c6c6c6;
|
||||
}
|
||||
}
|
||||
.code{
|
||||
color: #333;
|
||||
}
|
||||
.normal{
|
||||
.bp3-popover-wrapper{
|
||||
width: 100%;
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
&-sidebar-toggle{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
margin-left: 2px;
|
||||
|
||||
.#{$ns}-button{
|
||||
color: #8E8E8E;
|
||||
color: #6B8193;
|
||||
|
||||
&,
|
||||
&:hover,
|
||||
@@ -116,7 +116,7 @@
|
||||
|
||||
&,
|
||||
&-group{
|
||||
height: 44px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.#{$ns}-navbar-divider{
|
||||
@@ -124,17 +124,17 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
.#{$ns}-button{
|
||||
color: #5C5C5C;
|
||||
color: #4d4d4d;
|
||||
padding: 8px 12px;
|
||||
|
||||
&:hover{
|
||||
background: rgba(167, 182, 194, 0.12);
|
||||
color: #5C5C5C;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
&.bp3-minimal:active,
|
||||
&.bp3-minimal.bp3-active{
|
||||
background: rgba(167, 182, 194, 0.12);
|
||||
color: #5C5C5C;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
&.has-active-filters{
|
||||
@@ -145,7 +145,7 @@
|
||||
}
|
||||
}
|
||||
.#{$ns}-icon{
|
||||
color: #666;
|
||||
color: #4d4d4d;
|
||||
margin-right: 7px;
|
||||
}
|
||||
&.#{$ns}-minimal.#{$ns}-intent-danger{
|
||||
@@ -193,12 +193,12 @@
|
||||
&__title{
|
||||
align-items: center;;
|
||||
display: flex;
|
||||
margin-left: 6px;
|
||||
margin-left: 4px;
|
||||
|
||||
h1{
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
color: #4d4c4c;
|
||||
color: #393939;
|
||||
margin: 0;
|
||||
}
|
||||
h3{
|
||||
@@ -262,7 +262,7 @@
|
||||
.tbody{
|
||||
.th.selection,
|
||||
.td.selection{
|
||||
padding-left: 18px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,16 +294,17 @@
|
||||
.tabs--dashboard-views{
|
||||
|
||||
.#{$ns}-tab{
|
||||
color: #5C5C5C;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
|
||||
> a{
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
&[aria-selected='true'] {
|
||||
color: #0052cc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
|
||||
.#{$ns}-menu-item {
|
||||
color: $sidebar-menu-item-color;
|
||||
border-radius: 0;
|
||||
padding: 9px 16px;
|
||||
padding: 8px 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -86,10 +86,10 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
|
||||
}
|
||||
&-label{
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 12px;
|
||||
padding: 4px 16px;
|
||||
margin-top: 6px;
|
||||
padding: 6px 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
|
||||
color: $sidebar-menu-item-color;
|
||||
}
|
||||
.#{$ns}-menu-divider {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
border-top-color: rgba(255, 255, 255, 0.15);
|
||||
color: #6b708c;
|
||||
margin: 4px 0;
|
||||
}
|
||||
@@ -135,7 +135,6 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
|
||||
margin: 4px 0;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--mini-sidebar{
|
||||
|
||||
@@ -23,12 +23,13 @@
|
||||
&:not(:last-of-type) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.bp3-html-select select,
|
||||
.bp3-select select{
|
||||
padding: 0 20px 0 6px;
|
||||
padding: 0 15px 0 4px;
|
||||
|
||||
&:after{
|
||||
margin-right: 10px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.bp3-input{
|
||||
@@ -45,7 +46,7 @@
|
||||
.bp3-html-select::after,
|
||||
.form-group--select-list .bp3-button::after{
|
||||
border-top-color: #aaa;
|
||||
margin-right: 10px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,19 +56,24 @@
|
||||
}
|
||||
.form-group{
|
||||
&--condition{
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
width: 65px;
|
||||
min-width: 65px;
|
||||
}
|
||||
&--field{
|
||||
width: 45%;
|
||||
width: 40%;
|
||||
}
|
||||
&--compatator{
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
max-width: 120px;
|
||||
&--comparator{
|
||||
min-width: 100px;
|
||||
width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
&--value{
|
||||
width: 55%;
|
||||
width: 50%;
|
||||
|
||||
.bp3-control{
|
||||
display: inline-block;
|
||||
margin-left: 1.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +107,7 @@
|
||||
}
|
||||
|
||||
.bp3-input-group{
|
||||
padding: 8px;
|
||||
margin: 8px 8px 0;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.bp3-input:not(:first-child){
|
||||
|
||||
@@ -181,4 +181,11 @@ export const firstLettersArgs = (...args) => {
|
||||
}
|
||||
});
|
||||
return letters.join('').toUpperCase();
|
||||
}
|
||||
|
||||
|
||||
export const uniqueMultiProps = (items, props) => {
|
||||
return _.uniqBy(items, (item) => {
|
||||
return JSON.stringify(_.pick(item, props));
|
||||
});
|
||||
}
|
||||
@@ -40,6 +40,11 @@ export default {
|
||||
},
|
||||
'created_at': {
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
},
|
||||
active: {
|
||||
column: 'active',
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ exports.up = function (knex) {
|
||||
table.boolean('builtin').defaultTo(false);
|
||||
table.boolean('columnable');
|
||||
table.integer('index');
|
||||
table.string('data_resource');
|
||||
table.json('options');
|
||||
table.integer('resource_id').unsigned();
|
||||
}).raw('ALTER TABLE `RESOURCE_FIELDS` AUTO_INCREMENT = 1000').then(() => {
|
||||
|
||||
@@ -1,15 +1,51 @@
|
||||
|
||||
exports.seed = function(knex) {
|
||||
exports.seed = function (knex) {
|
||||
// Deletes ALL existing entries
|
||||
return knex('resource_fields').del()
|
||||
return knex('resource_fields')
|
||||
.del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('resource_fields').insert([
|
||||
{ id: 1, label_name: 'Name', key: 'name', data_type: '', active: 1, predefined: 1 },
|
||||
{ id: 2, label_name: 'Code', key: 'code', data_type: '', active: 1, predefined: 1 },
|
||||
{ id: 3, label_name: 'Account Type', key: 'type', data_type: '', active: 1, predefined: 1 },
|
||||
{ id: 4, label_name: 'Description', key: 'description', data_type: '', active: 1, predefined: 1 },
|
||||
{ id: 5, label_name: 'Account Normal', key: 'normal', data_type: 'string', active: 1, predefined: 1 },
|
||||
{
|
||||
id: 1,
|
||||
label_name: 'Name',
|
||||
key: 'name',
|
||||
data_type: '',
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label_name: 'Code',
|
||||
key: 'code',
|
||||
data_type: '',
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label_name: 'Account Type',
|
||||
key: 'type',
|
||||
data_type: '',
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
data_resource_id: 8,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label_name: 'Description',
|
||||
key: 'description',
|
||||
data_type: '',
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label_name: 'Account Normal',
|
||||
key: 'normal',
|
||||
data_type: 'string',
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label_name: 'Root Account Type',
|
||||
@@ -18,6 +54,14 @@ exports.seed = function(knex) {
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label_name: 'Active',
|
||||
key: 'active',
|
||||
data_type: 'boolean',
|
||||
active: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ exports.seed = (knex) => {
|
||||
// Inserts seed entries
|
||||
return knex('resources').insert([
|
||||
{ id: 1, name: 'accounts' },
|
||||
{ id: 8, name: 'accounts_types' },
|
||||
|
||||
{ id: 2, name: 'items' },
|
||||
{ id: 3, name: 'expenses' },
|
||||
{ id: 4, name: 'manual_journals' },
|
||||
|
||||
@@ -30,6 +30,7 @@ exports.seed = (knex) => {
|
||||
data_type: 'options',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
data_resource: 'accounts_types',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -58,6 +59,15 @@ exports.seed = (knex) => {
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
resource_id: 1,
|
||||
data_type: 'boolean',
|
||||
label_name: 'Active',
|
||||
key: 'active',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
|
||||
// Expenses
|
||||
{
|
||||
|
||||
@@ -120,9 +120,9 @@ export default {
|
||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }],
|
||||
});
|
||||
}
|
||||
await Account.query().insert({ ...form });
|
||||
const insertedAccount = await Account.query().insertAndFetch({ ...form });
|
||||
|
||||
return res.status(200).send({ item: { } });
|
||||
return res.status(200).send({ account: { ...insertedAccount } });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -135,7 +135,7 @@ export default {
|
||||
check('name').exists().isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code').exists().isLength({ max: 10 })
|
||||
check('code').optional().isLength({ max: 10 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
@@ -157,28 +157,30 @@ export default {
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
const foundAccountCodePromise = (form.code && form.code !== account.code)
|
||||
? Account.query().where('code', form.code).whereNot('id', account.id) : null;
|
||||
const errorReasons = [];
|
||||
|
||||
const foundAccountTypePromise = (form.account_type_id !== account.account_type_id)
|
||||
? AccountType.query().where('id', form.account_type_id) : null;
|
||||
|
||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||
foundAccountCodePromise, foundAccountTypePromise,
|
||||
]);
|
||||
if (foundAccountCode.length > 0 && foundAccountCodePromise) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||
// Validate the account type is not changed.
|
||||
if (account.account_type_id != form.accountTypeId) {
|
||||
errorReasons.push({
|
||||
type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 100,
|
||||
});
|
||||
}
|
||||
if (foundAccountType.length <= 0 && foundAccountTypePromise) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
|
||||
});
|
||||
}
|
||||
await account.patch({ ...form });
|
||||
// Validate the account code not exists on the storage.
|
||||
if (form.code && form.code !== account.code) {
|
||||
const foundAccountCode = await Account.query().where('code', form.code).whereNot('id', account.id);
|
||||
|
||||
return res.status(200).send();
|
||||
if (foundAccountCode.length > 0) {
|
||||
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ error: errorReasons });
|
||||
}
|
||||
// Update the account on the storage.
|
||||
const updatedAccount = await Account.query().patchAndFetchById(account.id, { ...form });
|
||||
|
||||
return res.status(200).send({ account: { ...updatedAccount } });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -268,7 +270,6 @@ export default {
|
||||
if (filter.stringified_filter_roles) {
|
||||
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
||||
}
|
||||
|
||||
const { Resource, Account, View } = req.models;
|
||||
const errorReasons = [];
|
||||
|
||||
@@ -338,14 +339,31 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
const query = Account.query()
|
||||
// .remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
|
||||
// console.log(builder.toKnexQuery().toSQL());
|
||||
}).toKnexQuery().toSQL();
|
||||
|
||||
console.log(query);
|
||||
|
||||
const accounts = await Account.query()
|
||||
.remember()
|
||||
// .remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
|
||||
// console.log(builder.toKnexQuery().toSQL());
|
||||
});
|
||||
|
||||
const nestedAccounts = Account.toNestedArray(accounts);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Lexer } from '@/lib/LogicEvaluation/Lexer';
|
||||
import Parser from '@/lib/LogicEvaluation/Parser';
|
||||
import QueryParser from '@/lib/LogicEvaluation/QueryParser';
|
||||
@@ -12,25 +13,8 @@ import resourceFieldsKeys from '@/data/ResourceFieldsKeys';
|
||||
// index: Number,
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get field column metadata and its relation with other tables.
|
||||
* @param {String} tableName - Table name of target column.
|
||||
* @param {String} columnKey - Target column key that stored in resource field.
|
||||
*/
|
||||
export function getRoleFieldColumn(tableName, columnKey) {
|
||||
const tableFields = resourceFieldsKeys[tableName];
|
||||
return (tableFields[columnKey]) ? tableFields[columnKey] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds roles queries.
|
||||
* @param {String} tableName -
|
||||
* @param {Object} role -
|
||||
*/
|
||||
export function buildRoleQuery(tableName, role) {
|
||||
const fieldRelation = getRoleFieldColumn(tableName, role.columnKey);
|
||||
const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
|
||||
|
||||
const textRoleQueryBuilder = (role, comparatorColumn) => {
|
||||
switch (role.comparator) {
|
||||
case 'equals':
|
||||
default:
|
||||
@@ -47,7 +31,82 @@ export function buildRoleQuery(tableName, role) {
|
||||
return (builder) => {
|
||||
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
|
||||
};
|
||||
case 'not_contain':
|
||||
case 'not_contains':
|
||||
return (builder) => {
|
||||
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const dateQueryBuilder = (role, comparatorColumn) => {
|
||||
switch(role.comparator) {
|
||||
case 'after':
|
||||
case 'before':
|
||||
return (builder) => {
|
||||
const comparator = role.comparator === 'before' ? '<' : '>';
|
||||
const hasTimeFormat = moment(role.value, 'YYYY-MM-DD HH:MM', true).isValid();
|
||||
const targetDate = moment(role.value);
|
||||
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
|
||||
|
||||
if (!hasTimeFormat) {
|
||||
if (role.comparator === 'before') {
|
||||
targetDate.startOf('day');
|
||||
} else {
|
||||
targetDate.endOf('day');
|
||||
}
|
||||
}
|
||||
const comparatorValue = targetDate.format(dateFormat);
|
||||
builder.where(comparatorColumn, comparator, comparatorValue);
|
||||
};
|
||||
case 'in':
|
||||
return (builder) => {
|
||||
const hasTimeFormat = moment(role.value, 'YYYY-MM-DD HH:MM', true).isValid();
|
||||
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
|
||||
|
||||
if (hasTimeFormat) {
|
||||
const targetDateTime = moment(role.value).format(dateFormat);
|
||||
builder.where(comparatorColumn, '=', targetDateTime);
|
||||
} else {
|
||||
const startDate = moment(role.value).startOf('day');
|
||||
const endDate = moment(role.value).endOf('day');
|
||||
|
||||
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
|
||||
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get field column metadata and its relation with other tables.
|
||||
* @param {String} tableName - Table name of target column.
|
||||
* @param {String} columnKey - Target column key that stored in resource field.
|
||||
*/
|
||||
export function getRoleFieldColumn(tableName, columnKey) {
|
||||
const tableFields = resourceFieldsKeys[tableName];
|
||||
return (tableFields[columnKey]) ? tableFields[columnKey] : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Builds roles queries.
|
||||
* @param {String} tableName -
|
||||
* @param {Object} role -
|
||||
*/
|
||||
export function buildRoleQuery(tableName, role) {
|
||||
const fieldRelation = getRoleFieldColumn(tableName, role.columnKey);
|
||||
const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
|
||||
|
||||
switch (fieldRelation.columnType) {
|
||||
case 'date':
|
||||
return dateQueryBuilder(role, comparatorColumn);
|
||||
case 'text':
|
||||
case 'varchar':
|
||||
default:
|
||||
return textRoleQueryBuilder(role, comparatorColumn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user