mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
WIP Version 0.0.1
This commit is contained in:
53
client/src/containers/Items/ItemCategoriesList.js
Normal file
53
client/src/containers/Items/ItemCategoriesList.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import ItemCategoriesDataTable from 'containers/Items/ItemCategoriesTable';
|
||||
import ItemsCategoryActionsBar from 'containers/Items/ItemsCategoryActionsBar';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
|
||||
import withItemCategories from 'containers/Items/withItemCategories';
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
const ItemCategoryList = ({
|
||||
changePageTitle,
|
||||
requestFetchItemCategories,
|
||||
}) => {
|
||||
const { id } = useParams();
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
id
|
||||
? changePageTitle('Edit Category Details')
|
||||
: changePageTitle('Category List');
|
||||
}, []);
|
||||
|
||||
const fetchCategories = useAsync(() => {
|
||||
return Promise.all([
|
||||
requestFetchItemCategories(),
|
||||
]);
|
||||
});
|
||||
|
||||
const handleFilterChanged = useCallback(() => {
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DashboardInsider name={'item-category-list'}>
|
||||
<ItemsCategoryActionsBar
|
||||
onFilterChanged={handleFilterChanged}
|
||||
selectedRows={selectedRows} />
|
||||
|
||||
<ItemCategoriesDataTable />
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
withItemCategoriesActions,
|
||||
withItemCategories,
|
||||
)(ItemCategoryList);
|
||||
108
client/src/containers/Items/ItemCategoriesTable.js
Normal file
108
client/src/containers/Items/ItemCategoriesTable.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import { compose } from 'utils';
|
||||
import DataTable from 'components/DataTable';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
const ItemsCategoryList = ({
|
||||
categories,
|
||||
onFetchData,
|
||||
onDeleteCategory,
|
||||
onEditCategory,
|
||||
openDialog,
|
||||
count,
|
||||
onSelectedRowsChange,
|
||||
}) => {
|
||||
const handelEditCategory = (category) => () => {
|
||||
openDialog('item-form', { action: 'edit', id: category.id });
|
||||
onEditCategory(category.id);
|
||||
};
|
||||
|
||||
const handleDeleteCategory = (category) => () => {
|
||||
onDeleteCategory(category);
|
||||
};
|
||||
|
||||
const actionMenuList = (category) => (
|
||||
<Menu>
|
||||
<MenuItem text='Edit Category' onClick={handelEditCategory(category)} />
|
||||
<MenuItem
|
||||
text='Delete Category'
|
||||
onClick={handleDeleteCategory(category)}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: 'Category Name',
|
||||
accessor: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
className: 'description',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
id: 'count',
|
||||
Header: 'Count',
|
||||
accessor: (r) => r.count || '',
|
||||
className: 'count',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Cell: ({ cell }) => (
|
||||
<Popover
|
||||
content={actionMenuList(cell.row.original)}
|
||||
position={Position.RIGHT_BOTTOM}
|
||||
>
|
||||
<Button icon={<Icon icon='ellipsis-h' />} />
|
||||
</Popover>
|
||||
),
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
disableResizing: false
|
||||
},
|
||||
], [actionMenuList]);
|
||||
|
||||
const handelFetchData = useCallback(() => {
|
||||
onFetchData && onFetchData();
|
||||
}, []);
|
||||
|
||||
const handleSelectedRowsChange = useCallback((selectedRows) => {
|
||||
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
|
||||
}, [onSelectedRowsChange]);
|
||||
|
||||
return (
|
||||
<LoadingIndicator spinnerSize={30}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(categories)}
|
||||
onFetchData={handelFetchData}
|
||||
manualSortBy={true}
|
||||
selectionColumn={true}
|
||||
expandable={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
/>
|
||||
</LoadingIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
ItemsCategoryConnect,
|
||||
)(ItemsCategoryList);
|
||||
459
client/src/containers/Items/ItemForm.js
Normal file
459
client/src/containers/Items/ItemForm.js
Normal file
@@ -0,0 +1,459 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
FormGroup,
|
||||
MenuItem,
|
||||
Intent,
|
||||
InputGroup,
|
||||
HTMLSelect,
|
||||
Button,
|
||||
Classes,
|
||||
Checkbox,
|
||||
} from '@blueprintjs/core';
|
||||
import { Row, Col } from 'react-grid-system';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import {compose} from 'utils';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'components/Icon';
|
||||
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
import Dragzone from 'components/Dragzone';
|
||||
import MediaConnect from 'connectors/Media.connect';
|
||||
import useMedia from 'hooks/useMedia';
|
||||
|
||||
const ItemForm = ({
|
||||
requestSubmitItem,
|
||||
|
||||
accounts,
|
||||
categories,
|
||||
|
||||
requestSubmitMedia,
|
||||
requestDeleteMedia,
|
||||
}) => {
|
||||
const [selectedAccounts, setSelectedAccounts] = useState({});
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
files,
|
||||
setFiles,
|
||||
saveMedia,
|
||||
deletedFiles,
|
||||
setDeletedFiles,
|
||||
deleteMedia,
|
||||
} = useMedia({
|
||||
saveCallback: requestSubmitMedia,
|
||||
deleteCallback: requestDeleteMedia,
|
||||
})
|
||||
|
||||
const ItemTypeDisplay = useMemo(() => ([
|
||||
{ value: null, label: 'Select Item Type' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'inventory', label: 'Inventory' },
|
||||
{ value: 'non-inventory', label: 'Non-Inventory' }
|
||||
]), []);
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
active: Yup.boolean(),
|
||||
name: Yup.string().required(),
|
||||
type: Yup.string().trim().required(),
|
||||
sku: Yup.string().trim(),
|
||||
cost_price: Yup.number(),
|
||||
sell_price: Yup.number(),
|
||||
cost_account_id: Yup.number().required(),
|
||||
sell_account_id: Yup.number().required(),
|
||||
inventory_account_id: Yup.number().when('type', {
|
||||
is: (value) => value === 'inventory',
|
||||
then: Yup.number().required(),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
category_id: Yup.number().nullable(),
|
||||
stock: Yup.string() || Yup.boolean()
|
||||
});
|
||||
|
||||
const initialValues = useMemo(() => ({
|
||||
active: true,
|
||||
name: '',
|
||||
type: '',
|
||||
sku: '',
|
||||
cost_price: 0,
|
||||
sell_price: 0,
|
||||
cost_account_id: null,
|
||||
sell_account_id: null,
|
||||
inventory_account_id: null,
|
||||
category_id: null,
|
||||
note: '',
|
||||
}), []);
|
||||
|
||||
const {
|
||||
getFieldProps,
|
||||
setFieldValue,
|
||||
values,
|
||||
touched,
|
||||
errors,
|
||||
handleSubmit,
|
||||
} = useFormik({
|
||||
enableReinitialize: true,
|
||||
validationSchema: validationSchema,
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
},
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
const saveItem = (mediaIds) => {
|
||||
const formValues = { ...values, media_ids: mediaIds };
|
||||
|
||||
return requestSubmitItem(formValues).then((response) => {
|
||||
AppToaster.show({
|
||||
message: 'The_Items_has_been_submit'
|
||||
});
|
||||
setSubmitting(false);
|
||||
history.push('/dashboard/items');
|
||||
})
|
||||
.catch((error) => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
Promise.all([
|
||||
saveMedia(),
|
||||
deleteMedia(),
|
||||
]).then(([savedMediaResponses]) => {
|
||||
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
|
||||
return saveItem(mediaIds);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const accountItem = useCallback((item, { handleClick }) => (
|
||||
<MenuItem key={item.id} text={item.name} label={item.code} onClick={handleClick} />
|
||||
), []);
|
||||
|
||||
// Filter Account Items
|
||||
const filterAccounts = (query, account, _index, exactMatch) => {
|
||||
const normalizedTitle = account.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
const onItemAccountSelect = useCallback((filedName) => {
|
||||
return (account) => {
|
||||
setSelectedAccounts({
|
||||
...selectedAccounts,
|
||||
[filedName]: account
|
||||
});
|
||||
setFieldValue(filedName, account.id);
|
||||
};
|
||||
}, [setFieldValue, selectedAccounts]);
|
||||
|
||||
const categoryItem = useCallback((item, { handleClick }) => (
|
||||
<MenuItem text={item.name} onClick={handleClick} />
|
||||
), []);
|
||||
|
||||
const getSelectedAccountLabel = useCallback((fieldName, defaultLabel) => {
|
||||
return typeof selectedAccounts[fieldName] !== 'undefined'
|
||||
? selectedAccounts[fieldName].name : defaultLabel;
|
||||
}, [selectedAccounts]);
|
||||
|
||||
const requiredSpan = useMemo(() => (<span class="required">*</span>), []);
|
||||
const infoIcon = useMemo(() => (<Icon icon="info-circle" iconSize={12} />), []);
|
||||
|
||||
const handleMoneyInputChange = (fieldKey) => (e, value) => {
|
||||
setFieldValue(fieldKey, value);
|
||||
};
|
||||
|
||||
const initialAttachmentFiles = useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const handleDropFiles = useCallback((_files) => {
|
||||
setFiles(_files.filter((file) => file.uploaded === false));
|
||||
}, []);
|
||||
|
||||
const handleDeleteFile = useCallback((_deletedFiles) => {
|
||||
_deletedFiles.forEach((deletedFile) => {
|
||||
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||
setDeletedFiles([
|
||||
...deletedFiles, deletedFile.metadata.id,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}, [setDeletedFiles, deletedFiles]);
|
||||
|
||||
const handleCancelClickBtn = () => { history.goBack(); };
|
||||
|
||||
return (
|
||||
<div class='item-form'>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class="item-form__primary-section">
|
||||
<Row>
|
||||
<Col xs={7}>
|
||||
<FormGroup
|
||||
medium={true}
|
||||
label={'Item Type'}
|
||||
labelInfo={requiredSpan}
|
||||
className={'form-group--item-type'}
|
||||
intent={(errors.type && touched.type) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="type" />}
|
||||
inline={true}
|
||||
>
|
||||
<HTMLSelect
|
||||
fill={true}
|
||||
options={ItemTypeDisplay}
|
||||
{...getFieldProps('type')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Item Name'}
|
||||
labelInfo={requiredSpan}
|
||||
className={'form-group--item-name'}
|
||||
intent={(errors.name && touched.name) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="name" />}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={(errors.name && touched.name) && Intent.DANGER}
|
||||
{...getFieldProps('name')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'SKU'}
|
||||
labelInfo={infoIcon}
|
||||
className={'form-group--item-sku'}
|
||||
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="sku" />}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
||||
{...getFieldProps('sku')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Category'}
|
||||
labelInfo={infoIcon}
|
||||
inline={true}
|
||||
intent={(errors.category_id && touched.category_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="category" />}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--category',
|
||||
Classes.FILL,
|
||||
)}
|
||||
>
|
||||
<Select
|
||||
items={categories}
|
||||
itemRenderer={categoryItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('category_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('category_id', 'Select category')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={' '}
|
||||
inline={true}
|
||||
className={'form-group--active'}
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={'Active'}
|
||||
defaultChecked={values.active}
|
||||
{...getFieldProps('active')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col xs={3}>
|
||||
<Dragzone
|
||||
initialFiles={initialAttachmentFiles}
|
||||
onDrop={handleDropFiles}
|
||||
onDeleteFile={handleDeleteFile}
|
||||
hint={'Attachments: Maxiumum size: 20MB'}
|
||||
className={'mt2'} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Row gutterWidth={16} className={'item-form__accounts-section'}>
|
||||
<Col width={404}>
|
||||
<h4>Purchase Information</h4>
|
||||
|
||||
<FormGroup
|
||||
label={'Selling Price'}
|
||||
className={'form-group--item-selling-price'}
|
||||
intent={(errors.selling_price && touched.selling_price) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="selling_price" />}
|
||||
inline={true}
|
||||
>
|
||||
<MoneyInputGroup
|
||||
value={values.selling_price}
|
||||
prefix={'$'}
|
||||
onChange={handleMoneyInputChange('selling_price')}
|
||||
inputGroupProps={{
|
||||
medium: true,
|
||||
intent: (errors.selling_price && touched.selling_price) && Intent.DANGER,
|
||||
}} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Account'}
|
||||
labelInfo={infoIcon}
|
||||
inline={true}
|
||||
intent={(errors.sell_account_id && touched.sell_account_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="sell_account_id" />}
|
||||
className={classNames(
|
||||
'form-group--sell-account', 'form-group--select-list',
|
||||
Classes.FILL)}
|
||||
>
|
||||
<Select
|
||||
items={accounts}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('sell_account_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('sell_account_id', 'Select account')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col width={404}>
|
||||
<h4>
|
||||
Sales Information
|
||||
</h4>
|
||||
|
||||
<FormGroup
|
||||
label={'Cost Price'}
|
||||
className={'form-group--item-cost-price'}
|
||||
intent={(errors.cost_price && touched.cost_price) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="cost_price" />}
|
||||
inline={true}
|
||||
>
|
||||
<MoneyInputGroup
|
||||
value={values.cost_price}
|
||||
prefix={'$'}
|
||||
onChange={handleMoneyInputChange('cost_price')}
|
||||
inputGroupProps={{
|
||||
medium: true,
|
||||
intent: (errors.cost_price && touched.cost_price) && Intent.DANGER,
|
||||
}} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Account'}
|
||||
labelInfo={infoIcon}
|
||||
inline={true}
|
||||
intent={(errors.cost_account_id && touched.cost_account_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="cost_account_id" />}
|
||||
className={classNames(
|
||||
'form-group--cost-account',
|
||||
'form-group--select-list',
|
||||
Classes.FILL)}
|
||||
>
|
||||
<Select
|
||||
items={accounts}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('cost_account_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('cost_account_id', 'Select account')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className={'item-form__accounts-section mt2'}>
|
||||
<Col width={404}>
|
||||
<h4>
|
||||
Inventory Information
|
||||
</h4>
|
||||
|
||||
<FormGroup
|
||||
label={'Inventory Account'}
|
||||
inline={true}
|
||||
intent={(errors.inventory_account_id && touched.inventory_account_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="inventory_account_id" />}
|
||||
className={classNames(
|
||||
'form-group--item-inventory_account',
|
||||
'form-group--select-list',
|
||||
Classes.FILL)}
|
||||
>
|
||||
<Select
|
||||
items={accounts}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('inventory_account_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('inventory_account_id','Select account')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Opening Stock'}
|
||||
className={'form-group--item-stock'}
|
||||
// intent={errors.cost_price && Intent.DANGER}
|
||||
// helperText={formik.errors.stock && formik.errors.stock}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={errors.stock && Intent.DANGER}
|
||||
{...getFieldProps('stock')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div class='form__floating-footer'>
|
||||
<Button intent={Intent.PRIMARY} type='submit'>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button className={'ml1'}>Save as Draft</Button>
|
||||
<Button className={'ml1'} onClick={handleCancelClickBtn}>Close</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
AccountsConnect,
|
||||
ItemsConnect,
|
||||
ItemCategoryConnect,
|
||||
MediaConnect,
|
||||
)(ItemForm);
|
||||
52
client/src/containers/Items/ItemFormPage.js
Normal file
52
client/src/containers/Items/ItemFormPage.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import ItemForm from 'containers/Items/ItemForm';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
const ItemFormContainer = ({
|
||||
// #withDashboard
|
||||
changePageTitle,
|
||||
|
||||
// #withAccountsActions
|
||||
requestFetchAccounts,
|
||||
|
||||
// #withItemCategoriesActions
|
||||
requestFetchItemCategories,
|
||||
}) => {
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
id ?
|
||||
changePageTitle('Edit Item Details') :
|
||||
changePageTitle('New Item');
|
||||
}, [id, changePageTitle]);
|
||||
|
||||
const fetchAccounts = useQuery('accounts-list',
|
||||
(key) => requestFetchAccounts());
|
||||
|
||||
const fetchCategories = useQuery('item-categories-list',
|
||||
(key) => requestFetchItemCategories());
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={fetchAccounts.isFetching || fetchCategories.isFetching}
|
||||
name={'item-form'}>
|
||||
<ItemForm />
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withDashboard,
|
||||
withAccountsActions,
|
||||
withItemCategoriesActions,
|
||||
)(ItemFormContainer);
|
||||
133
client/src/containers/Items/ItemsActionsBar.js
Normal file
133
client/src/containers/Items/ItemsActionsBar.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
MenuItem,
|
||||
Popover,
|
||||
NavbarGroup,
|
||||
Menu,
|
||||
NavbarDivider,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Button,
|
||||
Classes,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { compose } from 'utils';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
import Icon from 'components/Icon';
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import { If } from 'components';
|
||||
|
||||
const ItemsActionsBar = ({
|
||||
openDialog,
|
||||
|
||||
resourceName = 'items',
|
||||
resourceFields,
|
||||
|
||||
itemsViews,
|
||||
|
||||
onFilterChanged,
|
||||
selectedRows = [],
|
||||
}) => {
|
||||
const { path } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
const [filterCount, setFilterCount] = useState(0);
|
||||
|
||||
const viewsMenuItems = itemsViews.map(view =>
|
||||
(<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />));
|
||||
|
||||
const onClickNewItem = () => {
|
||||
history.push('/dashboard/items/new');
|
||||
};
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
setFilterCount(filterConditions.length);
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
}
|
||||
});
|
||||
|
||||
const onClickNewCategory = useCallback(() => {
|
||||
openDialog('item-form', {});
|
||||
}, [openDialog]);
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<Popover
|
||||
content={<Menu>{viewsMenuItems}</Menu>}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--table-views')}
|
||||
icon={<Icon icon='table' />}
|
||||
text='Table Views'
|
||||
rightIcon={'caret-down'}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<NavbarDivider />
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='plus' />}
|
||||
text='New Item'
|
||||
onClick={onClickNewItem}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='plus' />}
|
||||
text='New Category'
|
||||
onClick={onClickNewCategory}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--filter')}
|
||||
text={filterCount <= 0 ? 'Filter' : `${filterCount} filters applied`}
|
||||
icon={<Icon icon='filter' />}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<If condition={hasSelectedRows}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
intent={Intent.DANGER}
|
||||
icon={<Icon icon='trash' />}
|
||||
text='Delete'
|
||||
/>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-import' />}
|
||||
text='Import'
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-export' />}
|
||||
text='Export'
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
withItems,
|
||||
withResourceDetail,
|
||||
)(ItemsActionsBar);
|
||||
108
client/src/containers/Items/ItemsCategoryActionsBar.js
Normal file
108
client/src/containers/Items/ItemsCategoryActionsBar.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
import { compose } from 'utils';
|
||||
import {
|
||||
NavbarGroup,
|
||||
Button,
|
||||
Classes,
|
||||
Intent,
|
||||
Popover,
|
||||
Position,
|
||||
PopoverInteractionKind,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { If } from 'components';
|
||||
|
||||
import Icon from 'components/Icon';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
|
||||
|
||||
const ItemsCategoryActionsBar = ({
|
||||
resourceName = 'item_category',
|
||||
resourceFields,
|
||||
|
||||
openDialog,
|
||||
onDeleteCategory,
|
||||
onFilterChanged,
|
||||
selectedRows,
|
||||
}) => {
|
||||
const onClickNewCategory = useCallback(() => {
|
||||
openDialog('item-form', {});
|
||||
}, [openDialog]);
|
||||
|
||||
const handleDeleteCategory = useCallback((category) => {
|
||||
onDeleteCategory(selectedRows);
|
||||
}, [selectedRows, onDeleteCategory]);
|
||||
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='plus' />}
|
||||
text='New Category'
|
||||
onClick={onClickNewCategory}
|
||||
/>
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--filter')}
|
||||
text='Filter'
|
||||
icon={<Icon icon='filter' />}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<If condition={hasSelectedRows}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='trash' iconSize={15} />}
|
||||
text='Delete'
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteCategory}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-import' />}
|
||||
text='Import'
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-export' />}
|
||||
text='Export'
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: 'items_categories',
|
||||
});
|
||||
|
||||
const withItemsCategoriesActionsBar = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withItemsCategoriesActionsBar,
|
||||
DialogConnect,
|
||||
withDashboard,
|
||||
withResourceDetail
|
||||
)(ItemsCategoryActionsBar);
|
||||
134
client/src/containers/Items/ItemsDataTable.js
Normal file
134
client/src/containers/Items/ItemsDataTable.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, {useState, useEffect, useCallback, useMemo} from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Position,
|
||||
} from '@blueprintjs/core'
|
||||
import {compose} from 'utils';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Icon from 'components/Icon';
|
||||
import Money from 'components/Money';
|
||||
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
|
||||
const ItemsDataTable = ({
|
||||
loading,
|
||||
|
||||
itemsTableLoading,
|
||||
itemsCurrentPage,
|
||||
|
||||
// props
|
||||
onEditItem,
|
||||
onDeleteItem,
|
||||
onFetchData,
|
||||
onSelectedRowsChange,
|
||||
}) => {
|
||||
const [initialMount, setInitialMount] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!itemsTableLoading) {
|
||||
setInitialMount(true);
|
||||
}
|
||||
}, [itemsTableLoading, setInitialMount]);
|
||||
|
||||
const handleEditItem = (item) => () => { onEditItem(item); };
|
||||
const handleDeleteItem = (item) => () => { onDeleteItem(item); };
|
||||
|
||||
const actionMenuList = useCallback((item) =>
|
||||
(<Menu>
|
||||
<MenuItem text="View Details" />
|
||||
<MenuDivider />
|
||||
<MenuItem text="Edit Item" onClick={handleEditItem(item)} />
|
||||
<MenuItem text="Delete Item" onClick={handleDeleteItem(item)} />
|
||||
</Menu>), [handleEditItem, handleDeleteItem]);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
Header: 'Item Name',
|
||||
accessor: 'name',
|
||||
className: "actions",
|
||||
},
|
||||
{
|
||||
Header: 'SKU',
|
||||
accessor: 'sku',
|
||||
className: "sku",
|
||||
},
|
||||
{
|
||||
Header: 'Category',
|
||||
accessor: 'category.name',
|
||||
className: 'category',
|
||||
},
|
||||
{
|
||||
Header: 'Sell Price',
|
||||
accessor: row => (<Money amount={row.sell_price} currency={'USD'} />),
|
||||
className: 'sell-price',
|
||||
},
|
||||
{
|
||||
Header: 'Cost Price',
|
||||
accessor: row => (<Money amount={row.cost_price} currency={'USD'} />),
|
||||
className: 'cost-price',
|
||||
},
|
||||
// {
|
||||
// Header: 'Cost Account',
|
||||
// accessor: 'cost_account.name',
|
||||
// className: "cost-account",
|
||||
// },
|
||||
// {
|
||||
// Header: 'Sell Account',
|
||||
// accessor: 'sell_account.name',
|
||||
// className: "sell-account",
|
||||
// },
|
||||
// {
|
||||
// Header: 'Inventory Account',
|
||||
// accessor: 'inventory_account.name',
|
||||
// className: "inventory-account",
|
||||
// },
|
||||
{
|
||||
id: 'actions',
|
||||
Cell: ({ cell }) => (
|
||||
<Popover
|
||||
content={actionMenuList(cell.row.original)}
|
||||
position={Position.RIGHT_BOTTOM}>
|
||||
<Button icon={<Icon icon="ellipsis-h" />} />
|
||||
</Popover>
|
||||
),
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
},
|
||||
], [actionMenuList]);
|
||||
|
||||
const selectionColumn = useMemo(() => ({
|
||||
minWidth: 42,
|
||||
width: 42,
|
||||
maxWidth: 42,
|
||||
}), []);
|
||||
|
||||
const handleFetchData = useCallback((...args) => {
|
||||
onFetchData && onFetchData(...args)
|
||||
}, [onFetchData]);
|
||||
|
||||
const handleSelectedRowsChange = useCallback((selectedRows) => {
|
||||
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
|
||||
}, [onSelectedRowsChange]);
|
||||
|
||||
return (
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={itemsCurrentPage}
|
||||
selectionColumn={selectionColumn}
|
||||
onFetchData={handleFetchData}
|
||||
loading={itemsTableLoading && !initialMount}
|
||||
noInitialFetch={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange} />
|
||||
</LoadingIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withItems,
|
||||
)(ItemsDataTable);
|
||||
172
client/src/containers/Items/ItemsList.js
Normal file
172
client/src/containers/Items/ItemsList.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import {
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Intent,
|
||||
Alert,
|
||||
} from '@blueprintjs/core';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import { useQuery } from 'react-query';
|
||||
import ItemsActionsBar from 'containers/Items/ItemsActionsBar';
|
||||
import { compose } from 'utils';
|
||||
import ItemsDataTable from './ItemsDataTable';
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
|
||||
import ItemsViewsTabs from 'containers/Items/ItemsViewsTabs';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import withResourceActions from 'containers/Resources/withResourcesActions';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withItemsActions from 'containers/Items/withItemsActions';
|
||||
import withViewsActions from 'containers/Views/withViewsActions';
|
||||
|
||||
|
||||
function ItemsList({
|
||||
// #withDashboard
|
||||
changePageTitle,
|
||||
|
||||
// #withResourceActions
|
||||
requestFetchResourceViews,
|
||||
requestFetchResourceFields,
|
||||
|
||||
// #withItems
|
||||
itemsViews,
|
||||
itemsCurrentPage,
|
||||
itemsTableQuery,
|
||||
|
||||
// #withItemsActions
|
||||
requestDeleteItem,
|
||||
requestFetchItems,
|
||||
addItemsTableQueries,
|
||||
changeItemsCurrentView
|
||||
}) {
|
||||
const [deleteItem, setDeleteItem] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [tableLoading, setTableLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Items List');
|
||||
}, [changePageTitle]);
|
||||
|
||||
const fetchHook = useQuery('items-resource', () => {
|
||||
return Promise.all([
|
||||
requestFetchResourceViews('items'),
|
||||
requestFetchResourceFields('items'),
|
||||
]);
|
||||
});
|
||||
|
||||
const fetchItems = useQuery(['items-table', itemsTableQuery],
|
||||
() => requestFetchItems({}));
|
||||
|
||||
// Handle click delete item.
|
||||
const handleDeleteItem = useCallback((item) => {
|
||||
setDeleteItem(item);
|
||||
}, [setDeleteItem]);
|
||||
|
||||
const handleEditItem = () => {};
|
||||
|
||||
// Handle cancel delete the item.
|
||||
const handleCancelDeleteItem = useCallback(() => {
|
||||
setDeleteItem(false);
|
||||
}, [setDeleteItem]);
|
||||
|
||||
// handle confirm delete item.
|
||||
const handleConfirmDeleteItem = useCallback(() => {
|
||||
requestDeleteItem(deleteItem.id).then(() => {
|
||||
AppToaster.show({ message: 'the_item_has_been_deleted' });
|
||||
setDeleteItem(false);
|
||||
});
|
||||
}, [requestDeleteItem, deleteItem]);
|
||||
|
||||
// Handle fetch data table.
|
||||
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
|
||||
addItemsTableQueries({
|
||||
...(sortBy.length > 0) ? {
|
||||
column_sort_order: sortBy[0].id,
|
||||
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||
} : {},
|
||||
});
|
||||
}, [fetchItems, addItemsTableQueries]);
|
||||
|
||||
// Handle filter change to re-fetch the items.
|
||||
const handleFilterChanged = useCallback((filterConditions) => {
|
||||
addItemsTableQueries({
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
}, [fetchItems]);
|
||||
|
||||
// Handle custom view change to re-fetch the items.
|
||||
const handleCustomViewChanged = useCallback((customViewId) => {
|
||||
setTableLoading(true);
|
||||
}, [fetchItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableLoading && !fetchItems.isFetching) {
|
||||
setTableLoading(false);
|
||||
}
|
||||
}, [tableLoading, fetchItems.isFetching]);
|
||||
|
||||
// Handle selected rows change.
|
||||
const handleSelectedRowsChange = useCallback((accounts) => {
|
||||
setSelectedRows(accounts);
|
||||
}, [setSelectedRows]);
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
isLoading={fetchHook.isFetching}
|
||||
name={'items-list'}>
|
||||
|
||||
<ItemsActionsBar
|
||||
onFilterChanged={handleFilterChanged}
|
||||
selectedRows={selectedRows}
|
||||
views={itemsViews} />
|
||||
|
||||
<DashboardPageContent>
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path={[
|
||||
'/dashboard/items/:custom_view_id/custom_view',
|
||||
'/dashboard/items'
|
||||
]}>
|
||||
<ItemsViewsTabs
|
||||
itemsViews={itemsViews}
|
||||
onViewChanged={handleCustomViewChanged} />
|
||||
|
||||
<ItemsDataTable
|
||||
loading={tableLoading}
|
||||
onDeleteItem={handleDeleteItem}
|
||||
onEditItem={handleEditItem}
|
||||
onFetchData={handleFetchData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange} />
|
||||
|
||||
<Alert
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Move to Trash"
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={deleteItem}
|
||||
onCancel={handleCancelDeleteItem}
|
||||
onConfirm={handleConfirmDeleteItem}>
|
||||
<p>
|
||||
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
|
||||
but it will become private to you.
|
||||
</p>
|
||||
</Alert>
|
||||
</Route>
|
||||
</Switch>
|
||||
</DashboardPageContent>
|
||||
</DashboardInsider>
|
||||
)
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withItems,
|
||||
withResourceActions,
|
||||
withDashboardActions,
|
||||
withItemsActions,
|
||||
withViewsActions,
|
||||
)(ItemsList);
|
||||
124
client/src/containers/Items/ItemsViewsTabs.js
Normal file
124
client/src/containers/Items/ItemsViewsTabs.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Alignment,
|
||||
Navbar,
|
||||
NavbarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Icon from 'components/Icon';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
import {useUpdateEffect} from 'hooks';
|
||||
|
||||
import withItemsActions from 'containers/Items/withItemsActions';
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
import withViewDetail from 'containers/Views/withViewDetails';
|
||||
|
||||
|
||||
function ItemsViewsTabs({
|
||||
// #withViewDetail
|
||||
viewId,
|
||||
viewItem,
|
||||
|
||||
itemsViews,
|
||||
|
||||
// #withItemsActions
|
||||
addItemsTableQueries,
|
||||
changeItemsCurrentView,
|
||||
|
||||
// #withDashboard
|
||||
setTopbarEditView,
|
||||
changePageSubtitle,
|
||||
|
||||
// #props
|
||||
onViewChanged,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { custom_view_id: customViewId } = useParams();
|
||||
|
||||
const handleClickNewView = () => {
|
||||
setTopbarEditView(null);
|
||||
history.push('/dashboard/custom_views/items/new');
|
||||
};
|
||||
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
changeItemsCurrentView(customViewId || -1);
|
||||
setTopbarEditView(customViewId);
|
||||
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
|
||||
|
||||
addItemsTableQueries({
|
||||
custom_view_id: customViewId || null,
|
||||
});
|
||||
|
||||
return () => {
|
||||
setTopbarEditView(null);
|
||||
changeItemsCurrentView(-1);
|
||||
changePageSubtitle('');
|
||||
};
|
||||
}, [customViewId]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
onViewChanged && onViewChanged(customViewId);
|
||||
}, [customViewId]);
|
||||
|
||||
const tabs = itemsViews.map(view => {
|
||||
const baseUrl = '/dashboard/items';
|
||||
const link = (
|
||||
<Link to={`${baseUrl}/${view.id}/custom_view`} onClick={handleViewLinkClick}>
|
||||
{view.name}
|
||||
</Link>
|
||||
);
|
||||
return (<Tab id={`custom_view_${view.id}`} title={link} />);
|
||||
});
|
||||
|
||||
return (
|
||||
<Navbar className='navbar--dashboard-views'>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<Tabs
|
||||
id='navbar'
|
||||
large={true}
|
||||
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
|
||||
className='tabs--dashboard-views'
|
||||
>
|
||||
<Tab
|
||||
id='all'
|
||||
title={<Link to={`/dashboard/items`}>All</Link>}
|
||||
onClick={handleViewLinkClick} />
|
||||
|
||||
{tabs}
|
||||
|
||||
<Button
|
||||
className='button--new-view'
|
||||
icon={<Icon icon='plus' />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tabs>
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
withDashboard,
|
||||
withItemsActions,
|
||||
withViewDetail,
|
||||
)(ItemsViewsTabs);
|
||||
11
client/src/containers/Items/withItemCategories.js
Normal file
11
client/src/containers/Items/withItemCategories.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
categoriesList: Object.values(state.itemCategories.categories),
|
||||
categoriesTableLoading: state.itemCategories.loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
16
client/src/containers/Items/withItemCategoriesActions.js
Normal file
16
client/src/containers/Items/withItemCategoriesActions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchItemCategories,
|
||||
submitItemCategory,
|
||||
deleteItemCategory,
|
||||
editItemCategory,
|
||||
} from 'store/itemCategories/itemsCategory.actions';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
requestSubmitItemCategory: (form) => dispatch(submitItemCategory({ form })),
|
||||
requestFetchItemCategories: () => dispatch(fetchItemCategories()),
|
||||
requestDeleteItemCategory: (id) => dispatch(deleteItemCategory(id)),
|
||||
requestEditItemCategory: (id, form) => dispatch(editItemCategory(id, form)),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
12
client/src/containers/Items/withItemCategoryDetail.js
Normal file
12
client/src/containers/Items/withItemCategoryDetail.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getCategoryId,
|
||||
} from 'store/itemCategories/itemsCategory.reducer';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
itemCategory: getCategoryId(state, props.itemCategoryId),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
26
client/src/containers/Items/withItems.js
Normal file
26
client/src/containers/Items/withItems.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
getResourceViews,
|
||||
getViewPages,
|
||||
} from 'store/customViews/customViews.selectors'
|
||||
import {
|
||||
getCurrentPageResults
|
||||
} from 'store/selectors';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
const viewPages = getViewPages(state.items.views, state.items.currentViewId);
|
||||
|
||||
return {
|
||||
itemsViews: getResourceViews(state, 'items'),
|
||||
itemsCurrentPage: getCurrentPageResults(
|
||||
state.items.items,
|
||||
viewPages,
|
||||
state.items.currentPage,
|
||||
),
|
||||
itemsBulkSelected: state.items.bulkActions,
|
||||
itemsTableLoading: state.items.loading,
|
||||
itemsTableQuery: state.items.tableQuery,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
32
client/src/containers/Items/withItemsActions.js
Normal file
32
client/src/containers/Items/withItemsActions.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchItems,
|
||||
deleteItem,
|
||||
submitItem,
|
||||
} from 'store/items/items.actions';
|
||||
import t from 'store/types';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
requestFetchItems: (query) => dispatch(fetchItems({ query })),
|
||||
requestDeleteItem: (id) => dispatch(deleteItem({ id })),
|
||||
requestSubmitItem: (form) => dispatch(submitItem({ form })),
|
||||
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, key, value,
|
||||
}),
|
||||
addItemsTableQueries: (queries) => dispatch({
|
||||
type: t.ITEMS_TABLE_QUERIES_ADD, queries,
|
||||
}),
|
||||
|
||||
changeItemsCurrentView: (id) => dispatch({
|
||||
type: t.ITEMS_SET_CURRENT_VIEW,
|
||||
currentViewId: parseInt(id, 10),
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
Reference in New Issue
Block a user