diff --git a/client/src/components/ContextMenu.js b/client/src/components/ContextMenu.js
new file mode 100644
index 000000000..10dca8452
--- /dev/null
+++ b/client/src/components/ContextMenu.js
@@ -0,0 +1,52 @@
+import React, { memo } from 'react';
+import { Popover, Position, Classes } from '@blueprintjs/core';
+import { saveInvoke } from 'utils';
+
+const POPPER_MODIFIERS = {
+ preventOverflow: { boundariesElement: 'viewport' },
+};
+
+function ContextMenu(props) {
+ const { bindMenu, isOpen, children, onClosed, popoverProps } = props;
+
+ const handleClosed = () => {
+ requestAnimationFrame(() => saveInvoke(onClosed));
+ };
+
+ const handleInteraction = (nextOpenState) => {
+ if (!nextOpenState) {
+ // Delay the actual hiding till the event queue clears
+ // to avoid flicker of opening twice
+ requestAnimationFrame(() => saveInvoke(onClosed));
+ }
+ };
+
+ return (
+
+
}
+ usePortal={false}
+ onInteraction={handleInteraction}
+ {...popoverProps}
+ />
+
+ );
+}
+
+export default memo(ContextMenu, (prevProps, nextProps) => {
+ if (
+ prevProps.isOpen === nextProps.isOpen &&
+ prevProps.bindMenu.style === nextProps.bindMenu.style
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+});
diff --git a/client/src/components/ContextMenu.tsx b/client/src/components/ContextMenu.tsx
deleted file mode 100644
index 94ec49b83..000000000
--- a/client/src/components/ContextMenu.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// import classNames from 'classnames';
-import * as React from "react";
-import * as ReactDOM from "react-dom";
-// import { polyfill } from "react-lifecycles-compat";
-
-import {
- Popover,
- Classes,
- Position,
-} from '@blueprintjs/core';
-
-
-// import { IOverlayLifecycleProps } from "../overlay/overlay";
-// import { Popover } from "../popover/popover";
-// import { PopperModifiers } from "../popover/popoverSharedProps";
-
-export interface IOffset {
- left: number;
- top: number;
-}
-
-interface IContextMenuState {
- isOpen: boolean;
- isDarkTheme: boolean;
- menu?: JSX.Element;
- offset?: IOffset;
- onClose?: () => void;
-}
-
-const POPPER_MODIFIERS = {
- preventOverflow: { boundariesElement: "viewport" },
-};
-const TRANSITION_DURATION = 100;
-
-// type IContextMenuProps = IOverlayLifecycleProps;
-
-/* istanbul ignore next */
-
-export default class ContextMenu extends React.PureComponent {
- public state: IContextMenuState = {
- isDarkTheme: false,
- isOpen: false,
- };
-
- public render() {
- // prevent right-clicking in a context menu
- const content = {this.state.menu}
;
- const popoverClassName = {};
-
- // HACKHACK: workaround until we have access to Popper#scheduleUpdate().
- // https://github.com/palantir/blueprint/issues/692
- // Generate key based on offset so a new Popover instance is created
- // when offset changes, to force recomputing position.
- const key = this.state.offset === undefined ? "" : `${this.state.offset.left}x${this.state.offset.top}`;
-
- // wrap the popover in a positioned div to make sure it is properly
- // offset on the screen.
- return (
-
-
}
- transitionDuration={TRANSITION_DURATION}
- />
-
- );
-
- }
-
- public show(menu: JSX.Element, offset: IOffset, onClose?: () => void, isDarkTheme = false) {
- this.setState({ isOpen: true, menu, offset, onClose, isDarkTheme });
- }
-
- public hide() {
- this.state.onClose?.();
- this.setState({ isOpen: false, onClose: undefined });
- }
-
- private cancelContextMenu = (e: React.SyntheticEvent) => e.preventDefault();
-
- private handleBackdropContextMenu = (e: React.MouseEvent) => {
- // React function to remove from the event pool, useful when using a event within a callback
- e.persist();
- e.preventDefault();
- // wait for backdrop to disappear so we can find the "real" element at event coordinates.
- // timeout duration is equivalent to transition duration so we know it's animated out.
- setTimeout(() => {
- // retrigger context menu event at the element beneath the backdrop.
- // if it has a `contextmenu` event handler then it'll be invoked.
- // if it doesn't, no native menu will show (at least on OSX) :(
- const newTarget = document.elementFromPoint(e.clientX, e.clientY);
- const { view, ...newEventInit } = e;
- newTarget?.dispatchEvent(new MouseEvent("contextmenu", newEventInit));
- }, TRANSITION_DURATION);
- };
-
- private handlePopoverInteraction = (nextOpenState: boolean) => {
- if (!nextOpenState) {
- // delay the actual hiding till the event queue clears
- // to avoid flicker of opening twice
- this.hide();
- }
- };
-}
\ No newline at end of file
diff --git a/client/src/components/Dashboard/DashboardActionViewsList.js b/client/src/components/Dashboard/DashboardActionViewsList.js
index 17a3f0d98..0e42a8b9c 100644
--- a/client/src/components/Dashboard/DashboardActionViewsList.js
+++ b/client/src/components/Dashboard/DashboardActionViewsList.js
@@ -10,22 +10,20 @@ import {
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
-import { useHistory } from 'react-router-dom';
import { Icon } from 'components';
+/**
+ * Dashboard action views list.
+ */
export default function DashboardActionViewsList({
resourceName,
views,
onChange,
}) {
- const history = useHistory();
-
const handleClickViewItem = (view) => {
- history.push(
- view ? `/${resourceName}/${view.id}/custom_view` : '/accounts',
- );
onChange && onChange(view);
};
+
const viewsMenuItems = views.map((view) => {
return (
handleClickViewItem(view)} text={view.name} />
diff --git a/client/src/components/Dashboard/DashboardTopbar.js b/client/src/components/Dashboard/DashboardTopbar.js
index 0aa03bc72..24eb0396f 100644
--- a/client/src/components/Dashboard/DashboardTopbar.js
+++ b/client/src/components/Dashboard/DashboardTopbar.js
@@ -19,13 +19,14 @@ import { Icon, Hint, If } from 'components';
import withSearch from 'containers/GeneralSearch/withSearch';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
+import withSettings from 'containers/Settings/withSettings';
+
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
function DashboardTopbar({
// #withDashboard
pageTitle,
- pageSubtitle,
editViewId,
// #withDashboardActions
@@ -35,6 +36,9 @@ function DashboardTopbar({
// #withDashboard
sidebarExpended,
+ // #withSettings
+ organizationName,
+
// #withGlobalSearch
openGlobalSearch,
}) {
@@ -100,11 +104,7 @@ function DashboardTopbar({
-
- {pageSubtitle}
-
-
-
+
}
@@ -117,6 +117,10 @@ function DashboardTopbar({
+ {/*
+ { organizationName }
+
*/}
+
@@ -158,11 +162,13 @@ function DashboardTopbar({
export default compose(
withSearch,
- withDashboard(({ pageTitle, pageSubtitle, editViewId, sidebarExpended }) => ({
+ withDashboard(({ pageTitle, editViewId, sidebarExpended }) => ({
pageTitle,
- pageSubtitle,
editViewId,
sidebarExpended,
})),
+ withSettings(({ organizationSettings }) => ({
+ organizationName: organizationSettings.name,
+ })),
withDashboardActions,
)(DashboardTopbar);
diff --git a/client/src/components/Dashboard/DashboardViewsTabs.js b/client/src/components/Dashboard/DashboardViewsTabs.js
index 92fbfcb85..bb526d7f8 100644
--- a/client/src/components/Dashboard/DashboardViewsTabs.js
+++ b/client/src/components/Dashboard/DashboardViewsTabs.js
@@ -1,15 +1,19 @@
-import React, { useState, useRef, useMemo } from 'react';
+import React, { useRef, useState, useEffect } from 'react';
import { FormattedMessage as T } from 'react-intl';
import PropTypes from 'prop-types';
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
-import { debounce } from 'lodash';
import { useHistory } from 'react-router';
+import { debounce } from 'lodash';
import { If, Icon } from 'components';
-import { saveInvoke } from 'utils';
+import { saveInvoke } from 'utils';
+/**
+ * Dashboard views tabs.
+ *
+ */
export default function DashboardViewsTabs({
initialViewId = 0,
- viewId,
+ currentViewId,
tabs,
defaultTabText = ,
allTab = true,
@@ -17,41 +21,38 @@ export default function DashboardViewsTabs({
resourceName,
onNewViewTabClick,
onChange,
- onTabClick,
+ OnThrottledChange,
+ throttleTime = 250,
}) {
const history = useHistory();
const [currentView, setCurrentView] = useState(initialViewId || 0);
+ useEffect(() => {
+ if (typeof currentViewId !== 'undefined' && currentViewId !== currentView) {
+ setCurrentView(currentViewId || 0);
+ }
+ }, [currentView, setCurrentView, currentViewId]);
+
+ const throttledOnChange = useRef(
+ debounce((viewId) => saveInvoke(OnThrottledChange, viewId), throttleTime),
+ );
+
+ // Trigger `onChange` and `onThrottledChange` events.
+ const triggerOnChange = (viewId) => {
+ saveInvoke(onChange, viewId);
+ throttledOnChange.current(viewId);
+ };
+
+ // Handles click a new view.
const handleClickNewView = () => {
history.push(`/custom_views/${resourceName}/new`);
onNewViewTabClick && onNewViewTabClick();
};
- const handleTabClick = (viewId) => {
- saveInvoke(onTabClick, viewId);
- };
-
- const mappedTabs = useMemo(
- () => tabs.map((tab) => ({ ...tab, onTabClick: handleTabClick })),
- [tabs, handleTabClick],
- );
-
- const handleViewLinkClick = () => {
- saveInvoke(onNewViewTabClick);
- };
-
- const debounceChangeHistory = useRef(
- debounce((toUrl) => {
- history.push(toUrl);
- }, 250),
- );
-
+ // Handle tabs change.
const handleTabsChange = (viewId) => {
- const toPath = viewId ? `${viewId}/custom_view` : '';
- debounceChangeHistory.current(`/${resourceName}/${toPath}`);
-
setCurrentView(viewId);
- saveInvoke(onChange, viewId);
+ triggerOnChange(viewId)
};
return (
@@ -62,13 +63,11 @@ export default function DashboardViewsTabs({
className="tabs--dashboard-views"
onChange={handleTabsChange}
>
- {allTab && (
-
- )}
- {mappedTabs.map((tab) => (
-
- ))}
+ {allTab && }
+ {tabs.map((tab) => (
+
+ ))}
}
@@ -93,5 +92,6 @@ DashboardViewsTabs.propTypes = {
onNewViewTabClick: PropTypes.func,
onChange: PropTypes.func,
- onTabClick: PropTypes.func,
+ OnThrottledChange: PropTypes.func,
+ throttleTime: PropTypes.number,
};
diff --git a/client/src/components/DataTable.js b/client/src/components/DataTable.js
index b6d07e4a1..fbe5a3450 100644
--- a/client/src/components/DataTable.js
+++ b/client/src/components/DataTable.js
@@ -75,6 +75,8 @@ export default function DataTable(props) {
TableWrapperRenderer,
TableTBodyRenderer,
TablePaginationRenderer,
+
+ ...restProps
} = props;
const selectionColumnObj = {
@@ -117,6 +119,8 @@ export default function DataTable(props) {
autoResetSortBy,
autoResetFilters,
autoResetRowState,
+
+ ...restProps
},
useSortBy,
useExpanded,
diff --git a/client/src/components/Datatable/TableHeader.js b/client/src/components/Datatable/TableHeader.js
index 145296386..9642785e7 100644
--- a/client/src/components/Datatable/TableHeader.js
+++ b/client/src/components/Datatable/TableHeader.js
@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { ScrollSyncPane } from 'react-scroll-sync';
import { If } from 'components';
import TableContext from './TableContext';
+import MaterialProgressBar from 'components/MaterialProgressBar';
function TableHeaderCell({ column, index }) {
const {
@@ -77,7 +78,7 @@ function TableHeaderGroup({ headerGroup }) {
export default function TableHeader() {
const {
table: { headerGroups, page },
- props: { TableHeaderSkeletonRenderer, headerLoading },
+ props: { TableHeaderSkeletonRenderer, headerLoading, progressBarLoading },
} = useContext(TableContext);
if (headerLoading && TableHeaderSkeletonRenderer) {
@@ -89,6 +90,9 @@ export default function TableHeader() {
{headerGroups.map((headerGroup) => (
))}
+
+
+
);
diff --git a/client/src/components/Datatable/TablePagination.js b/client/src/components/Datatable/TablePagination.js
index ff1143192..f1edf56ff 100644
--- a/client/src/components/Datatable/TablePagination.js
+++ b/client/src/components/Datatable/TablePagination.js
@@ -1,11 +1,12 @@
import React, { useCallback, useContext } from 'react';
import { If, Pagination } from 'components';
import TableContext from './TableContext';
+import { saveInvoke } from 'utils';
/**
* Table pagination.
*/
-export default function TablePagination({}) {
+export default function TablePagination() {
const {
table: {
gotoPage,
@@ -13,28 +14,39 @@ export default function TablePagination({}) {
pageCount,
state: { pageIndex, pageSize },
},
- props: { pagination, loading },
+ props: { pagination, loading, onPaginationChange },
} = useContext(TableContext);
+ const triggerOnPaginationChange = useCallback((payload) => {
+ saveInvoke(onPaginationChange, payload)
+ }, [onPaginationChange]);
+
+ // Handles the page changing.
const handlePageChange = useCallback(
- (currentPage) => {
- gotoPage(currentPage - 1);
+ ({ page, pageSize }) => {
+ const pageIndex = page - 1;
+
+ gotoPage(pageIndex);
+ triggerOnPaginationChange({ page, pageSize });
},
- [gotoPage],
+ [gotoPage, triggerOnPaginationChange],
);
+ // Handles the page size changing.
const handlePageSizeChange = useCallback(
- (pageSize, currentPage) => {
+ ({ pageSize, page }) => {
gotoPage(0);
setPageSize(pageSize);
+
+ triggerOnPaginationChange({ page, pageSize });
},
- [gotoPage, setPageSize],
+ [gotoPage, setPageSize, triggerOnPaginationChange],
);
return (
(e) => {
- if (typeof rowContextMenu === 'function') {
- e.preventDefault();
- const tr = e.currentTarget.closest('.tr');
- tr.classList.add('is-context-menu-active');
+ const [
+ bindMenu,
+ bindMenuItem,
+ useContextTrigger,
+ { coords, setVisible, isVisible },
+ ] = useContextMenu();
- const DropdownEl = rowContextMenu({ row });
-
- ContextMenu.show(DropdownEl, { left: e.clientX, top: e.clientY }, () => {
- tr.classList.remove('is-context-menu-active');
- });
- }
- };
+ const [bindTrigger] = useContextTrigger({
+ collect: () => 'Title',
+ });
return (
{row.cells.map((cell, index) => (
))}
+
+
setVisible(false)}
+ >
+
+
);
}
diff --git a/client/src/components/MaterialProgressBar.js b/client/src/components/MaterialProgressBar.js
new file mode 100644
index 000000000..ccad69291
--- /dev/null
+++ b/client/src/components/MaterialProgressBar.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import 'style/components/MaterialProgressBar.scss';
+
+export default function MaterialProgressBar() {
+ return (
+
+ );
+}
diff --git a/client/src/components/Pagination.js b/client/src/components/Pagination.js
index 806356bab..cfea6da6c 100644
--- a/client/src/components/Pagination.js
+++ b/client/src/components/Pagination.js
@@ -1,6 +1,6 @@
import React, { useReducer, useEffect } from 'react';
import classNames from 'classnames';
-import { Button, ButtonGroup, Intent, HTMLSelect, } from '@blueprintjs/core';
+import { Button, ButtonGroup, Intent, HTMLSelect } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import PropTypes from 'prop-types';
import { range } from 'lodash';
@@ -8,6 +8,12 @@ import { Icon } from 'components';
import 'style/components/DataTable/Pagination.scss';
+const TYPE = {
+ PAGE_CHANGE: 'PAGE_CHANGE',
+ PAGE_SIZE_CHANGE: 'PAGE_SIZE_CHANGE',
+ INITIALIZE: 'INITIALIZE',
+};
+
const getState = ({ currentPage, size, total }) => {
const totalPages = Math.ceil(total / size);
const visibleItems = 5;
@@ -16,7 +22,7 @@ const getState = ({ currentPage, size, total }) => {
// create an array of pages to ng-repeat in the pager control
let startPage, endPage;
if (totalPages <= visibleItems) {
- // less than {visibleItems} total pages so show
+ // less than {visibleItems} total pages so show
startPage = 1;
endPage = totalPages;
} else {
@@ -50,11 +56,6 @@ const getState = ({ currentPage, size, total }) => {
};
};
-const TYPE = {
- PAGE_CHANGE: 'PAGE_CHANGE',
- PAGE_SIZE_CHANGE: 'PAGE_SIZE_CHANGE',
- INITIALIZE: 'INITIALIZE',
-};
const reducer = (state, action) => {
switch (action.type) {
case TYPE.PAGE_CHANGE:
@@ -63,15 +64,15 @@ const reducer = (state, action) => {
size: state.size,
total: state.total,
});
- case TYPE.PAGE_SIZE_CHANGE:
+ case TYPE.PAGE_SIZE_CHANGE:
return getState({
currentPage: state.currentPage,
size: action.size,
total: state.total,
});
- case TYPE.INITIALIZE:
+ case TYPE.INITIALIZE:
return getState({
- currentPage: state.currentPage,
+ currentPage: action.page,
size: action.size,
total: action.total,
});
@@ -80,82 +81,92 @@ const reducer = (state, action) => {
}
};
-const Pagination = ({
- initialPage,
+function Pagination({
+ currentPage,
total,
size,
pageSizesOptions = [5, 12, 20, 30, 50, 75, 100, 150],
onPageChange,
onPageSizeChange,
-}) => {
+}) {
const [state, dispatch] = useReducer(
reducer,
- { currentPage: initialPage, total, size },
+ { currentPage, total, size },
getState,
);
useEffect(() => {
dispatch({
- type: 'INITIALIZE',
+ type: TYPE.INITIALIZE,
total,
size,
+ page: currentPage,
});
- }, [total, size]);
+ }, [total, size, currentPage]);
return (
);
-};
+}
Pagination.propTypes = {
- initialPage: PropTypes.number.isRequired,
+ currentPage: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
total: PropTypes.number.isRequired,
onPageChange: PropTypes.func,
@@ -213,7 +226,7 @@ Pagination.propTypes = {
};
Pagination.defaultProps = {
- initialPage: 1,
+ currentPage: 1,
size: 25,
};
diff --git a/client/src/components/index.js b/client/src/components/index.js
index 3084f8c9b..c97be1dfa 100644
--- a/client/src/components/index.js
+++ b/client/src/components/index.js
@@ -45,7 +45,7 @@ import PageFormBigNumber from './PageFormBigNumber';
import AccountsMultiSelect from './AccountsMultiSelect';
import CustomersMultiSelect from './CustomersMultiSelect';
import Skeleton from './Skeleton'
-
+import ContextMenu from './ContextMenu'
import TableFastCell from './Datatable/TableFastCell';
const Hint = FieldHint;
@@ -99,4 +99,5 @@ export {
CustomersMultiSelect,
TableFastCell,
Skeleton,
+ ContextMenu
};
diff --git a/client/src/containers/Alerts/Items/ItemDeleteAlert.js b/client/src/containers/Alerts/Items/ItemDeleteAlert.js
index 16a9c25cc..93e6a2a96 100644
--- a/client/src/containers/Alerts/Items/ItemDeleteAlert.js
+++ b/client/src/containers/Alerts/Items/ItemDeleteAlert.js
@@ -1,20 +1,18 @@
-import React, { useState } from 'react';
+import React from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
-import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import { handleDeleteErrors } from 'containers/Items/utils';
+import { useDeleteItem } from 'hooks/query';
-import {
- useDeleteItem
-} from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
+import withItemsActions from 'containers/Items/withItemsActions';
import { compose } from 'utils';
@@ -30,15 +28,19 @@ function ItemDeleteAlert({
// #withAlertActions
closeAlert,
+
+ // #withItemsActions
+ addItemsTableQueries
}) {
const { mutateAsync: deleteItem, isLoading } = useDeleteItem();
const { formatMessage } = useIntl();
- // handle cancel delete item alert.
+ // Handle cancel delete item alert.
const handleCancelItemDelete = () => {
closeAlert(name);
};
+ // Handle confirm delete item.
const handleConfirmDeleteItem = () => {
deleteItem(itemId)
.then(() => {
@@ -48,6 +50,8 @@ function ItemDeleteAlert({
}),
intent: Intent.SUCCESS,
});
+ // Reset to page number one.
+ addItemsTableQueries({ page: 1 });
})
.catch(({ errors }) => {
handleDeleteErrors(errors);
@@ -80,4 +84,5 @@ function ItemDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
+ withItemsActions
)(ItemDeleteAlert);
diff --git a/client/src/containers/InventoryAdjustments/InventoryAdjustmentDataTable.js b/client/src/containers/InventoryAdjustments/InventoryAdjustmentDataTable.js
index ad06fd434..4a2ce1330 100644
--- a/client/src/containers/InventoryAdjustments/InventoryAdjustmentDataTable.js
+++ b/client/src/containers/InventoryAdjustments/InventoryAdjustmentDataTable.js
@@ -179,6 +179,7 @@ function InventoryAdjustmentDataTable({
autoResetSortBy={false}
autoResetPage={false}
isLoading={isLoading}
+ noResults={'There is no inventory adjustments transactions yet.'}
// pagesCount={inventoryAdjustmentsPagination.pagesCount}
// initialPageSize={inventoryAdjustmentsPagination.pageSize}
// initialPageIndex={inventoryAdjustmentsPagination.page - 1}
diff --git a/client/src/containers/Items/ItemFormPage.js b/client/src/containers/Items/ItemFormPage.js
index 50d4399bd..ab2098229 100644
--- a/client/src/containers/Items/ItemFormPage.js
+++ b/client/src/containers/Items/ItemFormPage.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { ItemFormProvider } from './ItemFormProvider';
@@ -12,8 +12,21 @@ import { compose } from 'utils';
/**
* Item form page.
*/
-function ItemFormPage() {
+function ItemFormPage({
+ // #withDashboardActions
+ setDashboardBackLink
+}) {
const { id } = useParams();
+
+ useEffect(() => {
+ // Show the back link on dashboard topbar.
+ setDashboardBackLink(true);
+
+ return () => {
+ // Hide the back link on dashboard topbar.
+ setDashboardBackLink(false);
+ };
+ }, [setDashboardBackLink]);
return (
@@ -24,4 +37,6 @@ function ItemFormPage() {
);
}
-export default compose(withDashboardActions)(ItemFormPage);
+export default compose(
+ withDashboardActions,
+)(ItemFormPage);
diff --git a/client/src/containers/Items/ItemsActionsBar.js b/client/src/containers/Items/ItemsActionsBar.js
index 7d2b41f13..fadd462d7 100644
--- a/client/src/containers/Items/ItemsActionsBar.js
+++ b/client/src/containers/Items/ItemsActionsBar.js
@@ -53,7 +53,7 @@ function ItemsActionsBar({
// Handle tab changing.
const handleTabChange = (viewId) => {
addItemsTableQueries({
- custom_view_id: viewId.id || null,
+ customViewId: viewId.id || null,
});
};
@@ -87,7 +87,7 @@ function ItemsActionsBar({
>
}
/>
diff --git a/client/src/containers/Items/ItemsDataTable.js b/client/src/containers/Items/ItemsDataTable.js
index 36bd23f6a..aade4cf76 100644
--- a/client/src/containers/Items/ItemsDataTable.js
+++ b/client/src/containers/Items/ItemsDataTable.js
@@ -18,6 +18,7 @@ import {
CostPriceCell,
ItemTypeAccessor,
ItemsActionsTableCell,
+ ItemsActionMenuList
} from './components';
// Items datatable.
@@ -118,11 +119,14 @@ function ItemsDataTable({
manualSortBy={true}
pagesCount={1}
autoResetSortBy={false}
- autoResetPage={false}
+ autoResetPage={true}
+ manualPagination={true}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
- initialPageSize={pagination.pageSize}
- initialPageIndex={pagination.page}
+
+ pageSize={pagination.pageSize}
+ pageIndex={pagination.page - 1}
+ ContextMenu={ItemsActionMenuList}
{...tableProps}
/>
diff --git a/client/src/containers/Items/ItemsListProvider.js b/client/src/containers/Items/ItemsListProvider.js
index c0793cd53..02b7b844e 100644
--- a/client/src/containers/Items/ItemsListProvider.js
+++ b/client/src/containers/Items/ItemsListProvider.js
@@ -1,13 +1,21 @@
import React, { useEffect, createContext } from 'react';
import { useIntl } from 'react-intl';
-import { isEmpty } from 'lodash';
+
+import { transformTableQueryToParams, isTableEmptyStatus } from 'utils';
+
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useItems } from 'hooks/query';
import { useDashboardPageTitle } from 'hooks/state';
const ItemsContext = createContext();
-function ItemsListProvider({ query, ...props }) {
+/**
+ * Items list provider.
+ */
+function ItemsListProvider({
+ query,
+ ...props
+}) {
// Fetch accounts resource views and fields.
const { data: itemsViews, isFetching: isViewsLoading } = useResourceViews(
'items',
@@ -21,11 +29,16 @@ function ItemsListProvider({ query, ...props }) {
// Handle fetching the items table based on the given query.
const {
data: { items, pagination, filterMeta },
- isFetching: isItemsLoading,
- } = useItems(query);
+ isFetching: isItemsFetching,
+ isLoading: isItemsLoading,
+ } = useItems({
+ ...transformTableQueryToParams(query)
+ }, { keepPreviousData: true });
// Detarmines the datatable empty status.
- const isEmptyStatus = isEmpty(items) && !isItemsLoading && !filterMeta.view;
+ const isEmptyStatus = isTableEmptyStatus({
+ data: items, pagination, filterMeta,
+ }) && !isItemsFetching;
// Format message intl.
const { formatMessage } = useIntl();
@@ -42,15 +55,15 @@ function ItemsListProvider({ query, ...props }) {
itemsFields,
items,
pagination,
-
isViewsLoading,
isItemsLoading,
- isEmptyStatus: false,
+ isItemsFetching: isItemsFetching,
+ isEmptyStatus,
};
return (
diff --git a/client/src/containers/Items/ItemsViewPage.js b/client/src/containers/Items/ItemsViewPage.js
index b345ab4e5..9933ed1a8 100644
--- a/client/src/containers/Items/ItemsViewPage.js
+++ b/client/src/containers/Items/ItemsViewPage.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { Switch, Route, useHistory } from 'react-router-dom';
+import React, { useMemo } from 'react';
+import { useHistory } from 'react-router-dom';
import ItemsViewsTabs from './ItemsViewsTabs';
import ItemsDataTable from './ItemsDataTable';
@@ -7,9 +7,39 @@ import ItemsDataTable from './ItemsDataTable';
import withItemsActions from 'containers/Items/withItemsActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
+import withItems from 'containers/Items/withItems';
import { compose } from 'utils';
+import { useItemsListContext } from './ItemsListProvider';
+function transformPaginationToProps(pagination) {
+ const { page, pageSize, total } = pagination;
+
+ return {
+ initialPageIndex: Math.max(page - 1, 0),
+ initialPageSize: pageSize,
+ pagesCount: Math.ceil(total / pageSize),
+ };
+}
+
+function transformPaginationToQuery(query) {
+ const { pageSize, pageIndex, sortBy } = query;
+
+ return {
+ page_size: pageSize,
+ page: pageIndex + 1,
+ ...(sortBy.length > 0
+ ? {
+ column_sort_by: sortBy[0].id,
+ sort_order: sortBy[0].desc ? 'desc' : 'asc',
+ }
+ : {}),
+ };
+}
+
+/**
+ * Items view page.
+ */
function ItemsViewPage({
// #withAlertsActions.
openAlert,
@@ -19,10 +49,14 @@ function ItemsViewPage({
// #withItemsActions.
setSelectedRowsItems,
- addItemsTableQueries
+ addItemsTableQueries,
+
+ itemsTableQuery,
}) {
const history = useHistory();
+ const { pagination, isItemsFetching } = useItemsListContext();
+
// Handle delete action Item.
const handleDeleteItem = ({ id }) => {
openAlert('item-delete', { itemId: id });
@@ -52,49 +86,49 @@ function ItemsViewPage({
// Handle item make adjustment.
const handleMakeAdjustment = ({ id }) => {
openDialog('inventory-adjustment', { itemId: id });
- }
+ };
// Handle fetch data once the page index, size or sort by of the table change.
- const handleFetchData = ({ pageIndex, pageSize, sortBy }) => {
+ const handlePaginationChange = ({ pageSize, page }) => {
addItemsTableQueries({
- page_size: pageSize,
- page: pageIndex,
- ...(sortBy.length > 0
- ? {
- column_sort_by: sortBy[0].id,
- sort_order: sortBy[0].desc ? 'desc' : 'asc',
- }
- : {}),
+ // ...transformPaginationToQuery(query),
+ page,
+ pageSize,
});
};
+ const controlledState = (state) => ({
+ ...state,
+ pageIndex: itemsTableQuery.page - 1,
+ });
+
return (
-
-
-
-
-
-
+ <>
+
+
+ >
);
}
export default compose(
withAlertsActions,
withItemsActions,
- withDialogActions
+ withDialogActions,
+ withItems(({ itemsTableQuery }) => ({ itemsTableQuery })),
)(ItemsViewPage);
diff --git a/client/src/containers/Items/ItemsViewsTabs.js b/client/src/containers/Items/ItemsViewsTabs.js
index 7be8ebe95..ebe4d2fcf 100644
--- a/client/src/containers/Items/ItemsViewsTabs.js
+++ b/client/src/containers/Items/ItemsViewsTabs.js
@@ -1,11 +1,13 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
-import { useParams } from 'react-router-dom';
import { compose } from 'utils';
import { DashboardViewsTabs } from 'components';
import { pick } from 'lodash';
+import { withRouter } from 'react-router-dom';
import withItemsActions from 'containers/Items/withItemsActions';
+import withItems from 'containers/Items/withItems';
+
import { useItemsListContext } from './ItemsListProvider';
/**
@@ -14,17 +16,22 @@ import { useItemsListContext } from './ItemsListProvider';
function ItemsViewsTabs({
// #withItemsActions
addItemsTableQueries,
+
+ // #withItems
+ itemsTableQuery
}) {
- const { custom_view_id: customViewId = null } = useParams();
const { itemsViews } = useItemsListContext();
+ // Mapped items views.
const tabs = itemsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
+ // Handles the active tab change.
const handleTabChange = (viewId) => {
addItemsTableQueries({
- custom_view_id: viewId || null,
+ page: 1,
+ customViewId: viewId || null,
});
};
@@ -32,7 +39,7 @@ function ItemsViewsTabs({
({ itemsTableQuery })),
withItemsActions,
)(ItemsViewsTabs);
diff --git a/client/src/containers/Items/components.js b/client/src/containers/Items/components.js
index 74e56ecd9..f7978a003 100644
--- a/client/src/containers/Items/components.js
+++ b/client/src/containers/Items/components.js
@@ -71,7 +71,7 @@ export const ItemTypeAccessor = (row) => {
) : null;
};
-export const ItemsActionMenuList = ({
+export function ItemsActionMenuList({
row: { original },
payload: {
onEditItem,
@@ -80,9 +80,9 @@ export const ItemsActionMenuList = ({
onMakeAdjustment,
onDeleteItem,
},
-}) => {
+}) {
const { formatMessage } = useIntl();
-
+
return (
}
onClick={safeCallback(onMakeAdjustment, original)}
/>
diff --git a/client/src/containers/Items/withItems.js b/client/src/containers/Items/withItems.js
index 116219311..38681b06b 100644
--- a/client/src/containers/Items/withItems.js
+++ b/client/src/containers/Items/withItems.js
@@ -1,30 +1,15 @@
import {connect} from 'react-redux';
import {
- getResourceViews,
-} from 'store/customViews/customViews.selectors'
-import {
- getItemsCurrentPageFactory,
- getItemsPaginationMetaFactory,
getItemsTableQueryFactory,
- getItemsCurrentViewIdFactory
} from 'store/items/items.selectors';
export default (mapState) => {
- const getItemsCurrentPage = getItemsCurrentPageFactory();
- const getItemsPaginationMeta = getItemsPaginationMetaFactory();
const getItemsTableQuery = getItemsTableQueryFactory();
- const getItemsCurrentViewId = getItemsCurrentViewIdFactory();
-
+
const mapStateToProps = (state, props) => {
const mapped = {
- itemsViews: getResourceViews(state, props, 'items'),
- itemsCurrentPage: getItemsCurrentPage(state, props),
- itemsBulkSelected: state.items.bulkActions,
- itemsTableLoading: state.items.loading,
itemsSelectedRows: state.items.selectedRows,
itemsTableQuery: getItemsTableQuery(state, props),
- itemsPagination: getItemsPaginationMeta(state, props),
- itemsCurrentViewId: getItemsCurrentViewId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
diff --git a/client/src/containers/Items/withItemsActions.js b/client/src/containers/Items/withItemsActions.js
index 29c4c3797..28a2262ab 100644
--- a/client/src/containers/Items/withItemsActions.js
+++ b/client/src/containers/Items/withItemsActions.js
@@ -1,35 +1,7 @@
import { connect } from 'react-redux';
-import {
- fetchItems,
- fetchItem,
- deleteItem,
- submitItem,
- editItem,
- deleteBulkItems,
- activateItem,
- inactiveItem,
-} from 'store/items/items.actions';
import t from 'store/types';
export const mapDispatchToProps = (dispatch) => ({
- requestFetchItems: (query) => dispatch(fetchItems({ query })),
- requestFetchItem: (id) => dispatch(fetchItem({ id })),
- requestDeleteItem: (id) => dispatch(deleteItem({ id })),
- requestDeleteBulkItems: (ids) => dispatch(deleteBulkItems({ ids })),
- requestSubmitItem: (form) => dispatch(submitItem({ form })),
- requestEditItem: (id, form) => dispatch(editItem(id, form)),
- requestInactiveItem: (id) => dispatch(inactiveItem({ id })),
- requestActivateItem: (id) => dispatch(activateItem({ id })),
- addBulkActionItem: (id) =>
- dispatch({
- type: t.ITEM_BULK_ACTION_ADD,
- itemId: id,
- }),
- removeBulkActionItem: (id) =>
- dispatch({
- type: t.ITEM_BULK_ACTION_REMOVE,
- itemId: id,
- }),
setItemsTableQuery: (key, value) =>
dispatch({
type: t.ITEMS_TABLE_QUERY_SET,
@@ -41,12 +13,6 @@ export const mapDispatchToProps = (dispatch) => ({
type: t.ITEMS_TABLE_QUERIES_ADD,
payload: { queries },
}),
-
- changeItemsCurrentView: (id) =>
- dispatch({
- type: t.ITEMS_SET_CURRENT_VIEW,
- currentViewId: parseInt(id, 10),
- }),
setSelectedRowsItems: (selectedRows) =>
dispatch({
type: t.ITEM_SELECTED_ROWS_SET,
diff --git a/client/src/containers/ItemsCategories/ItemCategoriesTable.js b/client/src/containers/ItemsCategories/ItemCategoriesTable.js
index 8b8518de3..176ccd293 100644
--- a/client/src/containers/ItemsCategories/ItemCategoriesTable.js
+++ b/client/src/containers/ItemsCategories/ItemCategoriesTable.js
@@ -68,7 +68,7 @@ export default function ItemsCategoryTable({
sticky={true}
selectionColumn={true}
TableLoadingRenderer={TableSkeletonRows}
- {...tableProps}
+ noResults={'There is no items categories in table yet.'}
/>
);
diff --git a/client/src/hooks/query/accounts.js b/client/src/hooks/query/accounts.js
index e3f384575..88da066e0 100644
--- a/client/src/hooks/query/accounts.js
+++ b/client/src/hooks/query/accounts.js
@@ -1,6 +1,8 @@
import { useMutation, useQuery, useQueryClient } from 'react-query';
+import { defaultTo } from 'lodash';
import ApiService from 'services/ApiService';
+// Transform the account.
const transformAccount = (response) => {
return response.data.account;
};
@@ -9,17 +11,18 @@ const transformAccount = (response) => {
* Retrieve accounts list.
*/
export function useAccounts(query, props) {
- return useQuery(
+ const states = useQuery(
['ACCOUNTS', query],
() =>
ApiService.get('accounts', { params: query }).then(
(response) => response.data.accounts,
),
- {
- initialData: [],
- ...props
- },
+ props,
);
+ return {
+ ...states,
+ data: defaultTo(states.data, []),
+ };
}
/**
@@ -27,23 +30,30 @@ export function useAccounts(query, props) {
* @param {number} id -
*/
export function useAccount(id, props) {
- return useQuery(
+ const states = useQuery(
['ACCOUNT', id],
() => ApiService.get(`accounts/${id}`).then(transformAccount),
- {
- initialData: {},
- ...props,
- },
+ props,
);
+ return {
+ ...states,
+ data: defaultTo(states.data, {}),
+ };
}
/**
* Retrieve accounts types list.
*/
-export function useAccountsTypes() {
- return useQuery(['ACCOUNTS_TYPES'], () => ApiService.get('account_types'), {
- initialData: [],
- });
+export function useAccountsTypes(props) {
+ const states = useQuery(
+ ['ACCOUNTS_TYPES'],
+ () => ApiService.get('account_types'),
+ props,
+ );
+ return {
+ ...states,
+ data: defaultTo(states.data, {}),
+ };
}
/**
@@ -56,7 +66,7 @@ export function useCreateAccount(props) {
onSuccess: () => {
client.invalidateQueries('ACCOUNTS');
},
- ...props
+ ...props,
});
}
@@ -72,7 +82,7 @@ export function useEditAccount(props) {
onSuccess: () => {
query.invalidateQueries('ACCOUNTS');
},
- ...props
+ ...props,
},
);
}
@@ -103,15 +113,10 @@ export function useDeleteAccount(props) {
export function useActivateAccount(props) {
const query = useQueryClient();
- return useMutation(
- (id) => ApiService.post(`accounts/${id}/activate`),
- {
- onSuccess: () => {
-
- },
- ...props
- }
- );
+ return useMutation((id) => ApiService.post(`accounts/${id}/activate`), {
+ onSuccess: () => {},
+ ...props,
+ });
}
/**
@@ -120,13 +125,8 @@ export function useActivateAccount(props) {
export function useInactivateAccount(props) {
const query = useQueryClient();
- return useMutation(
- (id) => ApiService.post(`accounts/${id}/inactivate`),
- {
- onSuccess: () => {
-
- },
- ...props
- },
- );
+ return useMutation((id) => ApiService.post(`accounts/${id}/inactivate`), {
+ onSuccess: () => {},
+ ...props,
+ });
}
diff --git a/client/src/hooks/query/currencies.js b/client/src/hooks/query/currencies.js
index 37aab5782..47a6e3ef5 100644
--- a/client/src/hooks/query/currencies.js
+++ b/client/src/hooks/query/currencies.js
@@ -1,28 +1,55 @@
-import { useMutation, useQuery } from 'react-query';
+import { useMutation, useQueryClient, useQuery } from 'react-query';
+import { defaultTo } from 'lodash';
import ApiService from 'services/ApiService';
/**
* Create a new currency.
*/
-export function useCreateCurrency() {
- return useMutation((values) => ApiService.post('currencies', values));
+export function useCreateCurrency(props) {
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ (values) => ApiService.post('currencies', values),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries('CURRENCIES');
+ },
+ ...props,
+ }
+ );
}
/**
* Edits the given currency code.
*/
-export function useEditCurrency() {
+export function useEditCurrency(props) {
+ const queryClient = useQueryClient();
+
return useMutation((currencyCode, values) =>
ApiService.post(`currencies/${currencyCode}`, values),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries('CURRENCIES');
+ },
+ ...props,
+ }
);
}
/**
* Deletes the given currency.
*/
-export function useDeleteCurrency() {
+export function useDeleteCurrency(props) {
+ const queryClient = useQueryClient();
+
return useMutation((currencyCode) =>
ApiService.delete(`currencies/${currencyCode}`),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries('CURRENCIES');
+ },
+ ...props
+ }
);
}
@@ -30,12 +57,14 @@ export function useDeleteCurrency() {
* Retrieve the currencies list.
*/
export function useCurrencies(props) {
- return useQuery(
+ const states = useQuery(
['CURRENCIES'],
() => ApiService.get('currencies').then(res => res.data.currencies),
- {
- initialData: [],
- ...props,
- },
+ props,
);
+
+ return {
+ ...states,
+ data: defaultTo(states.data, []),
+ }
}
diff --git a/client/src/hooks/query/items.js b/client/src/hooks/query/items.js
index c64326d34..9d7f5690c 100644
--- a/client/src/hooks/query/items.js
+++ b/client/src/hooks/query/items.js
@@ -1,4 +1,5 @@
import { useMutation, useQuery, useQueryClient } from 'react-query';
+import { defaultTo } from 'lodash';
import ApiService from 'services/ApiService';
import { transformResponse } from 'utils';
@@ -12,7 +13,14 @@ const defaultPagination = {
* Creates a new item.
*/
export function useCreateItem(props) {
- return useMutation((values) => ApiService.post('items', values), props);
+ const queryClient = useQueryClient();
+
+ return useMutation((values) => ApiService.post('items', values), {
+ onSuccess: () => {
+ queryClient.invalidateQueries('ITEMS');
+ },
+ ...props,
+ });
}
/**
@@ -35,7 +43,7 @@ export function useEditItem(props) {
*/
export function useDeleteItem(props) {
const queryClient = useQueryClient();
-
+
return useMutation((id) => ApiService.delete(`items/${id}`), {
onSuccess: () => {
queryClient.invalidateQueries('ITEMS');
@@ -58,19 +66,21 @@ const transformItemsResponse = (response) => {
* Retrieves items list.
*/
export function useItems(query, props) {
- return useQuery(
+ const result = useQuery(
['ITEMS', query],
() =>
ApiService.get(`items`, { params: query }).then(transformItemsResponse),
- {
- initialData: {
- items: [],
- pagination: defaultPagination,
- filterMeta: {},
- },
- ...props,
- },
+ props,
);
+
+ return {
+ ...result,
+ data: defaultTo(result.data, {
+ items: [],
+ pagination: defaultPagination,
+ filterMeta: {},
+ }),
+ };
}
/**
diff --git a/client/src/hooks/query/views.js b/client/src/hooks/query/views.js
index e88d9b3d0..4d1509668 100644
--- a/client/src/hooks/query/views.js
+++ b/client/src/hooks/query/views.js
@@ -1,6 +1,6 @@
import { useQuery } from 'react-query';
+import { defaultTo } from 'lodash';
import ApiService from "services/ApiService";
-
// export function useSaveView(values) {
// return ApiService.post('views', form);
// }
@@ -18,14 +18,16 @@ import ApiService from "services/ApiService";
// }
export function useResourceViews(resourceSlug) {
- return useQuery(
+ const states = useQuery(
['RESOURCE_VIEW', resourceSlug],
() => ApiService.get(`views/resource/${resourceSlug}`)
.then((response) => response.data.views),
- {
- initialData: [],
- }
);
+
+ return {
+ ...states,
+ data: defaultTo(states.data, []),
+ }
}
@@ -39,13 +41,16 @@ export function useResourceColumns(resourceSlug) {
);
}
-export function useResourceFields(resourceSlug) {
- return useQuery(
+export function useResourceFields(resourceSlug, props) {
+ const states = useQuery(
['RESOURCE_FIELDS', resourceSlug],
() => ApiService.get(`resources/${resourceSlug}/fields`)
.then((res) => res.data.resource_fields),
- {
- initialData: [],
- },
+ props
);
+
+ return {
+ ...states,
+ data: defaultTo(states.data, []),
+ }
}
diff --git a/client/src/hooks/state/dashboard.js b/client/src/hooks/state/dashboard.js
index 75a85f3f4..542fe458b 100644
--- a/client/src/hooks/state/dashboard.js
+++ b/client/src/hooks/state/dashboard.js
@@ -1,10 +1,27 @@
+import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { dashboardPageTitle } from 'store/dashboard/dashboard.actions';
-export const useDashboardPageTitle = () => {
+export const useDispatchAction = (action) => {
const dispatch = useDispatch();
- return (pageTitle) => {
- dispatch(dashboardPageTitle(pageTitle));
- }
+ return useCallback(
+ (payload) => {
+ dispatch(action(payload));
+ },
+ [dispatch, action],
+ );
};
+
+export const useDashboardPageTitle = () => {
+ return useDispatchAction(dashboardPageTitle);
+};
+
+export const useSetAccountsTableQuery = () => {
+
+};
+
+export const useAccountsTableQuery = () => {
+
+}
+
diff --git a/client/src/static/json/icons.js b/client/src/static/json/icons.js
index da638ee6b..8e667eeb4 100644
--- a/client/src/static/json/icons.js
+++ b/client/src/static/json/icons.js
@@ -367,5 +367,11 @@ export default {
'M10 20C4.48 20 0 15.52 0 10S4.48 0 10 0s10 4.48 10 10-4.48 10-10 10zm5-14c-.28 0-.53.11-.71.29L8 12.59l-2.29-2.3a1.003 1.003 0 00-1.42 1.42l3 3c.18.18.43.29.71.29.28 0 .53-.11.71-.29l7-7A1.003 1.003 0 0015 6z'
],
viewBox: '0 0 20 20'
+ },
+ 'swap-vert': {
+ path: [
+ 'M10.6,10.9V5.4H9v5.5H6.7L9.8,14l3.1-3.1ZM5.1,0,2,3.1H4.3V8.6H5.9V3.1H8.2Z',
+ ],
+ viewBox: '0 0 14 14'
}
};
diff --git a/client/src/store/items/items.actions.js b/client/src/store/items/items.actions.js
index f9de6a876..e69de29bb 100644
--- a/client/src/store/items/items.actions.js
+++ b/client/src/store/items/items.actions.js
@@ -1,139 +0,0 @@
-import ApiService from 'services/ApiService';
-import t from 'store/types';
-
-export const submitItem = ({ form }) => {
- return (dispatch) =>
- new Promise((resolve, reject) => {
- ApiService.post('items', form)
- .then((response) => {
- resolve(response);
- })
- .catch((error) => {
- const { response } = error;
- const { data } = response;
-
- reject(data?.errors);
- });
- });
-};
-
-export const editItem = (id, form) => {
- return (dispatch) =>
- new Promise((resolve, reject) => {
- ApiService.post(`items/${id}`, form)
- .then((response) => {
- resolve(response);
- })
- .catch((error) => {
- const { response } = error;
- const { data } = response;
-
- reject(data?.errors);
- });
- });
-};
-
-export const fetchItems = ({ query }) => {
- return (dispatch, getState) =>
- new Promise((resolve, reject) => {
- let pageQuery = getState().items.tableQuery;
-
- dispatch({
- type: t.ITEMS_TABLE_LOADING,
- payload: { loading: true },
- });
-
- ApiService.get(`items`, { params: { ...pageQuery, ...query } })
- .then((response) => {
- dispatch({
- type: t.ITEMS_SET,
- items: response.data.items,
- });
- dispatch({
- type: t.ITEMS_PAGE_SET,
- items: response.data.items,
- customViewId:
- response.data?.filter_meta?.view?.custom_view_id || -1,
- paginationMeta: response.data.pagination,
- });
- dispatch({
- type: t.ITEMS_PAGINATION_SET,
- payload: {
- pagination: response.data.pagination,
- customViewId:
- response.data?.filter_meta?.view?.custom_view_id || -1,
- },
- });
- dispatch({
- type: t.ITEMS_TABLE_LOADING,
- payload: { loading: false },
- });
- resolve(response);
- })
- .catch((error) => {
- reject(error);
- });
- });
-};
-
-export const fetchItem = ({ id }) => {
- return (dispatch) =>
- new Promise((resolve, reject) => {
- ApiService.get(`items/${id}`)
- .then((response) => {
- dispatch({
- type: t.ITEM_SET,
- payload: {
- id,
- item: response.data.item,
- },
- });
- resolve(response);
- })
- .catch((error) => {
- reject(error);
- });
- });
-};
-
-export const deleteItem = ({ id }) => {
- return (dispatch) =>
- new Promise((resolve, reject) => {
- ApiService.delete(`items/${id}`)
- .then((response) => {
- dispatch({
- type: t.ITEM_DELETE,
- payload: { id },
- });
- resolve(response);
- })
- .catch((error) => {
- reject(error?.response?.data);
- });
- });
-};
-
-export const deleteBulkItems = ({ ids }) => {
- return (dispatch) =>
- new Promise((resolve, reject) => {
- ApiService.delete('items', { params: { ids } })
- .then((response) => {
- dispatch({
- type: t.ITEMS_BULK_DELETE,
- payload: { ids },
- });
- resolve(response);
- })
- .catch((error) => {
- reject(error.response.data.errors || []);
- });
- });
-};
-
-export const activateItem = ({ id }) => {
- return (dispatch) => ApiService.post(`items/${id}/activate`);
-};
-
-export const inactiveItem = ({ id }) => {
- return (dispatch) => ApiService.post(`items/${id}/inactivate`);
-};
diff --git a/client/src/store/items/items.reducer.js b/client/src/store/items/items.reducer.js
index 625759dd0..232d96d92 100644
--- a/client/src/store/items/items.reducer.js
+++ b/client/src/store/items/items.reducer.js
@@ -1,111 +1,16 @@
-import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
-import { getItemsViewPages } from 'store/items/items.selectors';
import {
- viewPaginationSetReducer,
createTableQueryReducers,
} from 'store/journalNumber.reducer';
const initialState = {
- items: {},
- views: {},
- itemsRelation: {},
- currentPage: 1,
- currentViewId: -1,
- bulkActions: {},
- loading: false,
tableQuery: {
- page_size: 12,
+ pageSize: 12,
page: 1,
},
selectedRows: [],
};
export default createReducer(initialState, {
- [t.ITEMS_SET]: (state, action) => {
- const _items = {};
-
- action.items.forEach((item) => {
- _items[item.id] = item;
- });
- state.items = {
- ...state.items,
- ..._items,
- };
- },
-
- [t.ITEM_SET]: (state, action) => {
- const { id, item } = action.payload;
- state.items[id] = { ...item };
- },
-
- [t.ITEMS_PAGE_SET]: (state, action) => {
- const { items, customViewId, paginationMeta } = action;
-
- const viewId = customViewId || -1;
- const view = state.views[viewId] || {};
-
- state.views[viewId] = {
- ...view,
- pages: {
- ...(state.views?.[viewId]?.pages || {}),
- [paginationMeta.page]: {
- ids: items.map((item) => item.id),
- },
- },
- };
- },
-
- [t.ITEM_BULK_ACTION_ADD]: (state, action) => {
- state.bulkActions[action.itemId] = true;
- },
-
- [t.ITEM_SELECTED_ROWS_SET]: (state, action) => {
- const { selectedRows } = action.payload;
- state.selectedRows = selectedRows;
- },
-
- [t.ITEM_BULK_ACTION_REMOVE]: (state, action) => {
- delete state.bulkActions[action.itemId];
- },
-
- [t.ITEM_DELETE]: (state, action) => {
- const { id } = action.payload;
- const items = { ...state.items };
-
- if (items[id]) {
- const item = items[id];
-
- delete items[id];
- state.items = items;
- }
- },
-
- [t.ITEMS_TABLE_LOADING]: (state, action) => {
- const { loading } = action.payload;
- state.loading = !!loading;
- },
-
- [t.ITEMS_SET_CURRENT_VIEW]: (state, action) => {
- state.currentViewId = action.currentViewId;
- },
-
- [t.ITEMS_BULK_DELETE]: (state, action) => {
- const { ids } = action.payload;
- const items = { ...state.items };
-
- ids.forEach((id) => {
- if (typeof items[id] !== 'undefined') {
- delete items[id];
- }
- });
- state.items = items;
- },
-
- ...viewPaginationSetReducer(t.ITEMS_PAGINATION_SET),
...createTableQueryReducers('ITEMS'),
-});
-
-export const getItemById = (state, id) => {
- return state.items.items[id];
-};
+});
\ No newline at end of file
diff --git a/client/src/store/items/items.selectors.js b/client/src/store/items/items.selectors.js
index a8466ff6d..9882b25c7 100644
--- a/client/src/store/items/items.selectors.js
+++ b/client/src/store/items/items.selectors.js
@@ -1,28 +1,11 @@
import { paginationLocationQuery } from 'store/selectors';
-import { createSelector } from 'reselect';
-import { pickItemsFromIds, defaultPaginationMeta } from 'store/selectors';
+import { createDeepEqualSelector } from 'utils';
const itemsTableQuerySelector = (state) => state.items.tableQuery;
-const itemsCurrentPageSelector = (state, props) => {
- const currentViewId = state.items.currentViewId;
- const currentView = state.items.views?.[currentViewId];
- const currentPageId = currentView?.paginationMeta?.page;
-
- return currentView?.pages?.[currentPageId];
-};
-const itemsDataSelector = (state) => state.items.items;
-
-const itemsPaginationSelector = (state, props) => {
- const viewId = state.items.currentViewId;
- return state.items.views?.[viewId]?.paginationMeta;
-};
-const itemsCurrentViewIdSelector = (state) => {
- return state.items.currentViewId;
-};
// Get items table query marged with location query.
export const getItemsTableQueryFactory = () =>
- createSelector(
+ createDeepEqualSelector(
paginationLocationQuery,
itemsTableQuerySelector,
(locationQuery, tableQuery) => {
@@ -32,30 +15,3 @@ export const getItemsTableQueryFactory = () =>
};
},
);
-
-// Retrieve items current page and view.
-export const getItemsCurrentPageFactory = () =>
- createSelector(
- itemsDataSelector,
- itemsCurrentPageSelector,
- (items, itemsIdsCurrentPage) => {
- return typeof itemsIdsCurrentPage === 'object'
- ? pickItemsFromIds(items, itemsIdsCurrentPage.ids) || []
- : [];
- },
- );
-
-// Retrieve items pagination meta.
-export const getItemsPaginationMetaFactory = () =>
- createSelector(itemsPaginationSelector, (itemsPagination) => {
- return {
- ...defaultPaginationMeta(),
- ...itemsPagination,
- };
- });
-
-// Retrieve items current view id.
-export const getItemsCurrentViewIdFactory = () =>
- createSelector(itemsCurrentViewIdSelector, (currentViewId) => {
- return currentViewId;
- });
diff --git a/client/src/store/selectors.js b/client/src/store/selectors.js
index 6f9292226..cc083180a 100644
--- a/client/src/store/selectors.js
+++ b/client/src/store/selectors.js
@@ -40,7 +40,7 @@ export const paginationLocationQuery = (state, props) => {
? new URLSearchParams(props.location.search)
: null;
- const queryParamsKeys = ['page_size', 'page'];
+ const queryParamsKeys = ['page_size', 'page', 'custom_view_id'];
return queryParams
? mapValues(pick(Object.fromEntries(queryParams), queryParamsKeys), (v) =>
diff --git a/client/src/style/App.scss b/client/src/style/App.scss
index afdb18d4e..75a6ca587 100644
--- a/client/src/style/App.scss
+++ b/client/src/style/App.scss
@@ -44,14 +44,7 @@ body.hide-scrollbar .Pane2{
}
-.select-list--fill-popover {
- position: relative;
- .bp3-transition-container,
- .bp3-popover {
- min-width: 100%;
- }
-}
.bp3-fill{
.bp3-popover-wrapper,
@@ -91,12 +84,6 @@ body.hide-scrollbar .Pane2{
max-width: 300px;
}
-.form-group--select-list{
- button{
- justify-content: start;
- }
-}
-
.bp3-timezone-picker{
.bp3-button{
@@ -105,4 +92,6 @@ body.hide-scrollbar .Pane2{
display: none;
}
}
-}
\ No newline at end of file
+}
+
+
diff --git a/client/src/style/components/DataTable/DataTable.scss b/client/src/style/components/DataTable/DataTable.scss
index 012106167..92b1de9a8 100644
--- a/client/src/style/components/DataTable/DataTable.scss
+++ b/client/src/style/components/DataTable/DataTable.scss
@@ -64,6 +64,10 @@
border-bottom: 0;
}
}
+
+ .bp3-context-menu-popover-target{
+ z-index: 100;
+ }
}
.th,
diff --git a/client/src/style/components/DataTable/Pagination.scss b/client/src/style/components/DataTable/Pagination.scss
index e0eed1137..52514fbfc 100644
--- a/client/src/style/components/DataTable/Pagination.scss
+++ b/client/src/style/components/DataTable/Pagination.scss
@@ -1,12 +1,12 @@
.pagination{
display: flex;
- padding: 25px 14px;
+ padding: 28px 14px;
font-size: 13px;
.bp3-button{
background: transparent;
- padding: 4px 4px;
+ padding: 5px;
}
&__item{
@@ -15,7 +15,7 @@
&:not([class*="bp3-intent-"]){
color: #666666;
- border-radius: 3px;
+ border-radius: 5px;
&:hover{
background-color: #E6EFFB;
@@ -28,14 +28,14 @@
}
.pagination .pagination__buttons-group .bp3-button-group &{
- border-radius: 3px;
+ border-radius: 5px;
}
&--next,
&--previous{
&.bp3-button{
- padding-left: 6px;
- padding-right: 6px;
+ padding-left: 10px;
+ padding-right: 10px;
}
}
@@ -63,20 +63,22 @@
}
&__info{
- padding-top: 6px;
- color: #999;
+ color: #888;
margin-left: 12px;
+ display: flex;
+ align-items: center;
}
&__controls{
display: flex;
+ align-items: center;
margin-left: auto;
.bp3-html-select{
margin-left: 6px;
select{
- height: 20px;
+ height: 24px;
width: auto;
padding: 0;
padding-right: 0px;
@@ -103,7 +105,6 @@
&__pagesize-control{
margin-left: 12px;
- padding-top: 4px;
- color: #777;
+ color: #888;
}
}
\ No newline at end of file
diff --git a/client/src/style/components/MaterialProgressBar.scss b/client/src/style/components/MaterialProgressBar.scss
new file mode 100644
index 000000000..3f37c016e
--- /dev/null
+++ b/client/src/style/components/MaterialProgressBar.scss
@@ -0,0 +1,101 @@
+// IONIC DEFAULT THEME COLORS
+$colors: (
+ primary: #387ef5,
+ secondary: #32db64,
+ danger: #f53d3d,
+ light: #f4f4f4,
+ dark: #222
+);
+
+// https://css-tricks.com/html5-progress-element/
+.progress, progress[value] {
+ width: 100%;
+ border: none;
+ margin: 5px 0;
+ height: 5px;
+ display: block;
+ appearance: none;
+ -webkit-appearance: none;
+ &::-webkit-progress-bar {
+ background-color: lighten( map-get($colors, primary), 35% );
+ }
+ &::-webkit-progress-value {
+ background-color: map-get($colors, primary);
+ }
+}
+
+// http://materializecss.com/preloader.html
+// https://github.com/Dogfalo/materialize/blob/master/dist/css/materialize.css
+.progress-materializecss {
+ position: absolute;
+ top: -1px;
+ height: 2px;
+ display: block;
+ width: 100%;
+ background-color: transparent;
+ margin: 0;
+ overflow: hidden;
+
+ .indeterminate {
+ background-color: #002fff;
+
+ &:before{
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ will-change: left, right;
+ animation: indeterminate 3.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
+ }
+ &:after {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ will-change: left, right;
+ animation: indeterminate-short 3.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
+ animation-delay: 2.15s;
+ }
+ }
+}
+
+@keyframes indeterminate {
+ 0% {
+ left: -35%;
+ right: 100%;
+ }
+ 60% {
+ left: 100%;
+ right: -90%;
+ }
+ 100% {
+ left: 100%;
+ right: -90%;
+ }
+}
+
+@keyframes indeterminate-short {
+ 0% {
+ left: -200%;
+ right: 100%;
+ }
+ 60% {
+ left: 107%;
+ right: -8%;
+ }
+ 100% {
+ left: 107%;
+ right: -8%;
+ }
+}
+.progress-container{
+ width: 100%;
+ position: relative;
+
+}
+// IRRELEVANTS STYLES //
+/////////////////////////
diff --git a/client/src/style/objects/buttons.scss b/client/src/style/objects/buttons.scss
index 5354f8f59..44b18a6be 100644
--- a/client/src/style/objects/buttons.scss
+++ b/client/src/style/objects/buttons.scss
@@ -1,30 +1,4 @@
-.form-group--select-list {
- .bp3-popover-target {
- .bp3-icon {
- position: absolute;
- top: 0;
- left: 0;
- margin: 7px;
- + .bp3-button-text {
- padding-left: 20px;
- }
- }
-
- .bp3-button {
- padding-left: 10px;
- color: #333;
- }
- }
-}
-
-.form-group--select-list {
- .bp3-popover-open {
- .bp3-button {
- border-color: #80bdff;
- }
- }
-}
.bp3-button {
min-width: 32px;
@@ -41,24 +15,6 @@
font-size: 13px;
min-height: 29px;
}
- .form-group--select-list & {
- border-radius: 2px;
-
- &,
- &:hover {
- box-shadow: 0 0 0 transparent;
- border: 1px solid #ced4da;
-
- &:not(:disabled) {
- background: #fff;
- }
- }
- }
- .form-group--select-list.bp3-intent-danger & {
- & {
- border-color: #db3737;
- }
- }
}
.bp3-button-group.bp3-minimal .bp3-button {
diff --git a/client/src/style/objects/form.scss b/client/src/style/objects/form.scss
index 09f973a51..dd2ff02c0 100644
--- a/client/src/style/objects/form.scss
+++ b/client/src/style/objects/form.scss
@@ -167,16 +167,70 @@ label.bp3-label {
}
.form-group--select-list {
+
+ .form-group--select-list.bp3-intent-danger & {
+ & {
+ border-color: #db3737;
+ }
+ }
+
.#{$ns}-button {
color: #8d8d8d;
position: relative;
padding-right: 25px;
+ justify-content: start;
+
+ &:not([class*="bp3-intent-"]):not(.bp3-minimal){
+ &,
+ &:hover {
+ outline: none;
+ box-shadow: 0 0 0 transparent;
+ border: 1px solid #ced4da;
+
+ &:not(:disabled) {
+ background: #fff;
+ }
+ }
+ &:focus{
+ box-shadow: 0 0 0 1px #116cd0;
+ border-color: #116cd0;
+ }
+ }
}
&.bp3-fill {
width: 100%;
}
+ .bp3-popover-target.bp3-popover-open{
+
+ .#{$ns}-button {
+
+ &:not([class*="bp3-intent-"]):not(.bp3-minimal):not(:disabled),
+ &:not([class*="bp3-intent-"]):not(.bp3-minimal):hover:not(:disabled){
+ background-color: #fafafa;
+ }
+ }
+ }
+
+ .bp3-popover-target {
+ .bp3-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 7px;
+
+ + .bp3-button-text {
+ padding-left: 20px;
+ }
+ }
+
+ .bp3-button {
+ padding-left: 10px;
+ color: #333;
+ }
+ }
+
&.bp3-intent-danger {
.bp3-button:not(.bp3-minimal) {
border-color: #db3737;
@@ -184,6 +238,15 @@ label.bp3-label {
}
}
+.select-list--fill-popover {
+ position: relative;
+
+ .bp3-transition-container,
+ .bp3-popover {
+ min-width: 100%;
+ }
+}
+
.select-list--tooltip-items .bp3-popover-target {
display: block;
}
diff --git a/client/src/style/pages/Dashboard/Dashboard.scss b/client/src/style/pages/Dashboard/Dashboard.scss
index e0db1af11..2bb2dce99 100644
--- a/client/src/style/pages/Dashboard/Dashboard.scss
+++ b/client/src/style/pages/Dashboard/Dashboard.scss
@@ -79,19 +79,18 @@ $dashboard-views-bar-height: 45px;
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
padding: 0;
background-size: contain;
- background-color: #eed1f2;
border-radius: 50%;
- height: 35px;
- width: 35px;
+ height: 32px;
+ width: 32px;
.user-text {
font-size: 12px;
- color: #804F87;
+ color: #fff;
}
&,
&:hover,
&:focus {
- background-color: #EEB1F7;
+ background-color: #cb20e5;
border: 0;
box-shadow: none;
}
@@ -120,7 +119,7 @@ $dashboard-views-bar-height: 45px;
}
&__back-link {
- margin-left: 24px;
+ margin-left: 20px;
display: flex;
a {
@@ -128,6 +127,15 @@ $dashboard-views-bar-height: 45px;
}
}
+ &__organization-name{
+ font-size: 14px;
+ font-weight: 400;
+ color: #818ca9;
+ margin: 0 0 0 15px;
+ display: flex;
+ align-items: center;
+ }
+
&__actions-bar {
border-bottom: 2px solid #eaeaea;
@@ -224,14 +232,7 @@ $dashboard-views-bar-height: 45px;
margin: 0;
}
h3 {
- border-left: 1px solid #d9d9d9;
- padding-left: 10px;
- font-size: 16px;
- font-weight: 400;
- color: #76768b;
- margin: 0 0 0 12px;
- padding-top: 4px;
- padding-bottom: 4px;
+
}
.button--view-edit {
diff --git a/client/src/style/pages/Preferences/Topbar.scss b/client/src/style/pages/Preferences/Topbar.scss
index 2e2fce323..17b6c5b91 100644
--- a/client/src/style/pages/Preferences/Topbar.scss
+++ b/client/src/style/pages/Preferences/Topbar.scss
@@ -32,17 +32,17 @@
background-size: contain;
background-color: #eed1f2;
border-radius: 50%;
- height: 35px;
- width: 35px;
+ height: 32px;
+ width: 32px;
.user-text {
font-size: 12px;
- color: #804f87;
+ color: #fff;
}
&,
&:hover,
&:focus {
- background-color: #eed1f2;
+ background-color: #cb20e5;
border: 0;
box-shadow: none;
}
diff --git a/client/src/utils.js b/client/src/utils.js
index 25c671015..cc59b1706 100644
--- a/client/src/utils.js
+++ b/client/src/utils.js
@@ -5,7 +5,8 @@ import Currency from 'js-money/lib/currency';
import PProgress from 'p-progress';
import accounting from 'accounting';
import deepMapKeys from 'deep-map-keys';
-
+import { createSelectorCreator, defaultMemoize } from 'reselect';
+import { isEqual } from 'lodash';
export function removeEmptyFromObject(obj) {
obj = Object.assign({}, obj);
@@ -411,6 +412,10 @@ export const transfromToSnakeCase = (object) => {
return deepMapKeys(object, (key) => _.snakeCase(key));
};
+export const transformTableQueryToParams = (object) => {
+ return transfromToSnakeCase(object);
+};
+
export function flatObject(obj) {
const flatObject = {};
const path = []; // current path
@@ -466,4 +471,20 @@ export function transactionNumber(prefix, number) {
export function safeCallback(callback, ...args) {
return () => callback && callback(...args);
+}
+
+export const createDeepEqualSelector = createSelectorCreator(
+ defaultMemoize,
+ isEqual
+);
+
+/**
+ * Detarmines whether the table has empty status.
+ */
+export const isTableEmptyStatus = ({ data, pagination, filterMeta }) => {
+ return [
+ _.isEmpty(data),
+ _.isEmpty(filterMeta.view),
+ pagination.page === 1,
+ ].every(cond => cond === true)
}
\ No newline at end of file