diff --git a/client/src/components/Dashboard/Dashboard.js b/client/src/components/Dashboard/Dashboard.js
index 38887f7ed..a65662853 100644
--- a/client/src/components/Dashboard/Dashboard.js
+++ b/client/src/components/Dashboard/Dashboard.js
@@ -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 (
-
+
@@ -35,9 +30,3 @@ function Dashboard({ sidebarExpended }) {
);
}
-
-export default compose(
- withDashboard(({ sidebarExpended }) => ({
- sidebarExpended,
- })),
-)(Dashboard);
diff --git a/client/src/components/Dashboard/DashboardViewsTabs.js b/client/src/components/Dashboard/DashboardViewsTabs.js
new file mode 100644
index 000000000..e82f2a7c3
--- /dev/null
+++ b/client/src/components/Dashboard/DashboardViewsTabs.js
@@ -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 (
+
+ {allTab && (
+ } onClick={handleViewLinkClick} />
+ )}
+ {mappedTabs.map((tab) => (
+
+ ))}
+
+
+ }
+ position={Position.RIGHT}
+ >
+ }
+ onClick={handleClickNewView}
+ minimal={true}
+ />
+
+
+
+ );
+}
+
+DashboardViewsTabs.propTypes = {
+ tabs: PropTypes.array.isRequired,
+ allTab: PropTypes.bool,
+ newViewTab: PropTypes.bool,
+
+ onNewViewTabClick: PropTypes.func,
+ onChange: PropTypes.func,
+ onTabClick: PropTypes.func,
+};
diff --git a/client/src/components/DialogReduxConnect.js b/client/src/components/DialogReduxConnect.js
index d40cc4f53..6960066e2 100644
--- a/client/src/components/DialogReduxConnect.js
+++ b/client/src/components/DialogReduxConnect.js
@@ -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 (
);
};
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(
diff --git a/client/src/components/DynamicFilter/DynamicFilterCompatatorField.js b/client/src/components/DynamicFilter/DynamicFilterCompatatorField.js
new file mode 100644
index 000000000..7559369d6
--- /dev/null
+++ b/client/src/components/DynamicFilter/DynamicFilterCompatatorField.js
@@ -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 (
+
+ );
+}
diff --git a/client/src/components/DynamicFilter/DynamicFilterCompatators.js b/client/src/components/DynamicFilter/DynamicFilterCompatators.js
new file mode 100644
index 000000000..1782c4387
--- /dev/null
+++ b/client/src/components/DynamicFilter/DynamicFilterCompatators.js
@@ -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];
+};
diff --git a/client/src/components/DynamicFilter/DynamicFilterValueField.js b/client/src/components/DynamicFilter/DynamicFilterValueField.js
index 4311ab46a..60a3f8da9 100644
--- a/client/src/components/DynamicFilter/DynamicFilterValueField.js
+++ b/client/src/components/DynamicFilter/DynamicFilterValueField.js
@@ -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 (
-
-
+
+ }
+ labelProp={'name'}
+ buttonProps={{ onClick: handleBtnClick }}
+ />
+
+
+
+
+
+
+
+
-
-
- }
- labelProp={'name'}
- buttonProps={{ onClick: handleBtnClick }}
- />
-
-
-
-
-
-
-
-
-
-
+
@@ -161,7 +184,7 @@ function DynamicFilterValueField({
}
const mapStateToProps = (state, props) => ({
- resourceName: props.fieldMeta.resource_key || 'account_type',
+ resourceName: props.dataResource,
});
const withResourceFilterValueField = connect(mapStateToProps);
diff --git a/client/src/components/FilterDropdown.js b/client/src/components/FilterDropdown.js
index c4bd9f055..f37dc4d58 100644
--- a/client/src/components/FilterDropdown.js
+++ b/client/src/components/FilterDropdown.js
@@ -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 (
@@ -190,18 +221,15 @@ export default function FilterDropdown({
/>
-
-
+
-
+
+
}
minimal={true}
diff --git a/client/src/components/Sidebar/SidebarContainer.js b/client/src/components/Sidebar/SidebarContainer.js
index dd241e162..9a0c428bf 100644
--- a/client/src/components/Sidebar/SidebarContainer.js
+++ b/client/src/components/Sidebar/SidebarContainer.js
@@ -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 (
{
+ const viewsMenuItems = manualJournalsViews.map((view) => {
return (
);
@@ -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({
>
}
- text={
}
+ icon={}
+ text={}
rightIcon={'caret-down'}
/>
@@ -95,27 +101,28 @@ function ManualJournalActionsBar({
}
- text={}
+ icon={}
+ text={}
onClick={onClickNewManualJournal}
/>
}
+ text="Filter"
+ icon={}
/>
}
- text={}
+ icon={}
+ text={}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
@@ -123,20 +130,27 @@ function ManualJournalActionsBar({
}
- text={}
+ icon={}
+ text={}
/>
}
- text={}
+ icon={}
+ text={}
/>
);
}
+const mapStateToProps = (state, props) => ({
+ resourceName: 'manual_journals',
+});
+
+const withManualJournalsActionsBar = connect(mapStateToProps);
+
export default compose(
+ withManualJournalsActionsBar,
withDialogActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
diff --git a/client/src/containers/Accounting/ManualJournalsList.js b/client/src/containers/Accounting/ManualJournalsList.js
index 232e499df..bf1b0926f 100644
--- a/client/src/containers/Accounting/ManualJournalsList.js
+++ b/client/src/containers/Accounting/ManualJournalsList.js
@@ -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 (
({
manualJournalsTableQuery,
})),
diff --git a/client/src/containers/Accounting/withManualJournals.js b/client/src/containers/Accounting/withManualJournals.js
index b4ce936c8..560ccd393 100644
--- a/client/src/containers/Accounting/withManualJournals.js
+++ b/client/src/containers/Accounting/withManualJournals.js
@@ -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,
diff --git a/client/src/containers/Accounts/AccountsActionsBar.js b/client/src/containers/Accounts/AccountsActionsBar.js
index 3bfe300db..36d6af5c1 100644
--- a/client/src/containers/Accounts/AccountsActionsBar.js
+++ b/client/src/containers/Accounts/AccountsActionsBar.js
@@ -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 ? (
) : (
- `${filterCount} filters applied`
+
)
}
icon={}
diff --git a/client/src/containers/Accounts/AccountsChart.js b/client/src/containers/Accounts/AccountsChart.js
index 929f05ea5..67be2af30 100644
--- a/client/src/containers/Accounts/AccountsChart.js
+++ b/client/src/containers/Accounts/AccountsChart.js
@@ -333,6 +333,7 @@ function AccountsChart({
{
+ 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 (
-
+
@@ -248,6 +256,8 @@ function AccountsDataTable({
}
export default compose(
+ withRouter,
+ withCurrentView,
withDialogActions,
withDashboardActions,
withAccountsActions,
diff --git a/client/src/containers/Accounts/AccountsViewsTabs.js b/client/src/containers/Accounts/AccountsViewsTabs.js
index 7ea300a66..a154c6930 100644
--- a/client/src/containers/Accounts/AccountsViewsTabs.js
+++ b/client/src/containers/Accounts/AccountsViewsTabs.js
@@ -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 = (
- { view.name }
- );
- return ;
- });
-
return (
-
+
-
- }
- onClick={handleViewLinkClick}
- />
- { tabs }
- }
- onClick={handleClickNewView}
- minimal={true}
- />
-
+
);
}
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);
diff --git a/client/src/containers/Accounts/withAccounts.js b/client/src/containers/Accounts/withAccounts.js
index e3755c7ca..67a53c731 100644
--- a/client/src/containers/Accounts/withAccounts.js
+++ b/client/src/containers/Accounts/withAccounts.js
@@ -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,
diff --git a/client/src/containers/Currencies/withCurrencies.js b/client/src/containers/Currencies/withCurrencies.js
index 7811a3620..ada37151a 100644
--- a/client/src/containers/Currencies/withCurrencies.js
+++ b/client/src/containers/Currencies/withCurrencies.js
@@ -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;
diff --git a/client/src/containers/Customers/withCustomers.js b/client/src/containers/Customers/withCustomers.js
index 82d943e4e..363d37906 100644
--- a/client/src/containers/Customers/withCustomers.js
+++ b/client/src/containers/Customers/withCustomers.js
@@ -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,
diff --git a/client/src/containers/Expenses/withExpenses.js b/client/src/containers/Expenses/withExpenses.js
index 2665a66a7..ad253a3fe 100644
--- a/client/src/containers/Expenses/withExpenses.js
+++ b/client/src/containers/Expenses/withExpenses.js
@@ -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,
diff --git a/client/src/containers/Items/withItems.js b/client/src/containers/Items/withItems.js
index 56f295f05..8286faf54 100644
--- a/client/src/containers/Items/withItems.js
+++ b/client/src/containers/Items/withItems.js
@@ -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,
diff --git a/client/src/containers/Resources/withResourceDetails.js b/client/src/containers/Resources/withResourceDetails.js
index 477ccb505..5f1fc3d56 100644
--- a/client/src/containers/Resources/withResourceDetails.js
+++ b/client/src/containers/Resources/withResourceDetails.js
@@ -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),
};
diff --git a/client/src/containers/Views/withCurrentView.js b/client/src/containers/Views/withCurrentView.js
new file mode 100644
index 000000000..c3456c73b
--- /dev/null
+++ b/client/src/containers/Views/withCurrentView.js
@@ -0,0 +1,7 @@
+import { connect } from 'react-redux';
+
+const mapStateToProps = (state, props) => ({
+ currentViewId: props.match.params.custom_view_id,
+});
+
+export default connect(mapStateToProps);
diff --git a/client/src/containers/Views/withViewDetails.js b/client/src/containers/Views/withViewDetails.js
index 4e67a4c00..5680c50f4 100644
--- a/client/src/containers/Views/withViewDetails.js
+++ b/client/src/containers/Views/withViewDetails.js
@@ -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),
};
};
diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js
index e00255fa8..4f6393dda 100644
--- a/client/src/lang/en/index.js
+++ b/client/src/lang/en/index.js
@@ -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',
};
diff --git a/client/src/store/accounts/accounts.selectors.js b/client/src/store/accounts/accounts.selectors.js
index 77f918e7f..608642a70 100644
--- a/client/src/store/accounts/accounts.selectors.js
+++ b/client/src/store/accounts/accounts.selectors.js
@@ -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) || []
- : [];
-};
\ No newline at end of file
+export const getAccountsItems = createSelector(
+ accountsViewsSelector,
+ accountsDataSelector,
+ accountsCurrentViewSelector,
+ (accountsViews, accountsItems, viewId) => {
+ const accountsView = accountsViews[viewId || -1];
+
+ return typeof accountsView === 'object'
+ ? pickItemsFromIds(accountsItems, accountsView.ids) || []
+ : [];
+ },
+);
diff --git a/client/src/store/currencies/currencies.selector.js b/client/src/store/currencies/currencies.selector.js
index b5bfd40fe..b523a0b0a 100644
--- a/client/src/store/currencies/currencies.selector.js
+++ b/client/src/store/currencies/currencies.selector.js
@@ -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;
diff --git a/client/src/store/customViews/customViews.selectors.js b/client/src/store/customViews/customViews.selectors.js
index 7df674845..bf950ff08 100644
--- a/client/src/store/customViews/customViews.selectors.js
+++ b/client/src/store/customViews/customViews.selectors.js
@@ -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;
-};
\ No newline at end of file
+ return typeof resourceViews[viewId] === 'undefined'
+ ? {}
+ : resourceViews[viewId].pages;
+};
diff --git a/client/src/store/customers/customers.selectors.js b/client/src/store/customers/customers.selectors.js
index 94fe8ad42..c34b910a0 100644
--- a/client/src/store/customers/customers.selectors.js
+++ b/client/src/store/customers/customers.selectors.js
@@ -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) || []
+ : [];
+ },
+);
\ No newline at end of file
diff --git a/client/src/store/dashboard/dashboard.selectors.js b/client/src/store/dashboard/dashboard.selectors.js
new file mode 100644
index 000000000..ce91fe0b4
--- /dev/null
+++ b/client/src/store/dashboard/dashboard.selectors.js
@@ -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;
+ },
+);
\ No newline at end of file
diff --git a/client/src/store/expenses/expenses.selectors.js b/client/src/store/expenses/expenses.selectors.js
index c48fd06f7..387efc32b 100644
--- a/client/src/store/expenses/expenses.selectors.js
+++ b/client/src/store/expenses/expenses.selectors.js
@@ -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) || [])
+ : [];
+ },
+);
diff --git a/client/src/store/resources/resources.reducer.js b/client/src/store/resources/resources.reducer.js
index 8a9bf1dc3..f814a8c84 100644
--- a/client/src/store/resources/resources.reducer.js
+++ b/client/src/store/resources/resources.reducer.js
@@ -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.
diff --git a/client/src/style/components/data-table.scss b/client/src/style/components/data-table.scss
index ff677e7a7..7d7ce08ed 100644
--- a/client/src/style/components/data-table.scss
+++ b/client/src/style/components/data-table.scss
@@ -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;
diff --git a/client/src/style/objects/typography.scss b/client/src/style/objects/typography.scss
index c48cea84f..5662bec67 100644
--- a/client/src/style/objects/typography.scss
+++ b/client/src/style/objects/typography.scss
@@ -1,7 +1,7 @@
body{
- color: #333;
+ color: #1f3255;
}
.#{$ns}-heading{
diff --git a/client/src/style/pages/accounts-chart.scss b/client/src/style/pages/accounts-chart.scss
index 93afeca4c..b876e5fab 100644
--- a/client/src/style/pages/accounts-chart.scss
+++ b/client/src/style/pages/accounts-chart.scss
@@ -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%;
diff --git a/client/src/style/pages/dashboard.scss b/client/src/style/pages/dashboard.scss
index ffc5cfd39..dbbfb770a 100644
--- a/client/src/style/pages/dashboard.scss
+++ b/client/src/style/pages/dashboard.scss
@@ -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;
}
}
diff --git a/client/src/style/views/Sidebar.scss b/client/src/style/views/Sidebar.scss
index 9d8f77d76..a4a99081f 100644
--- a/client/src/style/views/Sidebar.scss
+++ b/client/src/style/views/Sidebar.scss
@@ -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{
diff --git a/client/src/style/views/filter-dropdown.scss b/client/src/style/views/filter-dropdown.scss
index 95b45ed1c..d4f4555a2 100644
--- a/client/src/style/views/filter-dropdown.scss
+++ b/client/src/style/views/filter-dropdown.scss
@@ -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){
diff --git a/client/src/utils.js b/client/src/utils.js
index 38f5d7530..daf69213c 100644
--- a/client/src/utils.js
+++ b/client/src/utils.js
@@ -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));
+ });
}
\ No newline at end of file
diff --git a/server/src/data/ResourceFieldsKeys.js b/server/src/data/ResourceFieldsKeys.js
index 4772146b7..618c58d6e 100644
--- a/server/src/data/ResourceFieldsKeys.js
+++ b/server/src/data/ResourceFieldsKeys.js
@@ -40,6 +40,11 @@ export default {
},
'created_at': {
column: 'created_at',
+ columnType: 'date',
+ },
+ active: {
+ column: 'active',
+
},
},
diff --git a/server/src/database/migrations/20190822214905_create_resource_fields_table.js b/server/src/database/migrations/20190822214905_create_resource_fields_table.js
index eaed02ad8..1796ffe63 100644
--- a/server/src/database/migrations/20190822214905_create_resource_fields_table.js
+++ b/server/src/database/migrations/20190822214905_create_resource_fields_table.js
@@ -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(() => {
diff --git a/server/src/database/seeds/seed_accounts_fields.js b/server/src/database/seeds/seed_accounts_fields.js
index 2ad1974e1..f89f58699 100644
--- a/server/src/database/seeds/seed_accounts_fields.js
+++ b/server/src/database/seeds/seed_accounts_fields.js
@@ -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,
+ },
]);
});
};
diff --git a/server/src/database/seeds/seed_resources.js b/server/src/database/seeds/seed_resources.js
index 546da4568..e868fa87a 100644
--- a/server/src/database/seeds/seed_resources.js
+++ b/server/src/database/seeds/seed_resources.js
@@ -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' },
diff --git a/server/src/database/seeds/seed_resources_fields.js b/server/src/database/seeds/seed_resources_fields.js
index a473d48fd..203d290fe 100644
--- a/server/src/database/seeds/seed_resources_fields.js
+++ b/server/src/database/seeds/seed_resources_fields.js
@@ -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
{
diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js
index a5776c614..6a0665fd9 100644
--- a/server/src/http/controllers/Accounts.js
+++ b/server/src/http/controllers/Accounts.js
@@ -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);
diff --git a/server/src/lib/ViewRolesBuilder/index.js b/server/src/lib/ViewRolesBuilder/index.js
index 0e94533c5..7703bde1b 100644
--- a/server/src/lib/ViewRolesBuilder/index.js
+++ b/server/src/lib/ViewRolesBuilder/index.js
@@ -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);
+ }
}
/**