refactoring: migrating to react-query to manage service-side state.

This commit is contained in:
a.bouhuolia
2021-02-07 08:10:21 +02:00
parent e093be0663
commit adac2386bb
284 changed files with 8255 additions and 6610 deletions

View File

@@ -1,228 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Button,
Popover,
Menu,
Intent,
MenuItem,
MenuDivider,
Position,
Tag,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import { DataTable, Icon, LoadingIndicator } from 'components';
import { CLASSES } from 'common/classes';
import { useIsValuePassed } from 'hooks';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withInventoryAdjustments from './withInventoryAdjustments';
import withInventoryAdjustmentActions from './withInventoryAdjustmentActions';
import { compose, saveInvoke } from 'utils';
import { withRouter } from 'react-router-dom';
function InventoryAdjustmentDataTable({
// withInventoryAdjustments
inventoryAdjustmentItems,
inventoryAdjustmentCurrentPage,
inventoryAdjustmentLoading,
inventoryAdjustmentsPagination,
// withInventoryAdjustmentsActions
addInventoryAdjustmentTableQueries,
// #ownProps
onDeleteInventoryAdjustment,
onSelectedRowsChange,
}) {
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(inventoryAdjustmentLoading, false);
const handleDeleteInventoryAdjustment = useCallback(
(_adjustment) => () => {
saveInvoke(onDeleteInventoryAdjustment, _adjustment);
},
[onDeleteInventoryAdjustment],
);
const actionMenuList = useCallback(
(adjustment) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
text={formatMessage({ id: 'delete_adjustment' })}
intent={Intent.DANGER}
onClick={handleDeleteInventoryAdjustment(adjustment)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeleteInventoryAdjustment, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => actionMenuList(cell.row.original),
[actionMenuList],
);
const columns = useMemo(
() => [
{
id: 'date',
Header: formatMessage({ id: 'date' }),
accessor: (r) => moment(r.date).format('YYYY MMM DD'),
width: 115,
className: 'date',
},
{
id: 'type',
Header: formatMessage({ id: 'type' }),
accessor: (row) =>
row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{formatMessage({ id: row.type })}
</Tag>
) : (
''
),
className: 'type',
width: 100,
},
{
id: 'reason',
Header: formatMessage({ id: 'reason' }),
accessor: 'reason',
className: 'reason',
width: 115,
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
className: 'reference_no',
width: 100,
},
{
id: 'publish',
Header: formatMessage({ id: 'status' }),
accessor: (r) => {
return r.is_published ? (
<Tag minimal={true}>
<T id={'published'} />
</Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
);
},
width: 95,
className: 'publish',
},
{
id: 'description',
Header: formatMessage({ id: 'description' }),
accessor: 'description',
disableSorting: true,
width: 85,
className: 'description',
},
{
id: 'created_at',
Header: formatMessage({ id: 'created_at' }),
accessor: (r) => moment(r.created_at).format('YYYY MMM DD'),
width: 125,
className: 'created_at',
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
disableResizing: true,
},
],
[actionMenuList, formatMessage],
);
const handleDataTableFetchData = useCallback(
({ pageSize, pageIndex, sortBy }) => {
addInventoryAdjustmentTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page: pageIndex + 1,
});
},
[addInventoryAdjustmentTableQueries],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
onSelectedRowsChange &&
onSelectedRowsChange(selectedRows.map((s) => s.original));
},
[onSelectedRowsChange],
);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={inventoryAdjustmentLoading && !isLoadedBefore}>
<DataTable
columns={columns}
data={inventoryAdjustmentItems}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
autoResetSortBy={false}
autoResetPage={false}
pagesCount={inventoryAdjustmentsPagination.pagesCount}
initialPageSize={inventoryAdjustmentsPagination.pageSize}
initialPageIndex={inventoryAdjustmentsPagination.page - 1}
/>
</LoadingIndicator>
</div>
);
}
export default compose(
withRouter,
withDialogActions,
withInventoryAdjustmentActions,
withInventoryAdjustments(
({
inventoryAdjustmentLoading,
inventoryAdjustmentItems,
inventoryAdjustmentCurrentPage,
inventoryAdjustmentsPagination,
}) => ({
inventoryAdjustmentLoading,
inventoryAdjustmentItems,
inventoryAdjustmentCurrentPage,
inventoryAdjustmentsPagination,
}),
),
)(InventoryAdjustmentDataTable);

View File

@@ -1,79 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useQuery } from 'react-query';
import { Route, Switch } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ItemsAlerts from './ItemsAlerts';
import InventoryAdjustmentDataTable from './InventoryAdjustmentDataTable';
import withInventoryAdjustmentActions from './withInventoryAdjustmentActions';
import withInventoryAdjustments from './withInventoryAdjustments';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Inventory Adjustment List.
*/
function InventoryAdjustmentList({
// #withDashboardActions
changePageTitle,
// #withInventoryAdjustments
inventoryAdjustmentTableQuery,
// #withAlertsActions.
openAlert,
// #withInventoryAdjustmentsActions
requestFetchInventoryAdjustmentTable,
setSelectedRowsInventoryAdjustments,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'inventory_adjustment_list' }));
}, [changePageTitle, formatMessage]);
const fetchInventoryAdjustments = useQuery(
['inventory-adjustment-list', inventoryAdjustmentTableQuery],
(key, query) => requestFetchInventoryAdjustmentTable({ ...query }),
);
// Handle selected rows change.
const handleSelectedRowsChange = (selectedRows) => {
const selectedRowsIds = selectedRows.map((r) => r.id);
setSelectedRowsInventoryAdjustments(selectedRowsIds);
};
const handleDeleteInventoryAdjustment = ({ id }) => {
openAlert('inventory-adjustment-delete', { inventoryId: id });
};
return (
<DashboardInsider name={'inventory_adjustments'}>
<DashboardPageContent>
<Switch>
<Route exact={true}>
<InventoryAdjustmentDataTable
onDeleteInventoryAdjustment={handleDeleteInventoryAdjustment}
/>
</Route>
</Switch>
<ItemsAlerts />
</DashboardPageContent>
</DashboardInsider>
);
}
export default compose(
withDashboardActions,
withInventoryAdjustmentActions,
withInventoryAdjustments(({ inventoryAdjustmentTableQuery }) => ({
inventoryAdjustmentTableQuery,
})),
withAlertsActions,
)(InventoryAdjustmentList);

View File

@@ -1,88 +0,0 @@
import React, { useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { useQuery } from 'react-query';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ItemsAlerts from './ItemsAlerts';
import ItemCategoriesDataTable from 'containers/Items/ItemCategoriesTable';
import ItemsCategoryActionsBar from 'containers/Items/ItemsCategoryActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Item categories list.
*/
const ItemCategoryList = ({
// #withDashboardActions
changePageTitle,
// #withAlertsActions.
openAlert,
// #withItemCategoriesActions
requestFetchItemCategories,
setSelectedRowsCategories,
// #withDialog
openDialog,
}) => {
const { id } = useParams();
const { formatMessage } = useIntl();
useEffect(() => {
id
? changePageTitle(formatMessage({ id: 'edit_category_details' }))
: changePageTitle(formatMessage({ id: 'category_list' }));
}, [id, changePageTitle, formatMessage]);
const fetchCategories = useQuery(['items-categories-list'], () =>
requestFetchItemCategories(),
);
const handleFilterChanged = useCallback(() => {}, []);
// Handle selected rows change.
const handleSelectedRowsChange = (selectedRows) => {
const selectedRowsIds = selectedRows.map((r) => r.id);
setSelectedRowsCategories(selectedRowsIds);
};
// Handle delete Item.
const handleDeleteCategory = ({ id }) => {
openAlert('item-category-delete', { itemCategoryId: id });
};
// Handle Edit item category.
const handleEditCategory = (category) => {
openDialog('item-category-form', { action: 'edit', id: category.id });
};
return (
<DashboardInsider name={'item-category-list'}>
<ItemsCategoryActionsBar onFilterChanged={handleFilterChanged} />
<DashboardPageContent>
<ItemCategoriesDataTable
onDeleteCategory={handleDeleteCategory}
onEditCategory={handleEditCategory}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</DashboardPageContent>
<ItemsAlerts />
</DashboardInsider>
);
};
export default compose(
withItemCategoriesActions,
withDashboardActions,
withDialogActions,
withAlertsActions,
)(ItemCategoryList);

View File

@@ -1,174 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Button,
Popover,
Menu,
Intent,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import classNames from 'classnames';
import Icon from 'components/Icon';
import LoadingIndicator from 'components/LoadingIndicator';
import { compose } from 'utils';
import { useIsValuePassed } from 'hooks';
import DataTable from 'components/DataTable';
import { CLASSES } from 'common/classes';
import withItemCategories from './withItemCategories';
import withDialogActions from 'containers/Dialog/withDialogActions';
const ItemsCategoryList = ({
// #withItemCategories
categoriesList,
categoriesTableLoading,
// #withDialogActions.
openDialog,
// #ownProps
onFetchData,
onDeleteCategory,
onSelectedRowsChange,
}) => {
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(categoriesTableLoading, false);
const handelEditCategory = useCallback(
(category) => () => {
openDialog('item-category-form', { action: 'edit', id: category.id });
},
[openDialog],
);
const handleDeleteCategory = useCallback(
(category) => {
onDeleteCategory(category);
},
[onDeleteCategory],
);
const actionMenuList = useCallback(
(category) => (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_category' })}
onClick={handelEditCategory(category)}
/>
<MenuDivider />
<MenuItem
text={formatMessage({ id: 'delete_category' })}
intent={Intent.DANGER}
onClick={() => handleDeleteCategory(category)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handelEditCategory, handleDeleteCategory],
);
const columns = useMemo(
() => [
{
id: 'name',
Header: formatMessage({ id: 'category_name' }),
accessor: 'name',
width: 220,
},
{
id: 'description',
Header: formatMessage({ id: 'description' }),
accessor: 'description',
className: 'description',
width: 220,
},
{
id: 'count',
Header: formatMessage({ id: 'count' }),
accessor: 'count',
className: 'count',
width: 180,
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_TOP}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
},
],
[actionMenuList, formatMessage],
);
const handelFetchData = useCallback(
(...params) => {
onFetchData && onFetchData(...params);
},
[onFetchData],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
onSelectedRowsChange &&
onSelectedRowsChange(selectedRows.map((s) => s.original));
},
[onSelectedRowsChange],
);
const selectionColumn = useMemo(
() => ({
minWidth: 42,
width: 42,
maxWidth: 42,
}),
[],
);
const handleRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator
loading={categoriesTableLoading && !isLoadedBefore}
mount={false}
>
<DataTable
noInitialFetch={true}
columns={columns}
data={categoriesList}
onFetchData={handelFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={handleRowContextMenu}
/>
</LoadingIndicator>
</div>
);
};
export default compose(
withItemCategories(({ categoriesList, categoriesTableLoading }) => ({
categoriesList,
categoriesTableLoading,
})),
withDialogActions,
)(ItemsCategoryList);

View File

@@ -1,12 +1,13 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useHistory } from 'react-router-dom';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { defaultTo } from 'lodash';
import 'style/pages/Items/PageForm.scss';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import ItemFormPrimarySection from './ItemFormPrimarySection';
@@ -14,22 +15,16 @@ import ItemFormBody from './ItemFormBody';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import withItemsActions from 'containers/Items/withItemsActions';
import withMediaActions from 'containers/Media/withMediaActions';
import useMedia from 'hooks/useMedia';
import withItem from 'containers/Items/withItem';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import { compose, transformToForm } from 'utils';
import { transitionItemTypeKeyToLabel } from './utils';
import {
EditItemFormSchema,
CreateItemFormSchema,
transformItemFormData,
} from './ItemForm.schema';
import 'style/pages/Items/PageForm.scss';
import { useItemFormContext } from './ItemFormProvider';
const defaultInitialValues = {
active: 1,
@@ -50,46 +45,27 @@ const defaultInitialValues = {
* Item form.
*/
function ItemForm({
// #withItemActions
requestSubmitItem,
requestEditItem,
itemId,
item,
onFormSubmit,
// #withDashboardActions
changePageTitle,
changePageSubtitle,
// #withSettings
preferredCostAccount,
preferredSellAccount,
preferredInventoryAccount,
// #withMediaActions
requestSubmitMedia,
requestDeleteMedia,
}) {
const isNewMode = !itemId;
// Holds data of submit button once clicked to form submit function.
const [submitPayload, setSubmitPayload] = useState({});
const history = useHistory();
const { formatMessage } = useIntl();
// Item form context.
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
itemId,
item,
accounts,
createItemMutate,
editItemMutate,
submitPayload,
isNewMode
} = useItemFormContext();
// History context.
const history = useHistory();
const { formatMessage } = useIntl();
/**
* Initial values in create and edit mode.
*/
@@ -117,12 +93,7 @@ function ItemForm({
],
);
useEffect(() => {
!isNewMode
? changePageTitle(formatMessage({ id: 'edit_item_details' }))
: changePageTitle(formatMessage({ id: 'new_item' }));
}, [changePageTitle, isNewMode, formatMessage]);
// Transform API errors.
const transformApiErrors = (errors) => {
const fields = {};
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) {
@@ -155,12 +126,14 @@ function ItemForm({
});
resetForm();
setSubmitting(false);
queryCache.removeQueries(['items-table']);
// Submit payload.
if (submitPayload.redirect) {
history.push('/items');
}
};
// Handle response error.
const onError = (errors) => {
setSubmitting(false);
if (errors) {
@@ -169,58 +142,12 @@ function ItemForm({
}
};
if (isNewMode) {
requestSubmitItem(form).then(onSuccess).catch(onError);
createItemMutate(form).then(onSuccess).catch(onError);
} else {
requestEditItem(itemId, form).then(onSuccess).catch(onError);
editItemMutate([itemId, form]).then(onSuccess).catch(onError);
}
};
useEffect(() => {
if (item && item.type) {
changePageSubtitle(transitionItemTypeKeyToLabel(item.type));
}
}, [item, changePageSubtitle, formatMessage]);
const initialAttachmentFiles = useMemo(() => {
return item && item.media
? item.media.map((attach) => ({
preview: attach.attachment_file,
upload: true,
metadata: { ...attach },
}))
: [];
}, [item]);
const handleDropFiles = useCallback(
(_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
},
[setFiles],
);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
const handleCancelBtnClick = () => {
history.goBack();
};
const handleSubmitAndNewClick = () => {
setSubmitPayload({ redirect: false });
};
const handleSubmitClick = () => {
setSubmitPayload({ redirect: true });
};
return (
<div class={classNames(CLASSES.PAGE_FORM_ITEM)}>
<Formik
@@ -229,33 +156,21 @@ function ItemForm({
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ isSubmitting, handleSubmit }) => (
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection itemType={item?.type} />
<ItemFormBody />
<ItemFormInventorySection />
</div>
<ItemFormFloatingActions
isSubmitting={isSubmitting}
itemId={itemId}
handleSubmit={handleSubmit}
onCancelClick={handleCancelBtnClick}
onSubmitAndNewClick={handleSubmitAndNewClick}
onSubmitClick={handleSubmitClick}
/>
</Form>
)}
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions />
</Form>
</Formik>
</div>
);
}
export default compose(
withItemsActions,
withItem(({ item }) => ({ item })),
withDashboardActions,
withMediaActions,
withSettings(({ itemsSettings }) => ({
preferredCostAccount: parseInt(itemsSettings?.preferredCostAccount),
preferredSellAccount: parseInt(itemsSettings?.preferredSellAccount),

View File

@@ -12,15 +12,18 @@ import {
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import withAccounts from 'containers/Accounts/withAccounts';
import { useItemFormContext } from './ItemFormProvider';
import withSettings from 'containers/Settings/withSettings';
import { ACCOUNT_PARENT_TYPE } from 'common/accountTypes';
import { compose, inputIntent } from 'utils';
/**
* Item form body.
*/
function ItemFormBody({ accountsList, baseCurrency }) {
function ItemFormBody({ baseCurrency }) {
const { accounts } = useItemFormContext();
return (
<div class="page-form__section page-form__section--selling-cost">
<Row>
@@ -84,14 +87,14 @@ function ItemFormBody({ accountsList, baseCurrency }) {
)}
>
<AccountsSelectList
accounts={accountsList}
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('sell_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
disabled={!form.values.sellable}
filterByTypes={['income']}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
popoverFill={true}
/>
</FormGroup>
@@ -158,14 +161,14 @@ function ItemFormBody({ accountsList, baseCurrency }) {
)}
>
<AccountsSelectList
accounts={accountsList}
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('cost_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
disabled={!form.values.purchasable}
filterByTypes={['cost_of_goods_sold']}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
/>
</FormGroup>
@@ -178,9 +181,6 @@ function ItemFormBody({ accountsList, baseCurrency }) {
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),

View File

@@ -1,32 +1,39 @@
import React, { memo } from 'react';
import React from 'react';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { saveInvoke } from 'utils';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { FastField } from 'formik';
import { FastField, useFormikContext } from 'formik';
import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider';
/**
* Item form floating actions.
*/
export default function ItemFormFloatingActions({
isSubmitting,
itemId,
handleSubmit,
onCancelClick,
onSubmitClick,
onSubmitAndNewClick,
}) {
export default function ItemFormFloatingActions() {
// History context.
const history = useHistory();
// Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event.currentTarget.value);
history.goBack();
};
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
saveInvoke(onSubmitClick, event);
setSubmitPayload({ redirect: true });
};
// Handle submit & new button click.
const handleSubmitAndNewBtnClick = (event) => {
saveInvoke(onSubmitAndNewClick, event);
setSubmitPayload({ redirect: false });
};
return (
@@ -34,14 +41,16 @@ export default function ItemFormFloatingActions({
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
loading={isSubmitting}
onClick={handleSubmitBtnClick}
type="submit"
className={'btn--submit'}
>
{itemId ? <T id={'edit'} /> : <T id={'save'} />}
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />}
</Button>
<Button
className={'ml1'}
className={classNames('ml1', 'btn--submit-new')}
disabled={isSubmitting}
onClick={handleSubmitAndNewBtnClick}
type="submit"
@@ -49,7 +58,11 @@ export default function ItemFormFloatingActions({
<T id={'save_new'} />
</Button>
<Button className={'ml1'} onClick={handleCancelBtnClick}>
<Button
disabled={isSubmitting}
className={'ml1'}
onClick={handleCancelBtnClick}
>
<T id={'close'} />
</Button>

View File

@@ -1,37 +1,23 @@
import React from 'react';
import { FastField, ErrorMessage } from 'formik';
import {
FormGroup,
InputGroup,
ControlGroup,
Position,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import {
AccountsSelectList,
MoneyInputGroup,
InputPrependText,
Col,
Row,
Hint,
} from 'components';
import { FormGroup } from '@blueprintjs/core';
import { AccountsSelectList, Col, Row } from 'components';
import { CLASSES } from 'common/classes';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import withAccounts from 'containers/Accounts/withAccounts';
import withSettings from 'containers/Settings/withSettings';
import {
compose,
tansformDateValue,
momentFormatter,
inputIntent,
handleDateChange,
} from 'utils';
import { compose, inputIntent } from 'utils';
import { ACCOUNT_TYPE } from 'common/accountTypes';
import { useItemFormContext } from './ItemFormProvider';
/**
* Item form inventory sections.
*/
function ItemFormInventorySection({ accountsList, baseCurrency }) {
function ItemFormInventorySection({ baseCurrency }) {
const { accounts } = useItemFormContext();
return (
<div class="page-form__section page-form__section--inventory">
<h3>
@@ -55,12 +41,13 @@ function ItemFormInventorySection({ accountsList, baseCurrency }) {
)}
>
<AccountsSelectList
accounts={accountsList}
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('inventory_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={[ACCOUNT_TYPE.INVENTORY]}
/>
</FormGroup>
)}
@@ -72,9 +59,6 @@ function ItemFormInventorySection({ accountsList, baseCurrency }) {
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),

View File

@@ -1,82 +1,27 @@
import React, { useCallback } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import React from 'react';
import { useParams } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { ItemFormProvider } from './ItemFormProvider';
import DashboardCard from 'components/Dashboard/DashboardCard';
import ItemForm from 'containers/Items/ItemForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import withItemsActions from './withItemsActions';
import { compose } from 'utils';
const ItemFormContainer = ({
// #withDashboardActions
changePageTitle,
// #withAccountsActions
requestFetchAccounts,
// #withItemsActions
requestFetchItems,
requestFetchItem,
// #withItemCategoriesActions
requestFetchItemCategories,
}) => {
/**
* Item form page.
*/
function ItemFormPage() {
const { id } = useParams();
const history = useHistory();
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
const fetchCategories = useQuery('item-categories-list', (key) =>
requestFetchItemCategories(),
);
const fetchItemDetail = useQuery(
['item', id],
(key, _id) => requestFetchItem(_id),
{ enabled: id && id },
);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/items');
},
[history],
);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
<DashboardInsider
loading={
fetchItemDetail.isFetching ||
fetchAccounts.isFetching ||
fetchCategories.isFetching
}
name={'item-form'}
>
<ItemFormProvider itemId={id}>
<DashboardCard page>
<ItemForm
onFormSubmit={handleFormSubmit}
itemId={id}
onCancelForm={handleCancel}
/>
<ItemForm />
</DashboardCard>
</DashboardInsider>
</ItemFormProvider>
);
};
}
export default compose(
withDashboardActions,
withAccountsActions,
withItemCategoriesActions,
withItemsActions,
)(ItemFormContainer);
export default compose(withDashboardActions)(ItemFormPage);

View File

@@ -8,8 +8,7 @@ import {
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { ErrorMessage, FastField } from 'formik';
import { useIntl } from 'react-intl';
import { ErrorMessage, FastField, useFormikContext } from 'formik';
import {
CategoriesSelectList,
Hint,
@@ -20,28 +19,20 @@ import {
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import withItemCategories from 'containers/Items/withItemCategories';
import withAccounts from 'containers/Accounts/withAccounts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose, handleStringChange, inputIntent } from 'utils';
import { transitionItemTypeKeyToLabel } from './utils';
import { useItemFormContext } from './ItemFormProvider';
import { handleStringChange, inputIntent } from 'utils';
/**
* Item form primary section.
*/
function ItemFormPrimarySection({
// #withItemCategories
categoriesList,
export default function ItemFormPrimarySection() {
const { itemsCategories } = useItemFormContext();
// #withDashboardActions
changePageSubtitle,
// #ownProps
itemType,
}) {
const nameFieldRef = useRef(null);
// Formik context.
const { values: { itemType } } = useFormikContext();
useEffect(() => {
// Auto focus item name field once component mount.
if (nameFieldRef.current) {
@@ -94,7 +85,6 @@ function ItemFormPrimarySection({
inline={true}
onChange={handleStringChange((_value) => {
form.setFieldValue('type', _value);
changePageSubtitle(transitionItemTypeKeyToLabel(_value));
})}
selectedValue={value}
disabled={itemType === 'inventory'}
@@ -155,7 +145,7 @@ function ItemFormPrimarySection({
className={classNames('form-group--category', Classes.FILL)}
>
<CategoriesSelectList
categoriesList={categoriesList}
categories={itemsCategories}
selecetedCategoryId={value}
onCategorySelected={(category) => {
form.setFieldValue('category_id', category.id);
@@ -179,13 +169,3 @@ function ItemFormPrimarySection({
</div>
);
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withItemCategories(({ categoriesList }) => ({
categoriesList,
})),
withDashboardActions,
)(ItemFormPrimarySection);

View File

@@ -0,0 +1,85 @@
import React, { useEffect, createContext, useState } from 'react';
import { useIntl } from 'react-intl';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useItem,
useItemsCategories,
useCreateItem,
useEditItem,
useAccounts,
} from 'hooks/query';
import { useDashboardPageTitle } from 'hooks/state';
const ItemFormContext = createContext();
/**
* Accounts chart data provider.
*/
function ItemFormProvider({ itemId, ...props }) {
// Fetches the accounts list.
const { isFetching: isAccountsLoading, data: accounts } = useAccounts();
// Fetches the items categories list.
const {
isFetching: isItemsCategoriesLoading,
data: { itemsCategories },
} = useItemsCategories();
// Fetches the given item details.
const { isFetching: isItemLoading, data: item } = useItem(itemId, {
enabled: !!itemId,
});
// Create and edit item mutations.
const { mutateAsync: editItemMutate } = useEditItem();
const { mutateAsync: createItemMutate } = useCreateItem();
// Holds data of submit button once clicked to form submit function.
const [submitPayload, setSubmitPayload] = useState({});
// Detarmines whether the form new mode.
const isNewMode = !itemId;
// Provider state.
const provider = {
itemId,
accounts,
item,
itemsCategories,
submitPayload,
isNewMode,
isAccountsLoading,
isItemsCategoriesLoading,
isItemLoading,
createItemMutate,
editItemMutate,
setSubmitPayload
};
// Format message intl.
const { formatMessage } = useIntl();
// Change page title dispatcher.
const changePageTitle = useDashboardPageTitle();
// Changes the page title in new and edit mode.
useEffect(() => {
!isNewMode
? changePageTitle(formatMessage({ id: 'edit_item_details' }))
: changePageTitle(formatMessage({ id: 'new_item' }));
}, [changePageTitle, isNewMode, formatMessage]);
return (
<DashboardInsider
loading={isAccountsLoading || isItemsCategoriesLoading || isItemLoading}
name={'item-form'}
>
<ItemFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useItemFormContext = () => React.useContext(ItemFormContext);
export { ItemFormProvider, useItemFormContext };

View File

@@ -1,11 +1,9 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { useRouteMatch, useHistory, useParams } from 'react-router-dom';
import React from 'react';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import {
MenuItem,
Popover,
NavbarGroup,
Menu,
NavbarDivider,
PopoverInteractionKind,
Position,
@@ -16,57 +14,44 @@ import {
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import Icon from 'components/Icon';
import FilterDropdown from 'components/FilterDropdown';
import { If, DashboardActionViewsList } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import { useItemsListContext } from './ItemsListProvider';
import withItems from 'containers/Items/withItems';
import withItemsActions from './withItemsActions';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
import { connect } from 'react-redux';
const ItemsActionsBar = ({
// #withResourceDetail
resourceFields,
/**
* Items actions bar.
*/
function ItemsActionsBar({
// #withItems
itemsViews,
itemsSelectedRows,
//#withItemActions
// #withItemActions
addItemsTableQueries,
changeItemsCurrentView,
// #withAlertActions
openAlert,
onFilterChanged,
}) => {
}) {
// Items list context.
const { itemsViews } = useItemsListContext();
const { formatMessage } = useIntl();
// History context.
const history = useHistory();
const onClickNewItem = useCallback(() => {
// Handle `new item` button click.
const onClickNewItem = () => {
history.push('/items/new');
}, [history]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
initialCondition: {
fieldKey: 'name',
compatator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => {
addItemsTableQueries({
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
},
});
};
// Handle tab changing.
const handleTabChange = (viewId) => {
changeItemsCurrentView(viewId.id || -1);
addItemsTableQueries({
custom_view_id: viewId.id || null,
});
@@ -85,7 +70,6 @@ const ItemsActionsBar = ({
views={itemsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
@@ -97,7 +81,7 @@ const ItemsActionsBar = ({
<NavbarDivider />
<Popover
content={filterDropdown}
content={''}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
@@ -133,21 +117,9 @@ const ItemsActionsBar = ({
);
};
const mapStateToProps = (state, props) => ({
resourceName: 'items',
});
const withItemsActionsBar = connect(mapStateToProps);
export default compose(
withItemsActionsBar,
withItems(({ itemsViews, itemsSelectedRows }) => ({
itemsViews,
itemsSelectedRows,
})),
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
withItems(({ itemsSelectedRows }) => ({ itemsSelectedRows })),
withItemsActions,
withAlertActions,
)(ItemsActionsBar);

View File

@@ -3,9 +3,6 @@ import ItemDeleteAlert from 'containers/Alerts/Items/ItemDeleteAlert';
import ItemInactivateAlert from 'containers/Alerts/Items/ItemInactivateAlert';
import ItemActivateAlert from 'containers/Alerts/Items/ItemActivateAlert';
import ItemBulkDeleteAlert from 'containers/Alerts/Items/ItemBulkDeleteAlert';
import ItemCategoryDeleteAlert from 'containers/Alerts/Items/ItemCategoryDeleteAlert';
import ItemCategoryBulkDeleteAlert from 'containers/Alerts/Items/ItemCategoryBulkDeleteAlert';
import InventoryAdjustmentDeleteAlert from 'containers/Alerts/Items/InventoryAdjustmentDeleteAlert';
/**
* Items alert.
@@ -17,9 +14,6 @@ export default function ItemsAlerts() {
<ItemInactivateAlert name={'item-inactivate'} />
<ItemActivateAlert name={'item-activate'} />
<ItemBulkDeleteAlert name={'items-bulk-delete'} />
<ItemCategoryDeleteAlert name={'item-category-delete'} />
<ItemCategoryBulkDeleteAlert name={'item-categories-bulk-delete'} />
<InventoryAdjustmentDeleteAlert name={'inventory-adjustment-delete'} />
</div>
);
}

View File

@@ -1,114 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { If, Icon } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withItemCategories from './withItemCategories';
import withItemCategoriesActions from './withItemCategoriesActions';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
const ItemsCategoryActionsBar = ({
// #withDialog
openDialog,
// #withItemCategories
itemCategoriesSelectedRows,
// #withAlertActions
openAlert,
}) => {
const [filterCount, setFilterCount] = useState(0);
const onClickNewCategory = useCallback(() => {
openDialog('item-category-form', {});
}, [openDialog]);
const handelBulkDelete = () => {
openAlert('item-categories-bulk-delete', {
itemCategoriesIds: itemCategoriesSelectedRows,
});
};
console.log(itemCategoriesSelectedRows, 'EE');
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_category'} />}
onClick={onClickNewCategory}
/>
<NavbarDivider />
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} filters applied`
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={itemCategoriesSelectedRows.length}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handelBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
};
export default compose(
withDialogActions,
withDashboardActions,
withItemCategories(({ itemCategoriesSelectedRows }) => ({
itemCategoriesSelectedRows,
})),
withItemCategoriesActions,
withAlertActions,
)(ItemsCategoryActionsBar);

View File

@@ -1,170 +1,39 @@
import React, { useCallback, useMemo } from 'react';
import {
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
Intent,
Tag,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { DataTable, Choose } from 'components';
import {
Icon,
DataTable,
Money,
LoadingIndicator,
Choose,
If,
} from 'components';
import ItemsEmptyStatus from './ItemsEmptyStatus';
import { useIsValuePassed } from 'hooks';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { CLASSES } from 'common/classes';
import withItems from 'containers/Items/withItems';
import withItemsActions from 'containers/Items/withItemsActions';
import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose, saveInvoke, isBlank, defaultToTransform } from 'utils';
import { useItemsListContext } from './ItemsListProvider';
import { compose } from 'utils';
import {
QuantityOnHandCell,
SellPriceCell,
CostPriceCell,
ItemTypeAccessor,
ItemsActionsTableCell,
} from './components';
// Items datatable.
function ItemsDataTable({
// #withItems
itemsTableLoading,
itemsCurrentPage,
itemsTableQuery,
itemsCurrentViewId,
itemsPagination,
// #withDialogActions
openDialog,
// #withItemsActions
addItemsTableQueries,
// #withSettings
baseCurrency,
// props
onEditItem,
onDeleteItem,
onInactiveItem,
onActivateItem,
onSelectedRowsChange,
itemsViewLoading,
// #ownProps
tableProps
}) {
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(itemsTableLoading, false);
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addItemsTableQueries({
page_size: pageSize,
page: pageIndex + 1,
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
});
},
[addItemsTableQueries],
);
const handleMakeAdjustment = useCallback(
(item) => () => {
openDialog('inventory-adjustment-form', {
action: 'make_adjustment',
itemId: item.id,
});
},
[openDialog],
);
const handleEditItem = useCallback(
(item) => () => {
onEditItem && onEditItem(item);
},
[onEditItem],
);
const handleDeleteItem = useCallback(
(item) => () => {
onDeleteItem(item);
},
[onDeleteItem],
);
const actionMenuList = useCallback(
(item) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_item' })}
onClick={handleEditItem(item)}
/>
<If condition={item.active}>
<MenuItem
text={formatMessage({ id: 'inactivate_item' })}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={() => onInactiveItem(item)}
/>
</If>
<If condition={!item.active}>
<MenuItem
text={formatMessage({ id: 'activate_item' })}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={() => onActivateItem(item)}
/>
</If>
<If condition={item.type === 'inventory'}>
<MenuItem
text={formatMessage({ id: 'make_adjustment' })}
onClick={handleMakeAdjustment(item)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_item' })}
icon={<Icon icon="trash-16" iconSize={16} />}
onClick={handleDeleteItem(item)}
intent={Intent.DANGER}
/>
</Menu>
),
[
handleEditItem,
handleDeleteItem,
onInactiveItem,
onActivateItem,
formatMessage,
],
);
const quantityonHandCell = ({ value: quantity }) => {
return quantity <= 0 ? (
<span className={'quantity_on_hand'}>{quantity}</span>
) : (
<span>{quantity}</span>
);
};
const handleRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
const {
items,
pagination,
isItemsLoading,
isEmptyStatus,
} = useItemsListContext();
// Datatable columns.
const columns = useMemo(
() => [
{
@@ -181,14 +50,7 @@ function ItemsDataTable({
},
{
Header: formatMessage({ id: 'item_type' }),
accessor: (row) =>
row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{formatMessage({ id: row.type })}
</Tag>
) : (
''
),
accessor: ItemTypeAccessor,
className: 'item_type',
width: 120,
},
@@ -200,127 +62,77 @@ function ItemsDataTable({
},
{
Header: formatMessage({ id: 'sell_price' }),
accessor: (row) =>
!isBlank(row.sell_price) ? (
<Money amount={row.sell_price} currency={baseCurrency} />
) : (
''
),
Cell: SellPriceCell,
accessor: 'sell_price',
className: 'sell-price',
width: 150,
},
{
Header: formatMessage({ id: 'cost_price' }),
accessor: (row) =>
!isBlank(row.cost_price) ? (
<Money amount={row.cost_price} currency={baseCurrency} />
) : (
''
),
Cell: CostPriceCell,
accessor: 'cost_price',
className: 'cost-price',
width: 150,
},
{
Header: formatMessage({ id: 'quantity_on_hand' }),
accessor: 'quantity_on_hand',
Cell: quantityonHandCell,
Cell: QuantityOnHandCell,
width: 140,
},
{
id: 'actions',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
Cell: ItemsActionsTableCell,
width: 60,
skeletonWidthMin: 100,
},
],
[actionMenuList, formatMessage],
[formatMessage],
);
// Handle selected row change.
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
const rowClassNames = (row) => {
return {
inactive: !row.original.active,
};
};
const showEmptyStatus = [
itemsCurrentPage.length === 0,
itemsCurrentViewId === -1,
].every((condition) => condition === true);
// Table row class names.
const rowClassNames = (row) => ({
inactive: !row.original.active,
});
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator
loading={(itemsTableLoading && !isLoadedBefore) || itemsViewLoading}
>
<Choose>
<Choose.When condition={showEmptyStatus}>
<ItemsEmptyStatus />
</Choose.When>
<Choose>
<Choose.When condition={isEmptyStatus}>
<ItemsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={itemsCurrentPage}
onFetchData={handleFetchData}
noInitialFetch={true}
selectionColumn={true}
spinnerProps={{ size: 30 }}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={handleRowContextMenu}
expandable={false}
sticky={true}
rowClassNames={rowClassNames}
pagination={true}
pagesCount={itemsPagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={itemsTableQuery.page_size}
initialPageIndex={itemsTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
<Choose.Otherwise>
<DataTable
columns={columns}
data={items}
loading={isItemsLoading}
headerLoading={isItemsLoading}
noInitialFetch={true}
selectionColumn={true}
spinnerProps={{ size: 30 }}
expandable={false}
sticky={true}
rowClassNames={rowClassNames}
pagination={true}
manualSortBy={true}
pagesCount={1}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
initialPageSize={pagination.pageSize}
initialPageIndex={pagination.page}
{...tableProps}
/>
</Choose.Otherwise>
</Choose>
</div>
);
}
export default compose(
withItems(
({
itemsCurrentPage,
itemsTableLoading,
itemsTableQuery,
itemsCurrentViewId,
itemsPagination,
}) => ({
itemsCurrentPage,
itemsTableLoading,
itemsTableQuery,
itemsCurrentViewId,
itemsPagination,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
withItemsActions,
withDialogActions,
)(ItemsDataTable);

View File

@@ -1,96 +1,36 @@
import React, { useEffect, useCallback, useState, useMemo } from 'react';
import { useQuery } from 'react-query';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import React from 'react';
import { compose } from 'utils';
import 'style/pages/Items/List.scss';
import ItemsViewPage from './ItemsViewPage';
import ItemsActionsBar from 'containers/Items/ItemsActionsBar';
import ItemsActionsBar from './ItemsActionsBar';
import ItemsAlerts from './ItemsAlerts';
import { ItemsListProvider } from './ItemsListProvider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import withItems from 'containers/Items/withItems';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withViewsActions from 'containers/Views/withViewsActions';
import 'style/pages/Items/List.scss';
/**
* Items list.
*/
function ItemsList({
// #withDashboardActions
changePageTitle,
// #withResourceActions
requestFetchResourceViews,
requestFetchResourceFields,
// #withItems
itemsTableQuery,
// #withItemsActions
requestFetchItems,
addItemsTableQueries,
itemsTableQuery
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'items_list' }));
}, [changePageTitle, formatMessage]);
// Handle fetching the resource views.
const fetchResourceViews = useQuery(
['resource-views', 'items'],
(key, resourceName) => requestFetchResourceViews(resourceName),
);
// Handle fetching the resource fields.
const fetchResourceFields = useQuery(
['resource-fields', 'items'],
(key, resourceName) => requestFetchResourceFields(resourceName),
);
// Handle fetching the items table based on the given query.
const fetchItems = useQuery(['items-table', itemsTableQuery], (key, _query) =>
requestFetchItems({ ..._query }),
);
// Handle filter change to re-fetch the items.
const handleFilterChanged = useCallback(
(filterConditions) => {
addItemsTableQueries({
filter_roles: filterConditions || '',
});
},
[addItemsTableQueries],
);
return (
<DashboardInsider
loading={fetchResourceViews.isFetching || fetchResourceFields.isFetching}
name={'items-list'}
>
<ItemsActionsBar onFilterChanged={handleFilterChanged} />
<ItemsListProvider query={itemsTableQuery}>
<ItemsActionsBar />
<DashboardPageContent>
<ItemsViewPage />
</DashboardPageContent>
<ItemsAlerts />
</DashboardInsider>
</ItemsListProvider>
);
}
export default compose(
withResourceActions,
withDashboardActions,
withItemsActions,
withViewsActions,
withItems(({ itemsTableQuery }) => ({
itemsTableQuery,
})),
withItems(({ itemsTableQuery }) => ({ itemsTableQuery })),
)(ItemsList);

View File

@@ -0,0 +1,63 @@
import React, { useEffect, createContext } from 'react';
import { useIntl } from 'react-intl';
import { isEmpty } from 'lodash';
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 }) {
// Fetch accounts resource views and fields.
const { data: itemsViews, isFetching: isViewsLoading } = useResourceViews(
'items',
);
// Fetch the accounts resource fields.
const { data: itemsFields, isFetching: isFieldsLoading } = useResourceFields(
'items',
);
// Handle fetching the items table based on the given query.
const {
data: { items, pagination, filterMeta },
isFetching: isItemsLoading,
} = useItems(query);
// Detarmines the datatable empty status.
const isEmptyStatus = isEmpty(items) && !isItemsLoading && !filterMeta.view;
// Format message intl.
const { formatMessage } = useIntl();
// Change page title dispatcher.
const changePageTitle = useDashboardPageTitle();
useEffect(() => {
changePageTitle(formatMessage({ id: 'items_list' }));
}, [changePageTitle, formatMessage]);
const state = {
itemsViews,
itemsFields,
items,
pagination,
isViewsLoading,
isItemsLoading,
isEmptyStatus: false,
};
return (
<DashboardInsider
loading={isFieldsLoading || isViewsLoading}
name={'items-list'}
>
<ItemsContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useItemsListContext = () => React.useContext(ItemsContext);
export { ItemsListProvider, useItemsListContext };

View File

@@ -1,18 +1,25 @@
import React from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import ItemsViewsTabs from 'containers/Items/ItemsViewsTabs';
import ItemsDataTable from 'containers/Items/ItemsDataTable';
import ItemsViewsTabs from './ItemsViewsTabs';
import ItemsDataTable from './ItemsDataTable';
import withItemsActions from 'containers/Items/withItemsActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function ItemsViewPage({
// #withAlertsActions.
openAlert,
// #withDialogActions
openDialog,
// #withItemsActions.
setSelectedRowsItems,
addItemsTableQueries
}) {
const history = useHistory();
@@ -42,6 +49,25 @@ function ItemsViewPage({
history.push(`/items/${id}/edit`);
};
// 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 }) => {
addItemsTableQueries({
page_size: pageSize,
page: pageIndex,
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
});
};
return (
<Switch>
<Route
@@ -49,12 +75,17 @@ function ItemsViewPage({
path={['/items/:custom_view_id/custom_view', '/items']}
>
<ItemsViewsTabs />
<ItemsDataTable
onDeleteItem={handleDeleteItem}
onEditItem={handleEditItem}
onInactiveItem={handleInactiveItem}
onActivateItem={handleActivateItem}
tableProps={{
payload: {
onDeleteItem: handleDeleteItem,
onEditItem: handleEditItem,
onInactivateItem: handleInactiveItem,
onActivateItem: handleActivateItem,
onMakeAdjustment: handleMakeAdjustment
},
onFetchData: handleFetchData
}}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</Route>
@@ -62,4 +93,8 @@ function ItemsViewPage({
);
}
export default compose(withAlertsActions, withItemsActions)(ItemsViewPage);
export default compose(
withAlertsActions,
withItemsActions,
withDialogActions
)(ItemsViewPage);

View File

@@ -1,58 +1,28 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import { compose } from 'utils';
import { DashboardViewsTabs } from 'components';
import { pick } from 'lodash';
import withItemsActions from 'containers/Items/withItemsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetail from 'containers/Views/withViewDetails';
import withItems from 'containers/Items/withItems';
import { useItemsListContext } from './ItemsListProvider';
/**
* Items views tabs.
*/
function ItemsViewsTabs({
// #withViewDetail
viewId,
viewItem,
// #withItems
itemsViews,
// #withItemsActions
addItemsTableQueries,
changeItemsCurrentView,
// #withDashboardActions
setTopbarEditView,
changePageSubtitle,
// #props
onViewChanged,
}) {
const { custom_view_id: customViewId = null } = useParams();
useEffect(() => {
setTopbarEditView(customViewId);
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
addItemsTableQueries({
custom_view_id: customViewId || null,
});
}, [customViewId]);
const handleClickNewView = () => {};
const { itemsViews } = useItemsListContext();
const tabs = itemsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const handleTabChange = (viewId) => {
changeItemsCurrentView(viewId || -1);
addItemsTableQueries({
custom_view_id: viewId || null,
});
@@ -72,20 +42,7 @@ function ItemsViewsTabs({
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
});
const withItemsViewsTabs = connect(mapStateToProps);
export default compose(
withRouter,
withItemsViewsTabs,
withDashboardActions,
withItemsActions,
withViewDetail(),
withItems(({ itemsViews }) => ({
itemsViews,
})),
)(ItemsViewsTabs);

View File

@@ -0,0 +1,137 @@
import React from 'react';
import {
Menu,
MenuDivider,
MenuItem,
Intent,
Tag,
Position,
Button,
Popover,
} from '@blueprintjs/core';
import { useIntl, FormattedMessage as T } from 'react-intl';
import { formatMessage } from 'services/intl';
import { isNumber } from 'lodash';
import { Icon, Money, If } from 'components';
import { isBlank, safeCallback } from 'utils';
/**
* Publish accessor
*/
export const PublishAccessor = (r) => {
return r.is_published ? (
<Tag minimal={true}>
<T id={'published'} />
</Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
);
};
export const TypeAccessor = (row) => {
return row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{formatMessage({ id: row.type })}
</Tag>
) : (
''
);
};
export const ItemCodeAccessor = (row) =>
row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{formatMessage({ id: row.type })}
</Tag>
) : (
''
);
export const QuantityOnHandCell = ({ cell: { value } }) => {
return isNumber(value) ? (
<span className={'quantity_on_hand'}>{value}</span>
) : null;
};
export const CostPriceCell = ({ cell: { value } }) => {
return !isBlank(value) ? <Money amount={value} currency={'USD'} /> : null;
};
export const SellPriceCell = ({ cell: { value } }) => {
return !isBlank(value) ? <Money amount={value} currency={'USD'} /> : null;
};
export const ItemTypeAccessor = (row) => {
return row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{formatMessage({ id: row.type })}
</Tag>
) : null;
};
export const ItemsActionMenuList = ({
row: { original },
payload: {
onEditItem,
onInactivateItem,
onActivateItem,
onMakeAdjustment,
onDeleteItem,
},
}) => {
const { formatMessage } = useIntl();
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={<T id={'view_details'} />}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_item' })}
onClick={safeCallback(onEditItem, original)}
/>
<If condition={original.active}>
<MenuItem
text={formatMessage({ id: 'inactivate_item' })}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivateItem, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={formatMessage({ id: 'activate_item' })}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivateItem, original)}
/>
</If>
<If condition={original.type === 'inventory'}>
<MenuItem
text={formatMessage({ id: 'make_adjustment' })}
onClick={safeCallback(onMakeAdjustment, original)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_item' })}
icon={<Icon icon="trash-16" iconSize={16} />}
onClick={safeCallback(onDeleteItem, original)}
intent={Intent.DANGER}
/>
</Menu>
);
};
export const ItemsActionsTableCell = (props) => {
return (
<Popover
position={Position.RIGHT_BOTTOM}
content={<ItemsActionMenuList {...props} />}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
};

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import {
submitInventoryAdjustment,
deleteInventoryAdjustment,
fetchInventoryAdjustmentsTable,
} from 'store/inventoryAdjustments/inventoryAdjustment.actions';
import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({
requestSubmitInventoryAdjustment: ({ form }) =>
dispatch(submitInventoryAdjustment({ form })),
requestFetchInventoryAdjustmentTable: (query = {}) =>
dispatch(fetchInventoryAdjustmentsTable({ query: { ...query } })),
requestDeleteInventoryAdjustment: (id) =>
dispatch(deleteInventoryAdjustment({ id })),
addInventoryAdjustmentTableQueries: (queries) =>
dispatch({
type: t.INVENTORY_ADJUSTMENTS_TABLE_QUERIES_ADD,
payload: { queries },
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import {
getInvoiceTableQueryFactory,
getInventoryAdjustmentCurrentPageFactory,
getInventoryAdjustmentPaginationMetaFactory,
} from 'store/inventoryAdjustments/inventoryAdjustment.selector';
export default (mapState) => {
const getInventoryAdjustmentItems = getInventoryAdjustmentCurrentPageFactory();
const getInventoryAdjustmentTableQuery = getInvoiceTableQueryFactory();
const getInventoryAdjustmentsPaginationMeta = getInventoryAdjustmentPaginationMetaFactory();
const mapStateToProps = (state, props) => {
const query = getInventoryAdjustmentTableQuery(state, props);
const mapped = {
inventoryAdjustmentCurrentPage: getInventoryAdjustmentItems(
state,
props,
query,
),
inventoryAdjustmentItems: Object.values(state.inventoryAdjustments.items),
inventoryAdjustmentTableQuery: query,
inventoryAdjustmentsPagination: getInventoryAdjustmentsPaginationMeta(
state,
props,
query,
),
inventoryAdjustmentLoading: state.inventoryAdjustments.loading,
inventoryAdjustmentsSelectedRows: state.inventoryAdjustments.selectedRows,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { getItemsCategoriesListFactory } from 'store/itemCategories/ItemsCategories.selectors';
import { getResourceViews } from 'store/customViews/customViews.selectors';
export default (mapState) => {
const getItemsCategoriesList = getItemsCategoriesListFactory();
const mapStateToProps = (state, props) => {
const mapped = {
categoriesList: getItemsCategoriesList(state, props),
itemCategoriesViews: getResourceViews(state, props, 'items_categories'),
categoriesTableLoading: state.itemCategories.loading,
itemCategoriesSelectedRows: state.itemCategories.selectedRows,
};
return mapState ? mapState(mapped, state, props) : mapState;
};
return connect(mapStateToProps);
};

View File

@@ -1,38 +0,0 @@
import { connect } from 'react-redux';
import {
fetchItemCategories,
submitItemCategory,
deleteItemCategory,
editItemCategory,
deleteBulkItemCategories,
} from 'store/itemCategories/itemsCategory.actions';
import t from 'store/types';
export const mapDispatchToProps = (dispatch) => ({
requestSubmitItemCategory: (form) => dispatch(submitItemCategory({ form })),
requestFetchItemCategories: (query) =>
dispatch(fetchItemCategories({ query })),
requestDeleteItemCategory: (id) => dispatch(deleteItemCategory(id)),
requestEditItemCategory: (id, form) => dispatch(editItemCategory(id, form)),
requestDeleteBulkItemCategories: (ids) =>
dispatch(deleteBulkItemCategories({ ids })),
changeItemCategoriesView: (id) =>
dispatch({
type: t.ITEM_CATEGORIES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addItemCategoriesTableQueries: (queries) =>
dispatch({
type: t.ITEM_CATEGORIES_TABLE_QUERIES_ADD,
queries,
}),
setSelectedRowsCategories: (selectedRows) =>
dispatch({
type: t.ITEM_CATEGORY_SELECTED_ROW_SET,
payload: { selectedRows },
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { getItemCategoryByIdFactory } from 'store/itemCategories/ItemsCategories.selectors';
export default () => {
const getCategoryId = getItemCategoryByIdFactory();
const mapStateToProps = (state, props) => {
return {
itemCategoryDetail: getCategoryId(state, props),
};
};
return connect(mapStateToProps);
};