chrone: sperate client and server to different repos.

This commit is contained in:
a.bouhuolia
2021-09-21 17:13:53 +02:00
parent e011b2a82b
commit 18df5530c7
10015 changed files with 17686 additions and 97524 deletions

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import 'style/pages/Items/PageForm.scss';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import ItemFormPrimarySection from './ItemFormPrimarySection';
import ItemFormBody from './ItemFormBody';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import { useItemFormInitialValues } from './utils';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import { useItemFormContext } from './ItemFormProvider';
/**
* Item form.
*/
export default function ItemForm() {
// Item form context.
const {
itemId,
item,
accounts,
createItemMutate,
editItemMutate,
submitPayload,
isNewMode,
} = useItemFormContext();
// History context.
const history = useHistory();
// Initial values in create and edit mode.
const initialValues = useItemFormInitialValues(item);
// Transform API errors.
const transformApiErrors = (error) => {
const {
response: {
data: { errors },
},
} = error;
const fields = {};
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) {
fields.name = intl.get('the_name_used_before');
}
if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) {
AppToaster.show({
message: intl.get('cannot_change_item_inventory_account'),
intent: Intent.DANGER,
});
}
return fields;
};
// Handles the form submit.
const handleFormSubmit = (
values,
{ setSubmitting, resetForm, setErrors },
) => {
setSubmitting(true);
const form = { ...values };
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_item_has_been_created_successfully'
: 'the_item_has_been_edited_successfully',
{
number: itemId,
},
),
intent: Intent.SUCCESS,
});
resetForm();
setSubmitting(false);
// Submit payload.
if (submitPayload.redirect) {
history.push('/items');
}
};
// Handle response error.
const onError = (errors) => {
setSubmitting(false);
if (errors) {
const _errors = transformApiErrors(errors);
setErrors({ ..._errors });
}
};
if (isNewMode) {
createItemMutate(form).then(onSuccess).catch(onError);
} else {
editItemMutate([itemId, form]).then(onSuccess).catch(onError);
}
};
return (
<div class={classNames(CLASSES.PAGE_FORM_ITEM)}>
<Formik
enableReinitialize={true}
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions />
</Form>
</Formik>
</div>
);
}

View File

@@ -0,0 +1,77 @@
import * as Yup from 'yup';
import { defaultTo } from 'lodash';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
active: Yup.boolean(),
name: Yup.string()
.required()
.min(0)
.max(DATATYPES_LENGTH.STRING)
.label(intl.get('item_name_')),
type: Yup.string()
.trim()
.required()
.min(0)
.max(DATATYPES_LENGTH.STRING)
.label(intl.get('item_type_')),
code: Yup.string().trim().min(0).max(DATATYPES_LENGTH.STRING),
cost_price: Yup.number()
.min(0)
.max(DATATYPES_LENGTH.DECIMAL_13_3)
.when(['purchasable'], {
is: true,
then: Yup.number()
.required()
.label(intl.get('cost_price_')),
otherwise: Yup.number().nullable(true),
}),
sell_price: Yup.number()
.min(0)
.max(DATATYPES_LENGTH.DECIMAL_13_3)
.when(['sellable'], {
is: true,
then: Yup.number()
.required()
.label(intl.get('sell_price_')),
otherwise: Yup.number().nullable(true),
}),
cost_account_id: Yup.number()
.when(['purchasable'], {
is: true,
then: Yup.number().required(),
otherwise: Yup.number().nullable(true),
})
.label(intl.get('cost_account_id')),
sell_account_id: Yup.number()
.when(['sellable'], {
is: true,
then: Yup.number().required(),
otherwise: Yup.number().nullable(),
})
.label(intl.get('sell_account_id')),
inventory_account_id: Yup.number()
.when(['type'], {
is: (value) => value === 'inventory',
then: Yup.number().required(),
otherwise: Yup.number().nullable(),
})
.label(intl.get('inventory_account')),
category_id: Yup.number().positive().nullable(),
stock: Yup.string() || Yup.boolean(),
sellable: Yup.boolean().required(),
purchasable: Yup.boolean().required(),
});
export const transformItemFormData = (item, defaultValue) => {
return {
...item,
sellable: !!defaultTo(item?.sellable, defaultValue.sellable),
purchasable: !!defaultTo(item?.purchasable, defaultValue.purchasable),
active: !!defaultTo(item?.active, defaultValue.active),
};
};
export const CreateItemFormSchema = Schema;
export const EditItemFormSchema = Schema;

View File

@@ -0,0 +1,266 @@
import React from 'react';
import { useFormikContext, FastField, Field, ErrorMessage } from 'formik';
import {
FormGroup,
Classes,
TextArea,
Checkbox,
ControlGroup,
} from '@blueprintjs/core';
import {
AccountsSelectList,
MoneyInputGroup,
Col,
Row,
Hint,
InputPrependText,
} from 'components';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { useItemFormContext } from './ItemFormProvider';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { ACCOUNT_PARENT_TYPE } from 'common/accountTypes';
import { compose, inputIntent } from 'utils';
import {
sellDescriptionFieldShouldUpdate,
sellAccountFieldShouldUpdate,
sellPriceFieldShouldUpdate,
costPriceFieldShouldUpdate,
costAccountFieldShouldUpdate,
purchaseDescFieldShouldUpdate,
} from './utils';
/**
* Item form body.
*/
function ItemFormBody({ organization: { base_currency } }) {
const { accounts } = useItemFormContext();
const { values } = useFormikContext();
return (
<div class="page-form__section page-form__section--selling-cost">
<Row>
<Col xs={6}>
{/*------------- Purchasable checbox ------------- */}
<FastField name={'sellable'} type="checkbox">
{({ form, field }) => (
<FormGroup inline={true} className={'form-group--sellable'}>
<Checkbox
inline={true}
label={
<h3>
<T id={'i_sell_this_item'} />
</h3>
}
name={'sellable'}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------- Selling price ------------- */}
<FastField
name={'sell_price'}
sellable={values.sellable}
shouldUpdate={sellPriceFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'selling_price'} />}
className={'form-group--sell_price'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'sell_price'} />}
inline={true}
>
<ControlGroup>
<InputPrependText text={base_currency} />
<MoneyInputGroup
value={value}
inputGroupProps={{ fill: true }}
disabled={!form.values.sellable}
onChange={(unformattedValue) => {
form.setFieldValue('sell_price', unformattedValue);
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------- Selling account ------------- */}
<FastField
name={'sell_account_id'}
sellable={values.sellable}
accounts={accounts}
shouldUpdate={sellAccountFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account'} />}
labelInfo={
<Hint content={<T id={'item.field.sell_account.hint'} />} />
}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="sell_account_id" />}
className={classNames(
'form-group--sell-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('sell_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
disabled={!form.values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<FastField
name={'sell_description'}
sellable={values.sellable}
shouldUpdate={sellDescriptionFieldShouldUpdate}
>
{({ form: { values }, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--sell-description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...field}
disabled={!values.sellable}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col xs={6}>
{/*------------- Sellable checkbox ------------- */}
<FastField name={'purchasable'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true} className={'form-group--purchasable'}>
<Checkbox
inline={true}
label={
<h3>
<T id={'i_purchase_this_item'} />
</h3>
}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------- Cost price ------------- */}
<FastField
name={'cost_price'}
purchasable={values.purchasable}
shouldUpdate={costPriceFieldShouldUpdate}
>
{({ field, form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'cost_price'} />}
className={'form-group--item-cost-price'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="cost_price" />}
inline={true}
>
<ControlGroup>
<InputPrependText text={base_currency} />
<MoneyInputGroup
value={value}
inputGroupProps={{ medium: true }}
disabled={!form.values.purchasable}
onChange={(unformattedValue) => {
form.setFieldValue('cost_price', unformattedValue);
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------- Cost account ------------- */}
<FastField
name={'cost_account_id'}
purchasable={values.purchasable}
accounts={accounts}
shouldUpdate={costAccountFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account'} />}
labelInfo={
<Hint content={<T id={'item.field.cost_account.hint'} />} />
}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="cost_account_id" />}
className={classNames(
'form-group--cost-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('cost_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
disabled={!form.values.purchasable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<FastField
name={'purchase_description'}
purchasable={values.purchasable}
shouldUpdate={purchaseDescFieldShouldUpdate}
>
{({ form: { values }, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--purchase-description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...field}
disabled={!values.purchasable}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</div>
);
}
export default compose(withCurrentOrganization())(ItemFormBody);

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { FastField, useFormikContext } from 'formik';
import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider';
/**
* Item form floating actions.
*/
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) => {
history.goBack();
};
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
setSubmitPayload({ redirect: true });
};
// Handle submit & new button click.
const handleSubmitAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false });
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
loading={isSubmitting}
onClick={handleSubmitBtnClick}
type="submit"
className={'btn--submit'}
>
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />}
</Button>
<Button
className={classNames('ml1', 'btn--submit-new')}
disabled={isSubmitting}
onClick={handleSubmitAndNewBtnClick}
type="submit"
>
<T id={'save_new'} />
</Button>
<Button
disabled={isSubmitting}
className={'ml1'}
onClick={handleCancelBtnClick}
>
<T id={'close'} />
</Button>
{/*----------- Active ----------*/}
<FastField name={'active'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true} className={'form-group--active'}>
<Checkbox
inline={true}
label={<T id={'active'} />}
name={'active'}
{...field}
/>
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { FastField, ErrorMessage } from 'formik';
import { FormGroup } from '@blueprintjs/core';
import { AccountsSelectList, Col, Row } from 'components';
import { CLASSES } from 'common/classes';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { accountsFieldShouldUpdate } from './utils';
import { compose, inputIntent } from 'utils';
import { ACCOUNT_TYPE } from 'common/accountTypes';
import { useItemFormContext } from './ItemFormProvider';
/**
* Item form inventory sections.
*/
function ItemFormInventorySection({ organization: { base_currency } }) {
const { accounts } = useItemFormContext();
return (
<div class="page-form__section page-form__section--inventory">
<h3>
<T id={'inventory_information'} />
</h3>
<Row>
<Col xs={6}>
{/*------------- Inventory account ------------- */}
<FastField
name={'inventory_account_id'}
accounts={accounts}
shouldUpdate={accountsFieldShouldUpdate}
>
{({ form, field: { value }, meta: { touched, error } }) => (
<FormGroup
label={<T id={'inventory_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="inventory_account_id" />}
className={classNames(
'form-group--item-inventory_account',
'form-group--select-list',
CLASSES.FILL,
)}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('inventory_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={[ACCOUNT_TYPE.INVENTORY]}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</div>
);
}
export default compose(withCurrentOrganization())(ItemFormInventorySection);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { ItemFormProvider } from './ItemFormProvider';
import DashboardCard from 'components/Dashboard/DashboardCard';
import ItemForm from 'containers/Items/ItemForm';
/**
* Item form page.
*/
export default function ItemFormPage() {
const { id } = useParams();
const idInteger = parseInt(id, 10);
return (
<ItemFormProvider itemId={idInteger}>
<DashboardCard page>
<ItemForm />
</DashboardCard>
</ItemFormProvider>
);
}

View File

@@ -0,0 +1,172 @@
import React, { useEffect, useRef } from 'react';
import {
FormGroup,
InputGroup,
RadioGroup,
Classes,
Radio,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
import { ErrorMessage, FastField } from 'formik';
import {
CategoriesSelectList,
Hint,
Col,
Row,
FieldRequiredHint,
} from 'components';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider';
import { handleStringChange, inputIntent } from 'utils';
import { categoriesFieldShouldUpdate } from './utils';
/**
* Item form primary section.
*/
export default function ItemFormPrimarySection() {
// Item form context.
const { isNewMode, item, itemsCategories } = useItemFormContext();
const nameFieldRef = useRef(null);
useEffect(() => {
// Auto focus item name field once component mount.
if (nameFieldRef.current) {
nameFieldRef.current.focus();
}
}, []);
const itemTypeHintContent = (
<>
<div class="mb1">
<FormattedHTMLMessage id={'services_that_you_provide_to_customers'} />
</div>
<div class="mb1">
<FormattedHTMLMessage id={'products_you_buy_and_or_sell'} />
</div>
<div class="mb1">
<FormattedHTMLMessage
id={'products_you_buy_and_or_sell_but_don_t_need'}
/>
</div>
</>
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/*----------- Item type ----------*/}
<FastField name={'type'}>
{({ form, field: { value }, meta: { touched, error } }) => (
<FormGroup
medium={true}
label={<T id={'item_type'} />}
labelInfo={
<span>
<FieldRequiredHint />
<Hint
content={itemTypeHintContent}
position={Position.BOTTOM_LEFT}
/>
</span>
}
className={'form-group--item-type'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="item_type" />}
inline={true}
>
<RadioGroup
inline={true}
onChange={handleStringChange((_value) => {
form.setFieldValue('type', _value);
})}
selectedValue={value}
disabled={!isNewMode && item.type === 'inventory'}
>
<Radio label={<T id={'service'} />} value="service" />
<Radio label={<T id={'non_inventory'} />} value="non-inventory" />
<Radio label={<T id={'inventory'} />} value="inventory" />
</RadioGroup>
</FormGroup>
)}
</FastField>
<Row>
<Col xs={7}>
{/*----------- Item name ----------*/}
<FastField name={'name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'item_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--item-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'name'} />}
inline={true}
>
<InputGroup
medium={true}
{...field}
intent={inputIntent({ error, touched })}
inputRef={(ref) => (nameFieldRef.current = ref)}
/>
</FormGroup>
)}
</FastField>
{/*----------- SKU ----------*/}
<FastField name={'code'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'item_code'} />}
className={'form-group--item_code'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'code'} />}
inline={true}
>
<InputGroup medium={true} intent={inputIntent({ error, touched })} {...field} />
</FormGroup>
)}
</FastField>
{/*----------- Item category ----------*/}
<FastField
name={'category_id'}
categories={itemsCategories}
shouldUpdate={categoriesFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'category'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="category_id" />}
className={classNames('form-group--category', Classes.FILL)}
>
<CategoriesSelectList
categories={itemsCategories}
selecetedCategoryId={value}
onCategorySelected={(category) => {
form.setFieldValue('category_id', category.id);
}}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col xs={3}>
{/* <Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
className={'mt2'}
/> */}
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,101 @@
import React, { useEffect, createContext, useState } from 'react';
import intl from 'react-intl-universal';
import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useItem,
useSettingsItems,
useItemsCategories,
useCreateItem,
useEditItem,
useAccounts,
} from 'hooks/query';
import { useDashboardPageTitle } from 'hooks/state';
const ItemFormContext = createContext();
/**
* Accounts chart data provider.
*/
function ItemFormProvider({ itemId, ...props }) {
const { state } = useLocation();
const duplicateId = state?.action;
// Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
// Fetches the items categories list.
const {
isLoading: isItemsCategoriesLoading,
data: { itemsCategories },
} = useItemsCategories();
// Fetches the given item details.
const { isLoading: isItemLoading, data: item } = useItem(
itemId || duplicateId,
{
enabled: !!itemId || !!duplicateId,
},
);
// Fetches item settings.
const {
isLoading: isItemsSettingsLoading,
isFetching: isItemsSettingsFetching,
} = useSettingsItems();
// 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 = duplicateId || !itemId;
// Provider state.
const provider = {
itemId,
accounts,
item,
itemsCategories,
submitPayload,
isNewMode,
isAccountsLoading,
isItemsCategoriesLoading,
isItemLoading,
createItemMutate,
editItemMutate,
setSubmitPayload,
};
// Change page title dispatcher.
const changePageTitle = useDashboardPageTitle();
// Changes the page title in new and edit mode.
useEffect(() => {
isNewMode
? changePageTitle(intl.get('new_item'))
: changePageTitle(intl.get('edit_item_details'));
}, [changePageTitle, isNewMode]);
const loading =
isItemsSettingsLoading ||
isAccountsLoading ||
isItemsCategoriesLoading ||
isItemLoading;
return (
<DashboardInsider loading={loading} name={'item-form'}>
<ItemFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useItemFormContext = () => React.useContext(ItemFormContext);
export { ItemFormProvider, useItemFormContext };

View File

@@ -0,0 +1,160 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Switch,
Alignment,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import Icon from 'components/Icon';
import {
If,
DashboardActionViewsList,
AdvancedFilterPopover,
DashboardFilterButton,
} from 'components';
import { useItemsListContext } from './ItemsListProvider';
import { useRefreshItems } from 'hooks/query/items';
import withItems from 'containers/Items/withItems';
import withItemsActions from './withItemsActions';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Items actions bar.
*/
function ItemsActionsBar({
// #withItems
itemsSelectedRows,
itemsFilterRoles,
// #withItemActions
setItemsTableState,
itemsInactiveMode,
// #withAlertActions
openAlert,
}) {
// Items list context.
const { itemsViews, fields } = useItemsListContext();
// Items refresh action.
const { refresh } = useRefreshItems();
// History context.
const history = useHistory();
// Handle `new item` button click.
const onClickNewItem = () => {
history.push('/items/new');
};
// Handle tab changing.
const handleTabChange = (view) => {
setItemsTableState({ viewSlug: view ? view.slug : null });
};
// Handle cancel/confirm items bulk.
const handleBulkDelete = () => {
openAlert('items-bulk-delete', { itemsIds: itemsSelectedRows });
};
// Handle inactive switch changing.
const handleInactiveSwitchChange = (event) => {
const checked = event.target.checked;
setItemsTableState({ inactiveMode: checked });
};
const handleRefreshBtnClick = () => {
refresh();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'items'}
allMenuItem={true}
allMenuItemText={<T id={'all_items'} />}
views={itemsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_item'} />}
onClick={onClickNewItem}
/>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: itemsFilterRoles,
defaultFieldKey: 'name',
fields: fields,
onFilterChange: (filterConditions) => {
setItemsTableState({ filterRoles: filterConditions });
},
}}
>
<DashboardFilterButton conditionsCount={itemsFilterRoles.length} />
</AdvancedFilterPopover>
<NavbarDivider />
<If condition={itemsSelectedRows.length}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</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'} />}
/>
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={itemsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withItems(({ itemsSelectedRows, itemsTableState }) => ({
itemsSelectedRows,
itemsInactiveMode: itemsTableState.inactiveMode,
itemsFilterRoles: itemsTableState.filterRoles,
})),
withItemsActions,
withAlertActions,
)(ItemsActionsBar);

View File

@@ -0,0 +1,19 @@
import React from 'react';
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';
/**
* Items alert.
*/
export default function ItemsAlerts() {
return (
<div>
<ItemDeleteAlert name={'item-delete'} />
<ItemInactivateAlert name={'item-inactivate'} />
<ItemActivateAlert name={'item-activate'} />
<ItemBulkDeleteAlert name={'items-bulk-delete'} />
</div>
);
}

View File

@@ -0,0 +1,171 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import { DashboardContentTable, DataTable } from 'components';
import ItemsEmptyStatus from './ItemsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { TABLES } from 'common/tables';
import withItems from 'containers/Items/withItems';
import withItemsActions from 'containers/Items/withItemsActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { useItemsListContext } from './ItemsListProvider';
import { useItemsTableColumns, ItemsActionMenuList } from './components';
import { useMemorizedColumnsWidths } from 'hooks';
import { compose } from 'utils';
/**
* Items datatable.
*/
function ItemsDataTable({
// #withItemsActions
setItemsTableState,
// #withDialogAction
openDialog,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withItems
itemsTableState,
// #ownProps
tableProps,
}) {
// Items list context.
const { items, pagination, isItemsLoading, isEmptyStatus, isItemsFetching } =
useItemsListContext();
// Datatable columns.
const columns = useItemsTableColumns();
// History context.
const history = useHistory();
// Table row class names.
const rowClassNames = (row) => ({
inactive: !row.original.active,
});
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.ITEMS);
// Handle fetch data once the page index, size or sort by of the table change.
const handleFetchData = React.useCallback(
({ pageSize, pageIndex, sortBy }) => {
setItemsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setItemsTableState],
);
// Handle delete action Item.
const handleDeleteItem = ({ id }) => {
openAlert('item-delete', { itemId: id });
};
// Handle cancel/confirm item inactive.
const handleInactiveItem = ({ id }) => {
openAlert('item-inactivate', { itemId: id });
};
// Handle cancel/confirm item activate.
const handleActivateItem = ({ id }) => {
openAlert('item-activate', { itemId: id });
};
// Handle Edit item.
const handleEditItem = ({ id }) => {
history.push(`/items/${id}/edit`);
};
// Handle item make adjustment.
const handleMakeAdjustment = ({ id }) => {
openDialog('inventory-adjustment', { itemId: id });
};
// Display empty status instead of the table.
const handleDuplicate = ({ id }) => {
history.push(`/items/new?duplicate=${id}`, { action: id });
};
// Handle view detail item.
const handleViewDetailItem = ({ id }) => {
openDrawer('item-detail-drawer', { itemId: id });
};
// Cannot continue in case the items has empty status.
if (isEmptyStatus) {
return <ItemsEmptyStatus />;
}
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer('item-detail-drawer', { itemId: cell.row.original.id });
};
return (
<DashboardContentTable>
<DataTable
columns={columns}
data={items}
loading={isItemsLoading}
headerLoading={isItemsLoading}
progressBarLoading={isItemsFetching}
noInitialFetch={true}
selectionColumn={true}
spinnerProps={{ size: 30 }}
expandable={false}
sticky={true}
rowClassNames={rowClassNames}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={true}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ItemsActionMenuList}
onFetchData={handleFetchData}
onCellClick={handleCellClick}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
payload={{
onDeleteItem: handleDeleteItem,
onEditItem: handleEditItem,
onInactivateItem: handleInactiveItem,
onActivateItem: handleActivateItem,
onMakeAdjustment: handleMakeAdjustment,
onDuplicate: handleDuplicate,
onViewDetails: handleViewDetailItem,
}}
noResults={<T id={'there_is_no_items_in_the_table_yet'} />}
{...tableProps}
/>
</DashboardContentTable>
);
}
export default compose(
withItemsActions,
withAlertsActions,
withDrawerActions,
withDialogActions,
withItems(({ itemsTableState }) => ({ itemsTableState })),
)(ItemsDataTable);

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
import { FormattedMessage as T } from 'components';
export default function ItemsEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={<T id={'manage_the_organization_s_services_and_products'} />}
description={
<p>
<T id={'here_a_list_of_your_organization_products_and_services'} />
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/items/new');
}}
>
<T id={'new_item'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
export default function ItemFloatingFooter({
formik: { isSubmitting },
onSubmitClick,
onCancelClick,
itemDetail,
}) {
return (
<div class="form__floating-footer">
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
type="submit"
onClick={() => {
onSubmitClick({ publish: true, redirect: true });
}}
>
{itemDetail && itemDetail.id ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
onSubmitClick({ publish: true, redirect: false });
}}
>
<T id={'save_new'} />
</Button>
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={() => {
onSubmitClick({ publish: false, redirect: false });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'ml1'}
onClick={() => {
onCancelClick && onCancelClick();
}}
>
<T id={'close'} />
</Button>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { compose } from 'utils';
import 'style/pages/Items/List.scss';
import { DashboardPageContent } from 'components';
import ItemsActionsBar from './ItemsActionsBar';
import ItemsAlerts from './ItemsAlerts';
import ItemsViewsTabs from './ItemsViewsTabs';
import ItemsDataTable from './ItemsDataTable';
import { ItemsListProvider } from './ItemsListProvider';
import withItems from './withItems';
import withItemsActions from './withItemsActions';
/**
* Items list.
*/
function ItemsList({
// #withItems
itemsTableState,
itemsTableStateChanged,
// #withItemsActions
resetItemsTableState,
}) {
// Resets items table query state once the page unmount.
React.useEffect(
() => () => {
resetItemsTableState();
},
[resetItemsTableState],
);
return (
<ItemsListProvider
tableState={itemsTableState}
tableStateChanged={itemsTableStateChanged}
>
<ItemsActionsBar />
<DashboardPageContent>
<ItemsViewsTabs />
<ItemsDataTable />
</DashboardPageContent>
<ItemsAlerts />
</ItemsListProvider>
);
}
export default compose(
withItemsActions,
withItems(({ itemsTableState, itemsTableStateChanged }) => ({
itemsTableState,
itemsTableStateChanged,
})),
)(ItemsList);

View File

@@ -0,0 +1,76 @@
import React, { createContext } from 'react';
import { isEmpty } from 'lodash';
import {
getFieldsFromResourceMeta,
transformTableQueryToParams,
isTableEmptyStatus,
} from 'utils';
import { transformItemsTableState } from './utils';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceMeta, useItems } from 'hooks/query';
const ItemsContext = createContext();
/**
* Items list provider.
*/
function ItemsListProvider({ tableState, tableStateChanged, ...props }) {
const tableQuery = transformItemsTableState(tableState);
// Fetch accounts resource views and fields.
const { data: itemsViews, isLoading: isViewsLoading } =
useResourceViews('items');
// Fetch the accounts resource fields.
const {
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('items');
// Handle fetching the items table based on the given query.
const {
data: { items, pagination, filterMeta },
isFetching: isItemsFetching,
isLoading: isItemsLoading,
} = useItems(
{
...transformTableQueryToParams(tableQuery),
},
{ keepPreviousData: true },
);
// Detarmines the datatable empty status.
const isEmptyStatus =
!tableStateChanged && !isItemsLoading && isEmpty(items);
const state = {
itemsViews,
items,
pagination,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isViewsLoading,
isItemsLoading,
isItemsFetching: isItemsFetching,
isResourceLoading,
isResourceFetching,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isItemsLoading || isResourceLoading}
name={'items-list'}
>
<ItemsContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useItemsListContext = () => React.useContext(ItemsContext);
export { ItemsListProvider, useItemsListContext };

View File

@@ -0,0 +1,49 @@
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
/**
* Item univrsal search item select action.
*/
function ItemUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
onAction,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.ITEM) {
openDrawer('item-detail-drawer', { itemId: resourceId });
onAction && onAction();
}
return null;
}
export const ItemUniversalSearchSelectAction = withDrawerActions(
ItemUniversalSearchSelectComponent,
);
/**
* Transformes items to search.
* @param {*} item
* @returns
*/
const transfromItemsToSearch = (item) => ({
id: item.id,
text: item.name,
subText: item.code,
label: item.type,
reference: item,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchItemBind = () => ({
resourceType: RESOURCES_TYPES.ITEM,
optionItemLabel: intl.get('items'),
selectItemAction: ItemUniversalSearchSelectAction,
itemSelect: transfromItemsToSearch,
});

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { DashboardViewsTabs } from 'components';
import { withRouter } from 'react-router-dom';
import withItemsActions from 'containers/Items/withItemsActions';
import withItems from 'containers/Items/withItems';
import { useItemsListContext } from './ItemsListProvider';
import { compose, transfromViewsToTabs } from 'utils';
/**
* Items views tabs.
*/
function ItemsViewsTabs({
// #withItemsActions
setItemsTableState,
// #withItems
itemsCurrentView,
}) {
const { itemsViews } = useItemsListContext();
// Mapped items views.
const tabs = transfromViewsToTabs(itemsViews);
// Handles the active tab change.
const handleTabChange = (viewSlug) => {
setItemsTableState({ viewSlug });
};
return (
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
currentViewSlug={itemsCurrentView}
resourceName={'items'}
tabs={tabs}
onChange={handleTabChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withRouter,
withItems(({ itemsTableState }) => ({
itemsCurrentView: itemsTableState?.viewSlug,
})),
withItemsActions,
)(ItemsViewsTabs);

View File

@@ -0,0 +1,214 @@
import React from 'react';
import {
Menu,
MenuDivider,
MenuItem,
Intent,
Tag,
Position,
Button,
Popover,
} from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { isNumber } from 'lodash';
import { FormattedMessage as T, 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}>
{intl.get(row.type)}
</Tag>
) : (
''
);
};
export const ItemCodeAccessor = (row) =>
row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{intl.get(row.type)}
</Tag>
) : (
''
);
export const QuantityOnHandCell = ({ cell: { value } }) => {
return isNumber(value) ? (
<span className={value < 0 ? 'quantity_on_hand' : null}>{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}>
{intl.get(row.type)}
</Tag>
) : null;
};
export function ItemsActionMenuList({
row: { original },
payload: {
onEditItem,
onInactivateItem,
onActivateItem,
onMakeAdjustment,
onDeleteItem,
onDuplicate,
onViewDetails,
},
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={<T id={'view_details'} />}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_item')}
onClick={safeCallback(onEditItem, original)}
/>
<MenuItem
icon={<Icon icon="duplicate-16" />}
text={intl.get('duplicate')}
onClick={safeCallback(onDuplicate, original)}
/>
<If condition={original.active}>
<MenuItem
text={intl.get('inactivate_item')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivateItem, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={intl.get('activate_item')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivateItem, original)}
/>
</If>
<If condition={original.type === 'inventory'}>
<MenuItem
text={intl.get('make_adjustment')}
icon={<Icon icon={'swap-vert'} iconSize={16} />}
onClick={safeCallback(onMakeAdjustment, original)}
/>
</If>
<MenuItem
text={intl.get('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>
);
};
/**
* Retrieve all items table columns.
*/
export const useItemsTableColumns = () => {
return React.useMemo(
() => [
{
id: 'name',
Header: intl.get('item_name'),
accessor: 'name',
className: 'name',
width: 180,
clickable: true,
textOverview: true,
},
{
id: 'code',
Header: intl.get('item_code'),
accessor: 'code',
className: 'code',
width: 120,
clickable: true,
},
{
id: 'type',
Header: intl.get('item_type'),
accessor: ItemTypeAccessor,
className: 'item_type',
width: 120,
clickable: true,
},
{
id: 'category',
Header: intl.get('category'),
accessor: 'category.name',
className: 'category',
width: 150,
clickable: true,
textOverview: true,
},
{
id: 'sell_price',
Header: intl.get('sell_price'),
accessor: 'sell_price_formatted',
align: 'right',
width: 150,
clickable: true,
},
{
id: 'cost_price',
Header: intl.get('cost_price'),
accessor: 'cost_price_formatted',
align: 'right',
width: 150,
clickable: true,
},
{
id: 'quantity_on_hand',
Header: intl.get('quantity_on_hand'),
accessor: 'quantity_on_hand',
Cell: QuantityOnHandCell,
align: 'right',
width: 140,
clickable: true,
},
],
[],
);
};

View File

@@ -0,0 +1,184 @@
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { defaultTo } from 'lodash';
import { AppToaster } from 'components';
import {
transformTableStateToQuery,
defaultFastFieldShouldUpdate,
} from 'utils';
import { transformToForm } from 'utils';
import { useSettingsSelector } from '../../hooks/state';
import { transformItemFormData } from './ItemForm.schema';
const defaultInitialValues = {
active: 1,
name: '',
type: 'service',
code: '',
cost_price: '',
sell_price: '',
cost_account_id: '',
sell_account_id: '',
inventory_account_id: '',
category_id: '',
sellable: 1,
purchasable: true,
sell_description: '',
purchase_description: '',
};
/**
* Initial values in create and edit mode.
*/
export const useItemFormInitialValues = (item) => {
const { items: itemsSettings } = useSettingsSelector();
return useMemo(
() => ({
...defaultInitialValues,
cost_account_id: defaultTo(itemsSettings?.preferredCostAccount, ''),
sell_account_id: defaultTo(itemsSettings?.preferredSellAccount, ''),
inventory_account_id: defaultTo(
itemsSettings?.preferredInventoryAccount,
'',
),
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(
transformItemFormData(item, defaultInitialValues),
defaultInitialValues,
),
}),
[item, itemsSettings],
);
};
export const transitionItemTypeKeyToLabel = (itemTypeKey) => {
const table = {
service: intl.get('service'),
inventory: intl.get('inventory'),
'non-inventory': intl.get('non_inventory'),
};
return typeof table[itemTypeKey] === 'string' ? table[itemTypeKey] : '';
};
// handle delete errors.
export const handleDeleteErrors = (errors) => {
if (
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS')
) {
AppToaster.show({
message: intl.get('the_item_has_associated_transactions'),
intent: Intent.DANGER,
});
}
if (
errors.find(
(error) => error.type === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
)
) {
AppToaster.show({
message: intl.get(
'you_could_not_delete_item_that_has_associated_inventory_adjustments_transacions',
),
intent: Intent.DANGER,
});
}
if (
errors.find(
(error) => error.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
)
) {
AppToaster.show({
message: intl.get(
'cannot_change_item_type_to_inventory_with_item_has_associated_transactions',
),
intent: Intent.DANGER,
});
}
};
/**
* Detarmines accounts fast field should update.
*/
export const accountsFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Detarmines categories fast field should update.
*/
export const categoriesFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.categories !== oldProps.categories ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Sell price fast field should update.
*/
export const sellPriceFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.sellable !== oldProps.sellable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Sell account fast field should update.
*/
export const sellAccountFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
newProps.sellable !== oldProps.sellable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Sell description fast field should update.
*/
export const sellDescriptionFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.sellable !== oldProps.sellable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export const costAccountFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
newProps.purchasable !== oldProps.purchasable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export const costPriceFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.purchasable !== oldProps.purchasable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export const purchaseDescFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.purchasable !== oldProps.purchasable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export function transformItemsTableState(tableState) {
return {
...transformTableStateToQuery(tableState),
inactive_mode: tableState.inactiveMode,
};
}

View File

@@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { getItemById } from 'store/items/items.reducer';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
item: getItemById(state, props.itemId),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,21 @@
import {connect} from 'react-redux';
import {
getItemsTableStateFactory,
isItemsTableStateChangedFactory,
} from 'store/items/items.selectors';
export default (mapState) => {
const getItemsTableState = getItemsTableStateFactory();
const isItemsTableStateChanged = isItemsTableStateChangedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
itemsSelectedRows: state.items.selectedRows,
itemsTableState: getItemsTableState(state, props),
itemsTableStateChanged: isItemsTableStateChanged(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import {
setItemsTableState,
resetItemsTableState,
} from 'store/items/items.actions';
export const mapDispatchToProps = (dispatch) => ({
setItemsTableState: (queries) => dispatch(setItemsTableState(queries)),
resetItemsTableState: () => dispatch(resetItemsTableState()),
});
export default connect(null, mapDispatchToProps);