feat: fix accounts issue.

This commit is contained in:
Ahmed Bouhuolia
2020-06-25 13:43:47 +02:00
parent 6074134a53
commit 111aa83908
46 changed files with 797 additions and 345 deletions

View File

@@ -8,15 +8,10 @@ import DialogsContainer from 'components/DialogsContainer';
import PreferencesContent from 'components/Preferences/PreferencesContent'; import PreferencesContent from 'components/Preferences/PreferencesContent';
import PreferencesSidebar from 'components/Preferences/PreferencesSidebar'; import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
import Search from 'containers/GeneralSearch/Search'; import Search from 'containers/GeneralSearch/Search';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils'; export default function Dashboard() {
function Dashboard({ sidebarExpended }) {
return ( return (
<div className={classNames('dashboard', { <div className={classNames('dashboard')}>
'has-mini-sidebar': !sidebarExpended,
})}>
<Switch> <Switch>
<Route path="/preferences"> <Route path="/preferences">
<Sidebar /> <Sidebar />
@@ -35,9 +30,3 @@ function Dashboard({ sidebarExpended }) {
</div> </div>
); );
} }
export default compose(
withDashboard(({ sidebarExpended }) => ({
sidebarExpended,
})),
)(Dashboard);

View 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,
};

View File

@@ -1,19 +1,17 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isDialogOpen, getDialogPayload } from 'store/dashboard/dashboard.selectors';
export default (Dialog) => { export default (Dialog) => {
function DialogReduxConnect(props) { function DialogReduxConnect(props) {
return (<Dialog {...props} />); return (<Dialog {...props} />);
}; };
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const dialogs = state.dashboard.dialogs; return {
isOpen: isDialogOpen(state, props),
if (dialogs && dialogs.hasOwnProperty['name'] && dialogs[props.name]) { payload: getDialogPayload(state, props),
const { isOpen, payload } = dialogs[props.name]; };
return { isOpen, payload };
}
}; };
return connect( return connect(

View File

@@ -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 }}
/>
);
}

View File

@@ -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];
};

View File

@@ -1,5 +1,11 @@
import React, { useMemo, useRef, useEffect, useState } from 'react'; 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 { connect } from 'react-redux';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
@@ -13,35 +19,42 @@ import { If, Choose, ListSelect, MODIFIER } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails'; import withResourceDetail from 'containers/Resources/withResourceDetails';
import withResourceActions from 'containers/Resources/withResourcesActions'; import withResourceActions from 'containers/Resources/withResourcesActions';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator,
} from './DynamicFilterCompatators';
import { compose, momentFormatter } from 'utils'; import { compose, momentFormatter } from 'utils';
/** /**
* Dynamic filter fields. * Dynamic filter fields.
*/ */
function DynamicFilterValueField({ function DynamicFilterValueField({
fieldMeta, dataType,
value, value,
initialValue, initialValue,
error, error,
// fieldkey, // fieldkey,
// resourceKey, // resourceKey,
// #withResourceDetail
resourceName, resourceName,
resourceData, resourceData,
requestResourceData, requestResourceData,
onChange, onChange,
rosourceKey,
inputDebounceWait = 500, inputDebounceWait = 500,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [localValue, setLocalValue] = useState(); const [localValue, setLocalValue] = useState();
const fetchResourceData = useQuery( const fetchResourceData = useQuery(
['resource-data', resourceName], resourceName && ['resource-data', resourceName],
() => requestResourceData(resourceName), (k, resName) => requestResourceData(resName),
{ manual: true }, { manual: true },
); );
@@ -57,7 +70,7 @@ function DynamicFilterValueField({
}; };
const handleBtnClick = () => { const handleBtnClick = () => {
fetchResourceData.refetch({ force: true }); fetchResourceData.refetch({});
}; };
const listOptions = useMemo(() => Object.values(resourceData), [ const listOptions = useMemo(() => Object.values(resourceData), [
@@ -85,75 +98,85 @@ function DynamicFilterValueField({
onChange && onChange(value); onChange && onChange(value);
}, inputDebounceWait), }, inputDebounceWait),
); );
const handleInputChange = (e) => { 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); handleInputChangeThrottled.current(e.currentTarget.value);
}; };
const handleCheckboxChange = (e) => {
const value = !!e.currentTarget.checked;
setLocalValue(value);
onChange && onChange(value);
}
const handleDateChange = (date) => { const handleDateChange = (date) => {
setLocalValue(date); setLocalValue(date);
onChange && onChange(date); onChange && onChange(date);
}; };
const transformDateValue = (value) => { const transformDateValue = (value) => {
return moment(value || new Date()).toDate(); return value ? moment(value || new Date()).toDate() : null;
}; };
return ( return (
<FormGroup className={'form-group--value'}> <FormGroup className={'form-group--value'}>
<Choose> <Choose>
<Choose.When condition={true}> <Choose.When condition={dataType === 'options'}>
<Spinner size={18} /> <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.When>
<Choose.Otherwise> <Choose.Otherwise>
<Choose> <InputGroup
<Choose.When condition={fieldMeta.data_type === 'options'}> placeholder={formatMessage({ id: 'value' })}
<ListSelect onChange={handleInputChange}
className={classNames( value={localValue}
'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>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
</FormGroup> </FormGroup>
@@ -161,7 +184,7 @@ function DynamicFilterValueField({
} }
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
resourceName: props.fieldMeta.resource_key || 'account_type', resourceName: props.dataResource,
}); });
const withResourceFilterValueField = connect(mapStateToProps); const withResourceFilterValueField = connect(mapStateToProps);

View File

@@ -1,3 +1,4 @@
// @flow
import React, { useEffect, useMemo, useCallback, useRef } from 'react'; import React, { useEffect, useMemo, useCallback, useRef } from 'react';
import { import {
FormGroup, FormGroup,
@@ -7,25 +8,39 @@ import {
Intent, Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { isEqual } from 'lodash'; import { isEqual, last } from 'lodash';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { checkRequiredProperties } from 'utils'; import { checkRequiredProperties, uniqueMultiProps } from 'utils';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { DynamicFilterValueField } from 'components'; import {
DynamicFilterValueField,
DynamicFilterCompatatorField,
} from 'components';
import Toaster from 'components/AppToaster'; import Toaster from 'components/AppToaster';
import moment from 'moment'; import moment from 'moment';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator
} from './DynamicFilter/DynamicFilterCompatators';
let limitToast; let limitToast;
type InitialCondition = {
fieldKey: string,
comparator: string,
value: string,
};
/** /**
* Filter popover content. * Filter popover content.
*/ */
export default function FilterDropdown({ export default function FilterDropdown({
fields, fields,
onFilterChange, onFilterChange,
refetchDebounceWait = 250, refetchDebounceWait = 10,
initialCondition,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field])); const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field]));
@@ -37,6 +52,7 @@ export default function FilterDropdown({
], ],
[formatMessage], [formatMessage],
); );
const resourceFields = useMemo( const resourceFields = useMemo(
() => [ () => [
...fields.map((field) => ({ ...fields.map((field) => ({
@@ -46,23 +62,13 @@ export default function FilterDropdown({
], ],
[fields], [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( const defaultFilterCondition = useMemo(
() => ({ () => ({
condition: 'and', condition: 'and',
field_key: fields.length > 0 ? fields[0].key : '', field_key: initialCondition.fieldKey,
compatator: 'equals', comparator: initialCondition.comparator,
value: '', value: initialCondition.value,
}), }),
[fields], [fields],
); );
@@ -86,17 +92,17 @@ export default function FilterDropdown({
} else { } else {
setFieldValue('conditions', [ setFieldValue('conditions', [
...values.conditions, ...values.conditions,
defaultFilterCondition, last(values.conditions),
]); ]);
} }
}, [values, defaultFilterCondition, setFieldValue]); }, [values, defaultFilterCondition, setFieldValue]);
const filteredFilterConditions = useMemo(() => { const filteredFilterConditions = useMemo(() => {
const requiredProps = ['field_key', 'condition', 'compatator', 'value']; const requiredProps = ['field_key', 'condition', 'comparator', 'value'];
const conditions = values.conditions.filter(
return values.conditions.filter(
(condition) => !checkRequiredProperties(condition, requiredProps), (condition) => !checkRequiredProperties(condition, requiredProps),
); );
return uniqueMultiProps(conditions, requiredProps);
}, [values.conditions]); }, [values.conditions]);
const prevConditions = usePrevious(filteredFilterConditions); const prevConditions = usePrevious(filteredFilterConditions);
@@ -109,7 +115,7 @@ export default function FilterDropdown({
useEffect(() => { useEffect(() => {
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) { if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChangeThrottled.current(filteredFilterConditions); onFilterChange && onFilterChange(filteredFilterConditions);
} }
}, [filteredFilterConditions, prevConditions]); }, [filteredFilterConditions, prevConditions]);
@@ -124,7 +130,7 @@ export default function FilterDropdown({
setFieldValue('conditions', [...conditions]); setFieldValue('conditions', [...conditions]);
}; };
// transform dynamic value field. // Transform dynamic value field.
const transformValueField = (value) => { const transformValueField = (value) => {
if (value instanceof Date) { if (value instanceof Date) {
return moment(value).format('YYYY-MM-DD'); return moment(value).format('YYYY-MM-DD');
@@ -145,27 +151,52 @@ export default function FilterDropdown({
const currentField = fieldsKeyMapped.get( const currentField = fieldsKeyMapped.get(
values.conditions[index].field_key, 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`, ''); 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); override.onChange(e);
}, },
}; };
}; };
// Value field props. // Compatator field props.
const valueFieldProps = (name, index) => ({ const comparatorFieldProps = (name, index) => {
...fieldProps(name, index), const condition = values.conditions[index];
onChange: (value) => { const field = fieldsKeyMapped.get(condition.field_key);
const transformedValue = transformValueField(value);
setFieldValue(`conditions[${index}].${name}`, transformedValue);
},
});
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 ( return (
<div class="filter-dropdown"> <div class="filter-dropdown">
@@ -190,18 +221,15 @@ export default function FilterDropdown({
/> />
</FormGroup> </FormGroup>
<FormGroup className={'form-group--compatator'}> <FormGroup className={'form-group--comparator'}>
<HTMLSelect <DynamicFilterCompatatorField
options={compatatorsItems}
className={Classes.FILL} className={Classes.FILL}
{...fieldProps('compatator', index)} {...comparatorFieldProps('comparator', index)}
/> />
</FormGroup> </FormGroup>
<DynamicFilterValueField <DynamicFilterValueField {...valueFieldProps('value', index)} />
fieldMeta={fieldsKeyMapped.get(condition.field_key)}
{...valueFieldProps('value', index)}
/>
<Button <Button
icon={<Icon icon="times" iconSize={14} />} icon={<Icon icon="times" iconSize={14} />}
minimal={true} minimal={true}

View File

@@ -1,4 +1,4 @@
import * as React from 'react'; import React, { useEffect } from 'react';
import { Scrollbar } from 'react-scrollbars-custom'; import { Scrollbar } from 'react-scrollbars-custom';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -17,6 +17,10 @@ function SidebarContainer({
// #withDashboard // #withDashboard
sidebarExpended, sidebarExpended,
}) { }) {
useEffect(() => {
document.body.classList.toggle('has-mini-sidebar', !sidebarExpended);
}, [sidebarExpended]);
return ( return (
<div <div
className={classNames('sidebar', { className={classNames('sidebar', {

View File

@@ -6,11 +6,13 @@ import For from './Utils/For';
import ListSelect from './ListSelect'; import ListSelect from './ListSelect';
import FinancialStatement from './FinancialStatement'; import FinancialStatement from './FinancialStatement';
import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField'; import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField';
import DynamicFilterCompatatorField from './DynamicFilter/DynamicFilterCompatatorField';
import ErrorMessage from './ErrorMessage'; import ErrorMessage from './ErrorMessage';
import MODIFIER from './modifiers'; import MODIFIER from './modifiers';
import FieldHint from './FieldHint'; import FieldHint from './FieldHint';
import MenuItemLabel from './MenuItemLabel'; import MenuItemLabel from './MenuItemLabel';
import Pagination from './Pagination'; import Pagination from './Pagination';
import DashboardViewsTabs from './Dashboard/DashboardViewsTabs';
const Hint = FieldHint; const Hint = FieldHint;
@@ -23,11 +25,13 @@ export {
FinancialStatement, FinancialStatement,
Choose, Choose,
DynamicFilterValueField, DynamicFilterValueField,
DynamicFilterCompatatorField,
MODIFIER, MODIFIER,
ErrorMessage, ErrorMessage,
FieldHint, FieldHint,
Hint, Hint,
MenuItemLabel, MenuItemLabel,
Pagination, Pagination,
DashboardViewsTabs,
// For, // For,
}; };

View File

@@ -10,11 +10,12 @@ import {
Popover, Popover,
PopoverInteractionKind, PopoverInteractionKind,
Position, Position,
Intent Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom'; import { useRouteMatch, useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { connect } from 'react-redux';
import FilterDropdown from 'components/FilterDropdown'; import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -28,11 +29,9 @@ import withManualJournalsActions from 'containers/Accounting/withManualJournalsA
import { compose } from 'utils'; import { compose } from 'utils';
function ManualJournalActionsBar({ function ManualJournalActionsBar({
// #withResourceDetail // #withResourceDetail
resourceName = 'manual_journal', resourceName = 'manual_journals',
resourceFields, resourceFields,
// #withManualJournals // #withManualJournals
@@ -43,12 +42,12 @@ function ManualJournalActionsBar({
onFilterChanged, onFilterChanged,
selectedRows, selectedRows,
onBulkDelete onBulkDelete,
}) { }) {
const { path } = useRouteMatch(); const { path } = useRouteMatch();
const history = useHistory(); const history = useHistory();
const viewsMenuItems = manualJournalsViews.map(view => { const viewsMenuItems = manualJournalsViews.map((view) => {
return ( return (
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} /> <MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
); );
@@ -60,18 +59,25 @@ function ManualJournalActionsBar({
const filterDropdown = FilterDropdown({ const filterDropdown = FilterDropdown({
fields: resourceFields, fields: resourceFields,
onFilterChange: filterConditions => { initialCondition: {
fieldKey: 'journal_number',
compatator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => {
addManualJournalsTableQueries({ addManualJournalsTableQueries({
filter_roles: filterConditions || '' filter_roles: filterConditions || '',
}); });
onFilterChanged && onFilterChanged(filterConditions); onFilterChanged && onFilterChanged(filterConditions);
} },
}); });
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]); const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
// Handle delete button click. // Handle delete button click.
const handleBulkDelete = useCallback(() => { const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id)); onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]); }, [onBulkDelete, selectedRows]);
return ( return (
@@ -85,8 +91,8 @@ function ManualJournalActionsBar({
> >
<Button <Button
className={classNames(Classes.MINIMAL, 'button--table-views')} className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon='table-16' iconSize={16} />} icon={<Icon icon="table-16" iconSize={16} />}
text={<T id={'table_views'}/>} text={<T id={'table_views'} />}
rightIcon={'caret-down'} rightIcon={'caret-down'}
/> />
</Popover> </Popover>
@@ -95,27 +101,28 @@ function ManualJournalActionsBar({
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='plus' />} icon={<Icon icon="plus" />}
text={<T id={'new_journal'}/>} text={<T id={'new_journal'} />}
onClick={onClickNewManualJournal} onClick={onClickNewManualJournal}
/> />
<Popover <Popover
minimal={true}
content={filterDropdown} content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK} interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT} position={Position.BOTTOM_LEFT}
> >
<Button <Button
className={classNames(Classes.MINIMAL, 'button--filter')} className={classNames(Classes.MINIMAL, 'button--filter')}
text='Filter' text="Filter"
icon={<Icon icon='filter-16' iconSize={16} />} icon={<Icon icon="filter-16" iconSize={16} />}
/> />
</Popover> </Popover>
<If condition={hasSelectedRows}> <If condition={hasSelectedRows}>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='trash-16' iconSize={16} />} icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'}/>} text={<T id={'delete'} />}
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handleBulkDelete} onClick={handleBulkDelete}
/> />
@@ -123,20 +130,27 @@ function ManualJournalActionsBar({
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='file-import-16' iconSize={16} />} icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'}/>} text={<T id={'import'} />}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='file-export-16' iconSize={16} />} icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'}/>} text={<T id={'export'} />}
/> />
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );
} }
const mapStateToProps = (state, props) => ({
resourceName: 'manual_journals',
});
const withManualJournalsActionsBar = connect(mapStateToProps);
export default compose( export default compose(
withManualJournalsActionsBar,
withDialogActions, withDialogActions,
withResourceDetail(({ resourceFields }) => ({ withResourceDetail(({ resourceFields }) => ({
resourceFields, resourceFields,

View File

@@ -17,6 +17,7 @@ import withManualJournals from 'containers/Accounting/withManualJournals';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions'; import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import withViewsActions from 'containers/Views/withViewsActions'; import withViewsActions from 'containers/Views/withViewsActions';
import withRouteActions from 'containers/Router/withRouteActions'; import withRouteActions from 'containers/Router/withRouteActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -30,6 +31,9 @@ function ManualJournalsTable({
// #withViewsActions // #withViewsActions
requestFetchResourceViews, requestFetchResourceViews,
// #withResourceActions
requestFetchResourceFields,
// #withManualJournals // #withManualJournals
manualJournalsTableQuery, manualJournalsTableQuery,
@@ -50,10 +54,15 @@ function ManualJournalsTable({
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fetchViews = useQuery('journals-resource-views', () => { const fetchViews = useQuery('manual-journals-resource-views', () => {
return requestFetchResourceViews('manual_journals'); return requestFetchResourceViews('manual_journals');
}); });
const fetchResourceFields = useQuery(
'manual-journals-resource-fields',
() => requestFetchResourceFields('manual_journals'),
);
const fetchManualJournals = useQuery( const fetchManualJournals = useQuery(
['manual-journals-table', manualJournalsTableQuery], ['manual-journals-table', manualJournalsTableQuery],
(key, q) => requestFetchManualJournalsTable(q), (key, q) => requestFetchManualJournalsTable(q),
@@ -108,8 +117,8 @@ function ManualJournalsTable({
.then(() => { .then(() => {
AppToaster.show({ AppToaster.show({
message: formatMessage( message: formatMessage(
{ id: 'the_journals_has_been_successfully_deleted', }, { id: 'the_journals_has_been_successfully_deleted' },
{ count: selectedRowsCount, }, { count: selectedRowsCount },
), ),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
@@ -189,7 +198,7 @@ function ManualJournalsTable({
return ( return (
<DashboardInsider <DashboardInsider
loading={fetchViews.isFetching} loading={fetchViews.isFetching || fetchResourceFields.isFetching}
name={'manual-journals'} name={'manual-journals'}
> >
<ManualJournalsActionsBar <ManualJournalsActionsBar
@@ -265,6 +274,7 @@ export default compose(
withDashboardActions, withDashboardActions,
withManualJournalsActions, withManualJournalsActions,
withViewsActions, withViewsActions,
withResourceActions,
withManualJournals(({ manualJournalsTableQuery }) => ({ withManualJournals(({ manualJournalsTableQuery }) => ({
manualJournalsTableQuery, manualJournalsTableQuery,
})), })),

View File

@@ -28,7 +28,7 @@ export default (mapState) => {
manualJournalsTableQuery.page, manualJournalsTableQuery.page,
), ),
manualJournalsTableQuery, manualJournalsTableQuery,
manualJournalsViews: getResourceViews(state, 'manual_journals'), manualJournalsViews: getResourceViews(state, props, 'manual_journals'),
manualJournalsItems: state.manualJournals.items, manualJournalsItems: state.manualJournals.items,
manualJournalsPagination: state.manualJournals.paginationMeta, manualJournalsPagination: state.manualJournals.paginationMeta,

View File

@@ -64,6 +64,11 @@ function AccountsActionsBar({
const filterDropdown = FilterDropdown({ const filterDropdown = FilterDropdown({
fields: resourceFields, fields: resourceFields,
initialCondition: {
fieldKey: 'name',
compatator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => { onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0); setFilterCount(filterConditions.length || 0);
addAccountsTableQueries({ addAccountsTableQueries({
@@ -125,7 +130,7 @@ function AccountsActionsBar({
filterCount <= 0 ? ( filterCount <= 0 ? (
<T id={'filter'} /> <T id={'filter'} />
) : ( ) : (
`${filterCount} filters applied` <T id={'count_filters_applied'} values={{ count: filterCount }} />
) )
} }
icon={<Icon icon="filter-16" iconSize={16} />} icon={<Icon icon="filter-16" iconSize={16} />}

View File

@@ -333,6 +333,7 @@ function AccountsChart({
<AccountsViewsTabs onViewChanged={handleViewChanged} /> <AccountsViewsTabs onViewChanged={handleViewChanged} />
<AccountsDataTable <AccountsDataTable
loading={fetchAccountsHook.isFetching}
onDeleteAccount={handleDeleteAccount} onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount} onInactiveAccount={handleInactiveAccount}
onActivateAccount={handleActivateAccount} onActivateAccount={handleActivateAccount}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useMemo } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { import {
Button, Button,
Popover, Popover,
@@ -10,6 +10,7 @@ import {
Tooltip, Tooltip,
Intent, Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
@@ -24,6 +25,7 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccounts from 'containers/Accounts/withAccounts'; import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrentView from 'containers/Views/withCurrentView';
import { If } from 'components'; import { If } from 'components';
@@ -35,6 +37,8 @@ function AccountsDataTable({
// #withDialog. // #withDialog.
openDialog, openDialog,
currentViewId,
// own properties // own properties
loading, loading,
onFetchData, onFetchData,
@@ -43,14 +47,18 @@ function AccountsDataTable({
onInactiveAccount, onInactiveAccount,
onActivateAccount, onActivateAccount,
}) { }) {
const [initialMount, setInitialMount] = useState(false); const [isMounted, setIsMounted] = useState(false);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
useEffect(() => {
setIsMounted(false);
}, [currentViewId]);
useUpdateEffect(() => { useUpdateEffect(() => {
if (!accountsLoading) { if (!accountsLoading) {
setInitialMount(true); setIsMounted(true);
} }
}, [accountsLoading, setInitialMount]); }, [accountsLoading, setIsMounted]);
const handleEditAccount = useCallback( const handleEditAccount = useCallback(
(account) => () => { (account) => () => {
@@ -132,21 +140,21 @@ function AccountsDataTable({
); );
}, },
className: 'account_name', className: 'account_name',
width: 300, width: 220,
}, },
{ {
id: 'code', id: 'code',
Header: formatMessage({ id: 'code' }), Header: formatMessage({ id: 'code' }),
accessor: 'code', accessor: 'code',
className: 'code', className: 'code',
width: 100, width: 125,
}, },
{ {
id: 'type', id: 'type',
Header: formatMessage({ id: 'type' }), Header: formatMessage({ id: 'type' }),
accessor: 'type.name', accessor: 'type.name',
className: 'type', className: 'type',
width: 120, width: 140,
}, },
{ {
id: 'normal', id: 'normal',
@@ -168,7 +176,7 @@ function AccountsDataTable({
); );
}, },
className: 'normal', className: 'normal',
width: 75, width: 115,
}, },
{ {
id: 'balance', id: 'balance',
@@ -207,9 +215,9 @@ function AccountsDataTable({
const selectionColumn = useMemo( const selectionColumn = useMemo(
() => ({ () => ({
minWidth: 50, minWidth: 40,
width: 50, width: 40,
maxWidth: 50, maxWidth: 40,
}), }),
[], [],
); );
@@ -227,7 +235,7 @@ function AccountsDataTable({
); );
return ( return (
<LoadingIndicator loading={loading} mount={false}> <LoadingIndicator loading={loading && !isMounted} mount={false}>
<DataTable <DataTable
noInitialFetch={true} noInitialFetch={true}
columns={columns} columns={columns}
@@ -239,7 +247,7 @@ function AccountsDataTable({
treeGraph={true} treeGraph={true}
sticky={true} sticky={true}
onSelectedRowsChange={handleSelectedRowsChange} onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !initialMount} loading={accountsLoading && !isMounted}
spinnerProps={{ size: 30 }} spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu} rowContextMenu={rowContextMenu}
/> />
@@ -248,6 +256,8 @@ function AccountsDataTable({
} }
export default compose( export default compose(
withRouter,
withCurrentView,
withDialogActions, withDialogActions,
withDashboardActions, withDashboardActions,
withAccountsActions, withAccountsActions,

View File

@@ -1,4 +1,4 @@
import React, {useEffect} from 'react'; import React, { useEffect, useRef } from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
@@ -7,14 +7,15 @@ import {
NavbarGroup, NavbarGroup,
Tabs, Tabs,
Tab, Tab,
Button Button,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom'; import { useParams, withRouter } from 'react-router-dom';
import Icon from 'components/Icon';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { pick, debounce } from 'lodash';
import Icon from 'components/Icon';
import { FormattedMessage as T } from 'react-intl'; 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 withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccounts from 'containers/Accounts/withAccounts'; import withAccounts from 'containers/Accounts/withAccounts';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions'; import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
@@ -48,7 +49,7 @@ function AccountsViewsTabs({
useEffect(() => { useEffect(() => {
changeAccountsCurrentView(customViewId || -1); changeAccountsCurrentView(customViewId || -1);
setTopbarEditView(customViewId); setTopbarEditView(customViewId);
changePageSubtitle((customViewId && viewItem) ? viewItem.name : ''); changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
addAccountsTableQueries({ addAccountsTableQueries({
custom_view_id: customViewId, custom_view_id: customViewId,
@@ -57,7 +58,7 @@ function AccountsViewsTabs({
return () => { return () => {
setTopbarEditView(null); setTopbarEditView(null);
changePageSubtitle(''); changePageSubtitle('');
changeAccountsCurrentView(null) changeAccountsCurrentView(null);
}; };
}, [customViewId]); }, [customViewId]);
@@ -71,52 +72,37 @@ function AccountsViewsTabs({
history.push('/custom_views/accounts/new'); history.push('/custom_views/accounts/new');
}; };
// Handle view tab link click. const tabs = accountsViews.map((view) => ({
const handleViewLinkClick = () => { ...pick(view, ['name', 'id']),
setTopbarEditView(customViewId); }));
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 ( return (
<Navbar className='navbar--dashboard-views'> <Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}> <NavbarGroup align={Alignment.LEFT}>
<Tabs <DashboardViewsTabs
id='navbar' baseUrl={'/accounts'}
large={true} tabs={tabs}
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'} onNewViewTabClick={handleClickNewView}
className='tabs--dashboard-views' onChange={handleTabsChange}
> />
<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>
</NavbarGroup> </NavbarGroup>
</Navbar> </Navbar>
); );
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id, viewId: ownProps.match.params.custom_view_id,
}); });
@@ -130,5 +116,5 @@ export default compose(
accountsViews, accountsViews,
})), })),
withAccountsTableActions, withAccountsTableActions,
withViewDetail withViewDetail,
)(AccountsViewsTabs); )(AccountsViewsTabs);

View File

@@ -10,8 +10,8 @@ import {
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const mapped = { const mapped = {
accountsViews: getResourceViews(state, 'accounts'), accountsViews: getResourceViews(state, props, 'accounts'),
accounts: getAccountsItems(state, state.accounts.currentViewId), accounts: getAccountsItems(state, props),
accountsTypes: state.accounts.accountsTypes, accountsTypes: state.accounts.accountsTypes,
accountsTableQuery: state.accounts.tableQuery, accountsTableQuery: state.accounts.tableQuery,

View File

@@ -1,10 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getCurrenciesList } from 'store/currencies/currencies.selector';
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const mapped = { const mapped = {
currencies: state.currencies.data, currencies: state.currencies.data,
currenciesList: Object.values(state.currencies.data), currenciesList: getCurrenciesList(state, props),
currenciesLoading: state.currencies.loading, currenciesLoading: state.currencies.loading,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;

View File

@@ -6,7 +6,7 @@ export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const mapped = { const mapped = {
customersViews: getResourceViews(state, 'customers'), customersViews: getResourceViews(state, props, 'customers'),
customersItems: Object.values(state.customers.items), customersItems: Object.values(state.customers.items),
customers: getCustomersItems(state, state.customers.currentViewId), customers: getCustomersItems(state, state.customers.currentViewId),
customersLoading: state.customers.loading, customersLoading: state.customers.loading,

View File

@@ -6,7 +6,7 @@ export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const mapped = { const mapped = {
expenses: getExpensesItems(state, state.expenses.currentViewId), expenses: getExpensesItems(state, state.expenses.currentViewId),
expensesViews: getResourceViews(state, 'expenses'), expensesViews: getResourceViews(state, props, 'expenses'),
expensesItems: state.expenses.items, expensesItems: state.expenses.items,
expensesTableQuery: state.expenses.tableQuery, expensesTableQuery: state.expenses.tableQuery,
expensesLoading: state.expenses.loading, expensesLoading: state.expenses.loading,

View File

@@ -11,7 +11,7 @@ export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const viewPages = getViewPages(state.items.views, state.items.currentViewId); const viewPages = getViewPages(state.items.views, state.items.currentViewId);
const mapped = { const mapped = {
itemsViews: getResourceViews(state, 'items'), itemsViews: getResourceViews(state, props, 'items'),
itemsCurrentPage: getCurrentPageResults( itemsCurrentPage: getCurrentPageResults(
state.items.items, state.items.items,
viewPages, viewPages,

View File

@@ -11,7 +11,7 @@ export default (mapState) => {
const { resourceName } = props; const { resourceName } = props;
const mapped = { const mapped = {
resourceData: getResourceData(state, resourceName), resourceData: getResourceData(state, resourceName),
resourceFields: getResourceFields(state, resourceName), resourceFields: getResourceFields(state, props),
resourceColumns: getResourceColumns(state, resourceName), resourceColumns: getResourceColumns(state, resourceName),
resourceMetadata: getResourceMetadata(state, resourceName), resourceMetadata: getResourceMetadata(state, resourceName),
}; };

View 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);

View File

@@ -6,11 +6,9 @@ import {
export const mapStateToProps = (state, props) => { export const mapStateToProps = (state, props) => {
const { viewId } = props;
return { return {
viewMeta: getViewMeta(state, viewId), viewMeta: getViewMeta(state, props),
viewItem: getViewItem(state, viewId), viewItem: getViewItem(state, props),
}; };
}; };

View File

@@ -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_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_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?", "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',
}; };

View File

@@ -1,10 +1,19 @@
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors'; import { pickItemsFromIds } from 'store/selectors';
export const getAccountsItems = (state, viewId) => { const accountsViewsSelector = (state) => state.accounts.views;
const accountsView = state.accounts.views[viewId || -1]; const accountsDataSelector = (state) => state.accounts.items;
const accountsItems = state.accounts.items; const accountsCurrentViewSelector = (state) => state.accounts.currentViewId;
return typeof accountsView === 'object' export const getAccountsItems = createSelector(
? pickItemsFromIds(accountsItems, accountsView.ids) || [] accountsViewsSelector,
: []; accountsDataSelector,
}; accountsCurrentViewSelector,
(accountsViews, accountsItems, viewId) => {
const accountsView = accountsViews[viewId || -1];
return typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView.ids) || []
: [];
},
);

View File

@@ -1,4 +1,14 @@
// @flow // @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) => { export const getCurrencyById = (currencies: Object, id: Integer) => {
return Object.values(currencies).find(c => c.id == id) || null; return Object.values(currencies).find(c => c.id == id) || null;

View File

@@ -1,10 +1,20 @@
import {pickItemsFromIds} from 'store/selectors'; import { createSelector } from 'reselect';
import {getResourceColumn } from 'store/resources/resources.reducer'; import { pickItemsFromIds } from 'store/selectors';
import { getResourceColumn } from 'store/resources/resources.reducer';
export const getResourceViews = (state, resourceName) => { const resourceViewsIdsSelector = (state, props, resourceName) =>
const resourceViewsIds = state.views.resourceViews[resourceName] || []; state.views.resourceViews[resourceName] || [];
return pickItemsFromIds(state.views.views, resourceViewsIds);
}; 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) => { export const getViewMeta = (state, viewId) => {
const view = { ...state.views.viewsMeta[viewId] } || {}; const view = { ...state.views.viewsMeta[viewId] } || {};
@@ -24,6 +34,7 @@ export const getViewItem = (state, viewId) => {
}; };
export const getViewPages = (resourceViews, viewId) => { export const getViewPages = (resourceViews, viewId) => {
return (typeof resourceViews[viewId] === 'undefined') ? return typeof resourceViews[viewId] === 'undefined'
{} : resourceViews[viewId].pages; ? {}
: resourceViews[viewId].pages;
}; };

View File

@@ -1,10 +1,19 @@
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors'; import { pickItemsFromIds } from 'store/selectors';
export const getCustomersItems = (state, viewId) => {
const customersView = state.customers.views[viewId || -1]; const customersViewsSelector = state => state.customers.views;
const customersItems = state.customers.items; const customersItemsSelector = state => state.customers.items;
const customersCurrentViewSelector = state => state.customers.currentViewId;
return typeof customersView === 'object' export const getCustomersItems = createSelector(
? pickItemsFromIds(customersItems, customersView.ids) || [] customersViewsSelector,
: []; customersItemsSelector,
}; customersCurrentViewSelector,
(customersViews, customersItems, currentViewId) => {
const customersView = customersViews[currentViewId || -1];
return (typeof customersView === 'object')
? pickItemsFromIds(customersItems, customersView.ids) || []
: [];
},
);

View 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;
},
);

View File

@@ -1,10 +1,19 @@
import { createSelector } from '@reduxjs/toolkit';
import { pickItemsFromIds } from 'store/selectors'; import { pickItemsFromIds } from 'store/selectors';
export const getExpensesItems = (state, viewId) => { const expensesViewsSelector = state => state.expenses.views;
const accountsView = state.expenses.views[viewId || -1]; const expensesItemsSelector = state => state.expenses.items;
const accountsItems = state.expenses.items; const expensesCurrentViewSelector = state => state.expenses.currentViewId;
return typeof accountsView === 'object' export const getExpensesItems = createSelector(
? pickItemsFromIds(accountsItems, accountsView.ids) || [] expensesViewsSelector,
: []; expensesItemsSelector,
}; expensesCurrentViewSelector,
(expensesViews, expensesItems, currentViewId) => {
const expensesView = expensesViews[currentViewId || -1];
return (typeof expensesView === 'object')
? (pickItemsFromIds(expensesItems, expensesView.ids) || [])
: [];
},
);

View File

@@ -1,4 +1,5 @@
import { createReducer } from "@reduxjs/toolkit"; import { createReducer } from "@reduxjs/toolkit";
import { createSelector } from 'reselect';
import t from 'store/types'; import t from 'store/types';
import { pickItemsFromIds } from 'store/selectors' 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. * Retrieve resource fields of the given resource slug.
* @param {Object} state * @param {Object} state
* @param {String} resourceSlug * @param {String} resourceSlug
* @return {Array} * @return {Array}
*/ */
export const getResourceFields = (state, resourceSlug) => { export const getResourceFields = createSelector(
const resourceIds = state.resources.resourceFields[resourceSlug]; resourceFieldsIdsSelector,
const items = state.resources.fields; resourceFieldsItemsSelector,
return pickItemsFromIds(items, resourceIds); (fieldsIds, fieldsItems) => {
}; return pickItemsFromIds(fieldsItems, fieldsIds);
}
);
/** /**
* Retrieve resource columns of the given resource slug. * Retrieve resource columns of the given resource slug.

View File

@@ -22,7 +22,7 @@
overflow-x: hidden; overflow-x: hidden;
.th{ .th{
padding: 0.8rem 0.5rem; padding: 0.75rem 0.5rem;
background: #F8FAFA; background: #F8FAFA;
font-size: 14px; font-size: 14px;
color: #444; color: #444;
@@ -41,12 +41,12 @@
&--desc{ &--desc{
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-bottom: 6px solid #888; border-bottom: 6px solid #666;
} }
&--asc{ &--asc{
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-top: 6px solid #888; border-top: 6px solid #666;
} }
} }
} }
@@ -115,6 +115,7 @@
.tr .td{ .tr .td{
border-bottom: 1px solid #E8E8E8; border-bottom: 1px solid #E8E8E8;
align-items: center; align-items: center;
color: #252833;
.placeholder{ .placeholder{
color: #999; color: #999;

View File

@@ -1,7 +1,7 @@
body{ body{
color: #333; color: #1f3255;
} }
.#{$ns}-heading{ .#{$ns}-heading{

View File

@@ -4,7 +4,7 @@
.bigcapital-datatable{ .bigcapital-datatable{
.normal{ .normal{
.#{$ns}-icon{ .#{$ns}-icon{
color: #aaa; color: #838d9b;
padding-left: 15px; padding-left: 15px;
} }
} }
@@ -22,9 +22,6 @@
border-bottom-color: #c6c6c6; border-bottom-color: #c6c6c6;
} }
} }
.code{
color: #333;
}
.normal{ .normal{
.bp3-popover-wrapper{ .bp3-popover-wrapper{
width: 100%; width: 100%;

View File

@@ -21,10 +21,10 @@
&-sidebar-toggle{ &-sidebar-toggle{
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 4px; margin-left: 2px;
.#{$ns}-button{ .#{$ns}-button{
color: #8E8E8E; color: #6B8193;
&, &,
&:hover, &:hover,
@@ -116,7 +116,7 @@
&, &,
&-group{ &-group{
height: 44px; height: 42px;
} }
.#{$ns}-navbar-divider{ .#{$ns}-navbar-divider{
@@ -124,17 +124,17 @@
margin-right: 0; margin-right: 0;
} }
.#{$ns}-button{ .#{$ns}-button{
color: #5C5C5C; color: #4d4d4d;
padding: 8px 12px; padding: 8px 12px;
&:hover{ &:hover{
background: rgba(167, 182, 194, 0.12); background: rgba(167, 182, 194, 0.12);
color: #5C5C5C; color: #4d4d4d;
} }
&.bp3-minimal:active, &.bp3-minimal:active,
&.bp3-minimal.bp3-active{ &.bp3-minimal.bp3-active{
background: rgba(167, 182, 194, 0.12); background: rgba(167, 182, 194, 0.12);
color: #5C5C5C; color: #4d4d4d;
} }
&.has-active-filters{ &.has-active-filters{
@@ -145,7 +145,7 @@
} }
} }
.#{$ns}-icon{ .#{$ns}-icon{
color: #666; color: #4d4d4d;
margin-right: 7px; margin-right: 7px;
} }
&.#{$ns}-minimal.#{$ns}-intent-danger{ &.#{$ns}-minimal.#{$ns}-intent-danger{
@@ -193,12 +193,12 @@
&__title{ &__title{
align-items: center;; align-items: center;;
display: flex; display: flex;
margin-left: 6px; margin-left: 4px;
h1{ h1{
font-size: 26px; font-size: 26px;
font-weight: 300; font-weight: 300;
color: #4d4c4c; color: #393939;
margin: 0; margin: 0;
} }
h3{ h3{
@@ -262,7 +262,7 @@
.tbody{ .tbody{
.th.selection, .th.selection,
.td.selection{ .td.selection{
padding-left: 18px; padding-left: 14px;
} }
} }
} }
@@ -294,16 +294,17 @@
.tabs--dashboard-views{ .tabs--dashboard-views{
.#{$ns}-tab{ .#{$ns}-tab{
color: #5C5C5C; color: #666;
font-size: 14px; font-size: 14px;
line-height: 50px; line-height: 50px;
font-weight: 400; font-weight: 400;
padding: 0; padding: 0;
margin-right: 0; margin-right: 0;
padding-left: 14px;
padding-right: 14px;
> a{ &[aria-selected='true'] {
padding-left: 14px; color: #0052cc;
padding-right: 14px;
} }
} }

View File

@@ -60,7 +60,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
.#{$ns}-menu-item { .#{$ns}-menu-item {
color: $sidebar-menu-item-color; color: $sidebar-menu-item-color;
border-radius: 0; border-radius: 0;
padding: 9px 16px; padding: 8px 16px;
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
@@ -86,10 +86,10 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
} }
&-label{ &-label{
display: block; display: block;
color: rgba(255, 255, 255, 0.45); color: rgba(255, 255, 255, 0.5);
font-size: 12px; font-size: 12px;
padding: 4px 16px; padding: 6px 16px;
margin-top: 6px; margin-top: 4px;
} }
} }
@@ -127,7 +127,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
color: $sidebar-menu-item-color; color: $sidebar-menu-item-color;
} }
.#{$ns}-menu-divider { .#{$ns}-menu-divider {
border-top-color: rgba(255, 255, 255, 0.1); border-top-color: rgba(255, 255, 255, 0.15);
color: #6b708c; color: #6b708c;
margin: 4px 0; margin: 4px 0;
} }
@@ -135,7 +135,6 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
margin: 4px 0; margin: 4px 0;
height: 1px; height: 1px;
} }
} }
&--mini-sidebar{ &--mini-sidebar{

View File

@@ -23,12 +23,13 @@
&:not(:last-of-type) { &:not(:last-of-type) {
padding-right: 8px; padding-right: 8px;
} }
.bp3-html-select select, .bp3-html-select select,
.bp3-select select{ .bp3-select select{
padding: 0 20px 0 6px; padding: 0 15px 0 4px;
&:after{ &:after{
margin-right: 10px; margin-right: 8px;
} }
} }
.bp3-input{ .bp3-input{
@@ -45,7 +46,7 @@
.bp3-html-select::after, .bp3-html-select::after,
.form-group--select-list .bp3-button::after{ .form-group--select-list .bp3-button::after{
border-top-color: #aaa; border-top-color: #aaa;
margin-right: 10px; margin-right: 8px;
} }
} }
@@ -55,19 +56,24 @@
} }
.form-group{ .form-group{
&--condition{ &--condition{
width: 70px; width: 65px;
min-width: 70px; min-width: 65px;
} }
&--field{ &--field{
width: 45%; width: 40%;
} }
&--compatator{ &--comparator{
min-width: 120px; min-width: 100px;
width: 120px; width: 100px;
max-width: 120px; max-width: 100px;
} }
&--value{ &--value{
width: 55%; width: 50%;
.bp3-control{
display: inline-block;
margin-left: 1.8rem;
}
} }
} }
} }
@@ -101,7 +107,7 @@
} }
.bp3-input-group{ .bp3-input-group{
padding: 8px; margin: 8px 8px 0;
padding-bottom: 4px; padding-bottom: 4px;
.bp3-input:not(:first-child){ .bp3-input:not(:first-child){

View File

@@ -182,3 +182,10 @@ export const firstLettersArgs = (...args) => {
}); });
return letters.join('').toUpperCase(); return letters.join('').toUpperCase();
} }
export const uniqueMultiProps = (items, props) => {
return _.uniqBy(items, (item) => {
return JSON.stringify(_.pick(item, props));
});
}

View File

@@ -40,6 +40,11 @@ export default {
}, },
'created_at': { 'created_at': {
column: 'created_at', column: 'created_at',
columnType: 'date',
},
active: {
column: 'active',
}, },
}, },

View File

@@ -12,6 +12,7 @@ exports.up = function (knex) {
table.boolean('builtin').defaultTo(false); table.boolean('builtin').defaultTo(false);
table.boolean('columnable'); table.boolean('columnable');
table.integer('index'); table.integer('index');
table.string('data_resource');
table.json('options'); table.json('options');
table.integer('resource_id').unsigned(); table.integer('resource_id').unsigned();
}).raw('ALTER TABLE `RESOURCE_FIELDS` AUTO_INCREMENT = 1000').then(() => { }).raw('ALTER TABLE `RESOURCE_FIELDS` AUTO_INCREMENT = 1000').then(() => {

View File

@@ -1,15 +1,51 @@
exports.seed = function (knex) {
exports.seed = function(knex) {
// Deletes ALL existing entries // Deletes ALL existing entries
return knex('resource_fields').del() return knex('resource_fields')
.del()
.then(() => { .then(() => {
// Inserts seed entries // Inserts seed entries
return knex('resource_fields').insert([ 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: 1,
{ id: 3, label_name: 'Account Type', key: 'type', data_type: '', active: 1, predefined: 1 }, label_name: 'Name',
{ id: 4, label_name: 'Description', key: 'description', data_type: '', active: 1, predefined: 1 }, key: 'name',
{ id: 5, label_name: 'Account Normal', key: 'normal', data_type: 'string', active: 1, predefined: 1 }, 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, id: 6,
label_name: 'Root Account Type', label_name: 'Root Account Type',
@@ -18,6 +54,14 @@ exports.seed = function(knex) {
active: 1, active: 1,
predefined: 1, predefined: 1,
}, },
{
id: 7,
label_name: 'Active',
key: 'active',
data_type: 'boolean',
active: 1,
predefined: 1,
},
]); ]);
}); });
}; };

View File

@@ -6,6 +6,8 @@ exports.seed = (knex) => {
// Inserts seed entries // Inserts seed entries
return knex('resources').insert([ return knex('resources').insert([
{ id: 1, name: 'accounts' }, { id: 1, name: 'accounts' },
{ id: 8, name: 'accounts_types' },
{ id: 2, name: 'items' }, { id: 2, name: 'items' },
{ id: 3, name: 'expenses' }, { id: 3, name: 'expenses' },
{ id: 4, name: 'manual_journals' }, { id: 4, name: 'manual_journals' },

View File

@@ -30,6 +30,7 @@ exports.seed = (knex) => {
data_type: 'options', data_type: 'options',
predefined: 1, predefined: 1,
columnable: true, columnable: true,
data_resource: 'accounts_types',
}, },
{ {
id: 5, id: 5,
@@ -58,6 +59,15 @@ exports.seed = (knex) => {
predefined: 1, predefined: 1,
columnable: true, columnable: true,
}, },
{
id: 17,
resource_id: 1,
data_type: 'boolean',
label_name: 'Active',
key: 'active',
predefined: 1,
columnable: true,
},
// Expenses // Expenses
{ {

View File

@@ -120,9 +120,9 @@ export default {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }], 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 }) check('name').exists().isLength({ min: 3 })
.trim() .trim()
.escape(), .escape(),
check('code').exists().isLength({ max: 10 }) check('code').optional().isLength({ max: 10 })
.trim() .trim()
.escape(), .escape(),
check('account_type_id').exists().isNumeric().toInt(), check('account_type_id').exists().isNumeric().toInt(),
@@ -157,28 +157,30 @@ export default {
if (!account) { if (!account) {
return res.boom.notFound(); return res.boom.notFound();
} }
const foundAccountCodePromise = (form.code && form.code !== account.code) const errorReasons = [];
? Account.query().where('code', form.code).whereNot('id', account.id) : null;
const foundAccountTypePromise = (form.account_type_id !== account.account_type_id) // Validate the account type is not changed.
? AccountType.query().where('id', form.account_type_id) : null; if (account.account_type_id != form.accountTypeId) {
errorReasons.push({
const [foundAccountCode, foundAccountType] = await Promise.all([ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 100,
foundAccountCodePromise, foundAccountTypePromise,
]);
if (foundAccountCode.length > 0 && foundAccountCodePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
}); });
} }
if (foundAccountType.length <= 0 && foundAccountTypePromise) { // Validate the account code not exists on the storage.
return res.boom.badRequest(null, { if (form.code && form.code !== account.code) {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }], const foundAccountCode = await Account.query().where('code', form.code).whereNot('id', account.id);
});
}
await account.patch({ ...form });
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) { if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles); filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
} }
const { Resource, Account, View } = req.models; const { Resource, Account, View } = req.models;
const errorReasons = []; const errorReasons = [];
@@ -338,14 +339,31 @@ export default {
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons }); 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() const accounts = await Account.query()
.remember() // .remember()
.onBuild((builder) => { .onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types); builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type'); builder.withGraphFetched('type');
builder.withGraphFetched('balance'); builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
}); });
const nestedAccounts = Account.toNestedArray(accounts); const nestedAccounts = Account.toNestedArray(accounts);

View File

@@ -1,4 +1,5 @@
import { difference } from 'lodash'; import { difference } from 'lodash';
import moment from 'moment';
import { Lexer } from '@/lib/LogicEvaluation/Lexer'; import { Lexer } from '@/lib/LogicEvaluation/Lexer';
import Parser from '@/lib/LogicEvaluation/Parser'; import Parser from '@/lib/LogicEvaluation/Parser';
import QueryParser from '@/lib/LogicEvaluation/QueryParser'; import QueryParser from '@/lib/LogicEvaluation/QueryParser';
@@ -12,25 +13,8 @@ import resourceFieldsKeys from '@/data/ResourceFieldsKeys';
// index: Number, // 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) { switch (role.comparator) {
case 'equals': case 'equals':
default: default:
@@ -47,6 +31,81 @@ export function buildRoleQuery(tableName, role) {
return (builder) => { return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`); 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);
} }
} }