1
0

feat(webapp): redesign item form to match customer/vendor form layout

Redesign the item form UX to align with the customer and vendor form patterns:
- Add vertical sticky tabs (Basic, Selling, Purchasing, Inventory) with smooth scroll
- Split monolithic sections into discrete components (Basic, Selling, Purchasing, Inventory)
- Move Active checkbox from floating actions into Basic section
- Update floating actions to sticky bottom bar with Save + Save & New dropdown
- Remove Cancel button from floating actions
- Wrap form in centered max-width container (800px)
- Clean up legacy item form SCSS

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ahmed Bouhuolia
2026-04-17 11:34:08 +02:00
parent 52c97f1401
commit 42bc0bed27
12 changed files with 564 additions and 533 deletions
@@ -1,14 +1,14 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import ItemFormFormik from './ItemFormFormik'; import ItemFormFormik from './ItemFormFormik';
import { useDashboardPageTitle } from '@/hooks/state'; import { useDashboardPageTitle } from '@/hooks/state';
import { useItemFormContext, ItemFormProvider } from './ItemFormProvider'; import { useItemFormContext, ItemFormProvider } from './ItemFormProvider';
import { DashboardInsider, DashboardCard } from '@/components'; import { DashboardInsider } from '@/components';
import { Box } from '@/components';
/** /**
* Item form dashboard title. * Item form dashboard title.
@@ -39,9 +39,9 @@ function ItemFormPageLoading({ children }) {
const { isFormLoading } = useItemFormContext(); const { isFormLoading } = useItemFormContext();
return ( return (
<DashboardItemFormPageInsider loading={isFormLoading} name={'item-form'}> <DashboardInsider loading={isFormLoading}>
{children} {children}
</DashboardItemFormPageInsider> </DashboardInsider>
); );
} }
@@ -59,36 +59,18 @@ export default function ItemForm({ itemId }) {
history.push('/items'); history.push('/items');
} }
}; };
// Handle cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return ( return (
<ItemFormProvider itemId={itemId}> <ItemFormProvider itemId={itemId}>
<ItemFormDashboardTitle /> <ItemFormDashboardTitle />
<ItemFormPageLoading> <ItemFormPageLoading>
<DashboardCard page> <Box mx={'auto'} maxWidth={800}>
<ItemFormPageFormik <ItemFormFormik
onSubmitSuccess={handleSubmitSuccess} onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/> />
</DashboardCard> </Box>
</ItemFormPageLoading> </ItemFormPageLoading>
</ItemFormProvider> </ItemFormProvider>
); );
} }
const DashboardItemFormPageInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
const ItemFormPageFormik = styled(ItemFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
@@ -3,41 +3,31 @@ import React, { useEffect, useRef } from 'react';
import { import {
FormGroup, FormGroup,
RadioGroup, RadioGroup,
Classes,
Radio, Radio,
Position, Position,
MenuItem, Checkbox,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { ErrorMessage, FastField } from 'formik'; import { ErrorMessage, FastField } from 'formik';
import { CLASSES } from '@/constants/classes';
import { import {
Hint, Hint,
Col,
Row,
FieldRequiredHint, FieldRequiredHint,
FormattedMessage as T, FormattedMessage as T,
FormattedHTMLMessage, FormattedHTMLMessage,
FFormGroup, FFormGroup,
FSelect, FSelect,
FInputGroup, FInputGroup,
Box,
} from '@/components'; } from '@/components';
import classNames from 'classnames';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
import { handleStringChange, inputIntent } from '@/utils'; import { handleStringChange, inputIntent } from '@/utils';
// import { categoriesFieldShouldUpdate } from './utils'; import { ItemFormSectionTitle } from './ItemFormSectionTitle';
/** export function ItemFormBasicSection() {
* Item form primary section.
*/
export default function ItemFormPrimarySection() {
// Item form context.
const { isNewMode, item, itemsCategories } = useItemFormContext(); const { isNewMode, item, itemsCategories } = useItemFormContext();
const nameFieldRef = useRef(null); const nameFieldRef = useRef(null);
useEffect(() => { useEffect(() => {
// Auto focus item name field once component mount.
if (nameFieldRef.current) { if (nameFieldRef.current) {
nameFieldRef.current.focus(); nameFieldRef.current.focus();
} }
@@ -45,17 +35,19 @@ export default function ItemFormPrimarySection() {
const itemTypeHintContent = ( const itemTypeHintContent = (
<> <>
<div class="mb1"> <div className="mb1">
<FormattedHTMLMessage id={'services_that_you_provide_to_customers'} /> <FormattedHTMLMessage id={'services_that_you_provide_to_customers'} />
</div> </div>
<div class="mb1"> <div className="mb1">
<FormattedHTMLMessage id={'products_you_buy_and_or_sell'} /> <FormattedHTMLMessage id={'products_you_buy_and_or_sell'} />
</div> </div>
</> </>
); );
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}> <Box data-section-id="primary">
<ItemFormSectionTitle>Basic details</ItemFormSectionTitle>
{/*----------- Item type ----------*/} {/*----------- Item type ----------*/}
<FastField name={'type'}> <FastField name={'type'}>
{({ form, field: { value }, meta: { touched, error } }) => ( {({ form, field: { value }, meta: { touched, error } }) => (
@@ -91,61 +83,66 @@ export default function ItemFormPrimarySection() {
)} )}
</FastField> </FastField>
<Row> {/*----------- Item name ----------*/}
<Col xs={7}> <FFormGroup
{/*----------- Item name ----------*/} name={'name'}
<FFormGroup label={<T id={'item_name'} />}
name={'name'} labelInfo={<FieldRequiredHint />}
label={<T id={'item_name'} />} inline={true}
labelInfo={<FieldRequiredHint />} fill
inline={true} fastField
fastField >
> <FInputGroup
<FInputGroup name={'name'}
name={'name'} medium={true}
medium={true} inputRef={(ref) => (nameFieldRef.current = ref)}
inputRef={(ref) => (nameFieldRef.current = ref)} fastField
fastField fill
/>
</FFormGroup>
{/*----------- SKU ----------*/}
<FFormGroup
name={'code'}
label={<T id={'item_code'} />}
inline={true}
fill
fastField
>
<FInputGroup name={'code'} medium={true} fastField fill />
</FFormGroup>
{/*----------- Item category ----------*/}
<FFormGroup
name={'category_id'}
label={<T id={'category'} />}
inline={true}
fill
>
<FSelect
name={'category_id'}
items={itemsCategories}
valueAccessor={'id'}
textAccessor={'name'}
placeholder={<T id={'select_category'} />}
popoverProps={{ minimal: true, captureDismiss: true }}
fill
/>
</FFormGroup>
{/*----------- Active ----------*/}
<FastField name={'active'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true} className={'form-group--active'}>
<Checkbox
inline={true}
label={<T id={'active'} />}
name={'active'}
{...field}
/> />
</FFormGroup> </FormGroup>
)}
{/*----------- SKU ----------*/} </FastField>
<FFormGroup </Box>
name={'code'}
label={<T id={'item_code'} />}
inline={true}
fastField
>
<FInputGroup name={'code'} medium={true} fastField />
</FFormGroup>
{/*----------- Item category ----------*/}
<FFormGroup
name={'category_id'}
label={<T id={'category'} />}
inline={true}
>
<FSelect
name={'category_id'}
items={itemsCategories}
valueAccessor={'id'}
textAccessor={'name'}
placeholder={<T id={'select_category'} />}
popoverProps={{ minimal: true, captureDismiss: true }}
/>
</FFormGroup>
</Col>
<Col xs={3}>
{/* <Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
className={'mt2'}
/> */}
</Col>
</Row>
</div>
); );
} }
@@ -1,246 +0,0 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext, FastField, ErrorMessage } from 'formik';
import { FormGroup, Classes, Checkbox, ControlGroup } from '@blueprintjs/core';
import {
AccountsSelect,
MoneyInputGroup,
FMoneyInputGroup,
Col,
Row,
Hint,
InputPrependText,
FFormGroup,
FTextArea,
} from '@/components';
import { FormattedMessage as T } from '@/components';
import { useItemFormContext } from './ItemFormProvider';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import { ACCOUNT_PARENT_TYPE } from '@/constants/accountTypes';
import {
sellDescriptionFieldShouldUpdate,
sellAccountFieldShouldUpdate,
sellPriceFieldShouldUpdate,
costPriceFieldShouldUpdate,
costAccountFieldShouldUpdate,
purchaseDescFieldShouldUpdate,
taxRateFieldShouldUpdate,
} from './utils';
import { compose, inputIntent } from '@/utils';
import { TaxRatesSelect } from '@/components/TaxRates/TaxRatesSelect';
/**
* Item form body.
*/
function ItemFormBody({ organization: { base_currency } }) {
const { accounts, taxRates } = 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 ------------- */}
<FFormGroup
name={'sell_price'}
label={<T id={'selling_price'} />}
inline
fastField
>
<ControlGroup>
<InputPrependText text={base_currency} />
<FMoneyInputGroup
name={'sell_price'}
shouldUpdate={sellPriceFieldShouldUpdate}
sellable={values.sellable}
inputGroupProps={{ fill: true }}
disabled={!values.sellable}
fastField
/>
</ControlGroup>
</FFormGroup>
{/*------------- Selling account ------------- */}
<FFormGroup
label={<T id={'account'} />}
name={'sell_account_id'}
labelInfo={
<Hint content={<T id={'item.field.sell_account.hint'} />} />
}
inline={true}
items={accounts}
sellable={values.sellable}
shouldUpdate={sellAccountFieldShouldUpdate}
fastField={true}
>
<AccountsSelect
name={'sell_account_id'}
items={accounts}
placeholder={<T id={'select_account'} />}
disabled={!values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
fill={true}
allowCreate={true}
fastField={true}
/>
</FFormGroup>
{/*------------- Sell Tax Rate ------------- */}
<FFormGroup
name={'sell_tax_rate_id'}
label={'Tax Rate'}
inline={true}
>
<TaxRatesSelect
name={'sell_tax_rate_id'}
items={taxRates}
allowCreate
/>
</FFormGroup>
<FFormGroup
name={'sell_description'}
label={<T id={'description'} />}
inline={true}
sellable={values.sellable}
shouldUpdate={sellDescriptionFieldShouldUpdate}
fastField
>
<FTextArea
name={'sell_description'}
growVertically={true}
height={280}
disabled={!values.sellable}
fill
fastField
/>
</FFormGroup>
</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 ------------- */}
<FFormGroup
name={'cost_price'}
label={<T id={'cost_price'} />}
inline
fastField
>
<ControlGroup>
<InputPrependText text={base_currency} />
<FMoneyInputGroup
name={'cost_price'}
shouldUpdate={costPriceFieldShouldUpdate}
purchasable={values.purchasable}
inputGroupProps={{ medium: true }}
disabled={!values.purchasable}
fastField
/>
</ControlGroup>
</FFormGroup>
{/*------------- Cost account ------------- */}
<FFormGroup
name={'cost_account_id'}
purchasable={values.purchasable}
items={accounts}
shouldUpdate={costAccountFieldShouldUpdate}
label={<T id={'account'} />}
labelInfo={
<Hint content={<T id={'item.field.cost_account.hint'} />} />
}
inline={true}
fastField={true}
>
<AccountsSelect
name={'cost_account_id'}
items={accounts}
placeholder={<T id={'select_account'} />}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
allowCreate={true}
fastField={true}
disabled={!values.purchasable}
purchasable={values.purchasable}
shouldUpdate={costAccountFieldShouldUpdate}
/>
</FFormGroup>
{/*------------- Purchase Tax Rate ------------- */}
<FFormGroup
name={'purchase_tax_rate_id'}
label={'Tax Rate'}
inline={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
shouldUpdate={taxRateFieldShouldUpdate}
>
<TaxRatesSelect
name={'purchase_tax_rate_id'}
items={taxRates}
allowCreate={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
/>
</FFormGroup>
<FFormGroup
name={'purchase_description'}
label={<T id={'description'} />}
className={'form-group--purchase-description'}
helperText={<ErrorMessage name={'description'} />}
inline={true}
purchasable={values.purchasable}
shouldUpdate={purchaseDescFieldShouldUpdate}
>
<FTextArea
name={'purchase_description'}
growVertically={true}
height={280}
disabled={!values.purchasable}
fill
/>
</FFormGroup>
</Col>
</Row>
</div>
);
}
export default compose(withCurrentOrganization())(ItemFormBody);
@@ -0,0 +1,44 @@
// @ts-nocheck
import { Tab, Tabs } from "@blueprintjs/core";
import { Card, Group } from "@/components";
import { useState } from "react";
import { css } from '@emotion/css';
import { ItemFormFloatingActions } from "./ItemFormFloatingActions";
import { ItemFormSections } from "./ItemFormFields";
export function ItemFormContent() {
const [selectedTabId, setSelectedTabId] = useState('primary');
const handleTabChange = (tabId) => {
const sectionId = String(tabId);
setSelectedTabId(sectionId);
const section = document.querySelector(
`[data-section-id="${sectionId}"]`,
);
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<Card className={css`padding-bottom: 0 !important;`}>
<Group verticalAlign={'top'} alignItems={'flex-start'} flexWrap={'nowrap'}>
<Tabs
selectedTabId={selectedTabId}
onChange={handleTabChange}
className={css`position: sticky; top: 20px;`}
vertical
>
<Tab id={'primary'} title={'Basic'} />
<Tab id={'selling'} title={'Selling'} />
<Tab id={'purchasing'} title={'Purchasing'} />
<Tab id={'inventory'} title={'Inventory'} />
</Tabs>
<ItemFormSections />
</Group>
<ItemFormFloatingActions />
</Card>
)
}
@@ -0,0 +1,30 @@
// @ts-nocheck
import { Divider } from '@blueprintjs/core';
import { css } from '@emotion/css';
import { Box } from '@/components';
import { ItemFormBasicSection } from './ItemFormBasicSection';
import { ItemFormSellingSection } from './ItemFormSellingSection';
import { ItemFormPurchasingSection } from './ItemFormPurchasingSection';
import { ItemFormInventorySection } from './ItemFormInventorySection';
const itemFormSectionDividerClass = css`
margin: 20px 0;
`;
export function ItemFormSections() {
return (
<Box>
<ItemFormBasicSection />
<Divider className={itemFormSectionDividerClass} />
<ItemFormSellingSection />
<Divider className={itemFormSectionDividerClass} />
<ItemFormPurchasingSection />
<Divider className={itemFormSectionDividerClass} />
<ItemFormInventorySection />
</Box>
);
}
@@ -1,89 +1,83 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import styled from 'styled-components'; import styled from 'styled-components';
import classNames from 'classnames'; import { useFormikContext } from 'formik';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FastField, useFormikContext } from 'formik'; import { Group, Icon, FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
import { Group, FormattedMessage as T } from '@/components';
import { saveInvoke } from '@/utils';
/** /**
* Item form floating actions. * Item form floating actions bar.
*/ */
export default function ItemFormFloatingActions({ onCancel }) { export function ItemFormFloatingActions() {
// Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext();
// Formik context. // Formik context.
const { isSubmitting, submitForm } = useFormikContext(); const { isSubmitting, submitForm } = useFormikContext();
// Handle cancel button click. // Item form context.
const handleCancelBtnClick = (event) => { const { isNewMode, setSubmitPayload } = useItemFormContext();
saveInvoke(onCancel, event);
};
// Handle submit button click. // Handle the submit button.
const handleSubmitBtnClick = (event) => { const handleSubmitBtnClick = () => {
setSubmitPayload({ redirect: true }); setSubmitPayload({ redirect: true });
}; };
// Handle submit & new button click. // Handle the submit & new button click.
const handleSubmitAndNewBtnClick = (event) => { const handleSubmitAndNewClick = () => {
setSubmitPayload({ redirect: false });
submitForm(); submitForm();
setSubmitPayload({ redirect: false });
}; };
return ( return (
<Group <FloatingActionsGroup spacing={10}>
spacing={10} <ButtonGroup>
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)} {/* ----------- Save and New ----------- */}
> <Button
<SaveButton disabled={isSubmitting}
intent={Intent.PRIMARY} loading={isSubmitting}
disabled={isSubmitting} intent={Intent.PRIMARY}
loading={isSubmitting} type="submit"
onClick={handleSubmitBtnClick} onClick={handleSubmitBtnClick}
type="submit" text={!isNewMode ? <T id={'edit'} /> : <T id={'save'} />}
className={'btn--submit'} />
> <Popover
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />} content={
</SaveButton> <Menu>
<MenuItem
<Button text={<T id={'save_and_new'} />}
className={classNames('ml1', 'btn--submit-new')} onClick={handleSubmitAndNewClick}
disabled={isSubmitting} />
onClick={handleSubmitAndNewBtnClick} </Menu>
> }
<T id={'save_new'} /> interactionKind={PopoverInteractionKind.CLICK}
</Button> position={Position.BOTTOM_RIGHT}
minimal
<Button >
disabled={isSubmitting} <Button
className={'ml1'} disabled={isSubmitting}
onClick={handleCancelBtnClick} intent={Intent.PRIMARY}
> rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
<T id={'close'} /> />
</Button> </Popover>
</ButtonGroup>
{/*----------- Active ----------*/} </FloatingActionsGroup>
<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>
</Group>
); );
} }
const SaveButton = styled(Button)` const FloatingActionsGroup = styled(Group)`
min-width: 100px; padding: 10px 0;
padding-left: 165px;
border-top: 1px solid #50555a;
position: sticky;
bottom: 0;
background: var(--color-card-background);
z-index: 1;
`; `;
@@ -4,15 +4,13 @@ import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import classNames from 'classnames';
import styled from 'styled-components';
import '@/style/pages/Items/Form.scss'; import '@/style/pages/Items/Form.scss';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
import ItemFormBody from './ItemFormBody'; import { ItemFormContent } from './ItemFormContent';
import ItemFormPrimarySection from './ItemFormPrimarySection';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import { import {
transformSubmitRequestErrors, transformSubmitRequestErrors,
@@ -37,7 +35,6 @@ export default function ItemFormFormik({
const { const {
itemId, itemId,
item, item,
accounts,
createItemMutate, createItemMutate,
editItemMutate, editItemMutate,
submitPayload, submitPayload,
@@ -91,7 +88,7 @@ export default function ItemFormFormik({
return ( return (
<div <div
class={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_ITEM, className)} className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_ITEM, className)}
> >
<Formik <Formik
enableReinitialize={true} enableReinitialize={true}
@@ -100,15 +97,24 @@ export default function ItemFormFormik({
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
<Form> <Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}> <ItemFormFields>
<ItemFormPrimarySection /> <ItemFormContent />
<ItemFormBody accounts={accounts} /> </ItemFormFields>
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions onCancel={onCancel} />
</Form> </Form>
</Formik> </Formik>
</div> </div>
); );
} }
const ItemFormFields = styled.div`
.bp4-form-content,
.bp6-form-content {
min-width: 300px;
}
.bp4-form-group {
margin-bottom: 20px;
}
.bp4-form-group.bp4-inline label.bp4-label {
min-width: 140px;
}
`;
@@ -4,8 +4,7 @@ import {
AccountsSelect, AccountsSelect,
FFormGroup, FFormGroup,
FormattedMessage as T, FormattedMessage as T,
Col, Box,
Row,
} from '@/components'; } from '@/components';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization'; import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
@@ -13,43 +12,38 @@ import { accountsFieldShouldUpdate } from './utils';
import { ACCOUNT_TYPE } from '@/constants/accountTypes'; import { ACCOUNT_TYPE } from '@/constants/accountTypes';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { ItemFormSectionTitle } from './ItemFormSectionTitle';
/** function ItemFormInventorySectionBase() {
* Item form inventory sections.
*/
function ItemFormInventorySection({ organization: { base_currency } }) {
const { accounts } = useItemFormContext(); const { accounts } = useItemFormContext();
return ( return (
<div class="page-form__section page-form__section--inventory"> <Box data-section-id="inventory">
<h3> <ItemFormSectionTitle>Inventory details</ItemFormSectionTitle>
<T id={'inventory_information'} />
</h3>
<Row> {/*------------- Inventory Account ------------- */}
<Col xs={6}> <FFormGroup
{/*------------- Inventory Account ------------- */} label={<T id={'inventory_account'} />}
<FFormGroup name={'inventory_account_id'}
label={<T id={'inventory_account'} />} items={accounts}
name={'inventory_account_id'} fastField={true}
items={accounts} shouldUpdate={accountsFieldShouldUpdate}
fastField={true} inline={true}
shouldUpdate={accountsFieldShouldUpdate} fill
inline={true} >
> <AccountsSelect
<AccountsSelect name={'inventory_account_id'}
name={'inventory_account_id'} items={accounts}
items={accounts} placeholder={<T id={'select_account'} />}
placeholder={<T id={'select_account'} />} filterByTypes={[ACCOUNT_TYPE.INVENTORY]}
filterByTypes={[ACCOUNT_TYPE.INVENTORY]} fastField={true}
fastField={true} shouldUpdate={accountsFieldShouldUpdate}
shouldUpdate={accountsFieldShouldUpdate} />
/> </FFormGroup>
</FFormGroup> </Box>
</Col>
</Row>
</div>
); );
} }
export default compose(withCurrentOrganization())(ItemFormInventorySection); export const ItemFormInventorySection = compose(withCurrentOrganization())(
ItemFormInventorySectionBase,
);
@@ -0,0 +1,142 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext, FastField, ErrorMessage } from 'formik';
import { FormGroup, Checkbox, ControlGroup } from '@blueprintjs/core';
import {
AccountsSelect,
FMoneyInputGroup,
Hint,
InputPrependText,
FFormGroup,
FTextArea,
Box,
} from '@/components';
import { FormattedMessage as T } from '@/components';
import { useItemFormContext } from './ItemFormProvider';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import { ACCOUNT_PARENT_TYPE } from '@/constants/accountTypes';
import {
costPriceFieldShouldUpdate,
costAccountFieldShouldUpdate,
purchaseDescFieldShouldUpdate,
taxRateFieldShouldUpdate,
} from './utils';
import { compose } from '@/utils';
import { TaxRatesSelect } from '@/components/TaxRates/TaxRatesSelect';
import { ItemFormSectionTitle } from './ItemFormSectionTitle';
function ItemFormPurchasingSectionBase({ organization: { base_currency } }) {
const { accounts, taxRates } = useItemFormContext();
const { values } = useFormikContext();
return (
<Box data-section-id="purchasing">
<ItemFormSectionTitle>Purchasing details</ItemFormSectionTitle>
{/*------------- Purchasable checkbox ------------- */}
<FastField name={'purchasable'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true} className={'form-group--purchasable'}>
<Checkbox
inline={true}
label={<T id={'i_purchase_this_item'} />}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------- Cost price ------------- */}
<FFormGroup
name={'cost_price'}
label={<T id={'cost_price'} />}
inline
fill
fastField
>
<ControlGroup fill>
<InputPrependText text={base_currency} />
<FMoneyInputGroup
name={'cost_price'}
shouldUpdate={costPriceFieldShouldUpdate}
purchasable={values.purchasable}
inputGroupProps={{ medium: true }}
disabled={!values.purchasable}
fastField
/>
</ControlGroup>
</FFormGroup>
{/*------------- Cost account ------------- */}
<FFormGroup
name={'cost_account_id'}
purchasable={values.purchasable}
items={accounts}
shouldUpdate={costAccountFieldShouldUpdate}
label={<T id={'account'} />}
labelInfo={
<Hint content={<T id={'item.field.cost_account.hint'} />} />
}
inline={true}
fill
fastField={true}
>
<AccountsSelect
name={'cost_account_id'}
items={accounts}
placeholder={<T id={'select_account'} />}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
allowCreate={true}
fastField={true}
disabled={!values.purchasable}
purchasable={values.purchasable}
shouldUpdate={costAccountFieldShouldUpdate}
/>
</FFormGroup>
{/*------------- Purchase Tax Rate ------------- */}
<FFormGroup
name={'purchase_tax_rate_id'}
label={'Tax Rate'}
inline={true}
fill
fastField={true}
shouldUpdateDeps={{ taxRates }}
shouldUpdate={taxRateFieldShouldUpdate}
>
<TaxRatesSelect
name={'purchase_tax_rate_id'}
items={taxRates}
allowCreate={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
/>
</FFormGroup>
<FFormGroup
name={'purchase_description'}
label={<T id={'description'} />}
className={'form-group--purchase-description'}
helperText={<ErrorMessage name={'description'} />}
inline={true}
fill
purchasable={values.purchasable}
shouldUpdate={purchaseDescFieldShouldUpdate}
>
<FTextArea
name={'purchase_description'}
growVertically={true}
height={280}
disabled={!values.purchasable}
fill
/>
</FFormGroup>
</Box>
);
}
export const ItemFormPurchasingSection = compose(withCurrentOrganization())(
ItemFormPurchasingSectionBase,
);
@@ -0,0 +1,14 @@
// @ts-nocheck
import React from 'react';
import { css } from '@emotion/css';
const itemFormSectionTitleClass = css`
font-size: 14px;
color: #8f99a8;
margin-bottom: 18px;
margin-top: 10px;
`;
export function ItemFormSectionTitle({ children }: { children: React.ReactNode | string }) {
return <h4 className={itemFormSectionTitleClass}>{children}</h4>;
}
@@ -0,0 +1,135 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext, FastField } from 'formik';
import { FormGroup, Checkbox, ControlGroup } from '@blueprintjs/core';
import {
AccountsSelect,
FMoneyInputGroup,
Hint,
InputPrependText,
FFormGroup,
FTextArea,
Box,
} from '@/components';
import { FormattedMessage as T } from '@/components';
import { useItemFormContext } from './ItemFormProvider';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import { ACCOUNT_PARENT_TYPE } from '@/constants/accountTypes';
import {
sellDescriptionFieldShouldUpdate,
sellAccountFieldShouldUpdate,
sellPriceFieldShouldUpdate,
} from './utils';
import { compose } from '@/utils';
import { TaxRatesSelect } from '@/components/TaxRates/TaxRatesSelect';
import { ItemFormSectionTitle } from './ItemFormSectionTitle';
function ItemFormSellingSectionBase({ organization: { base_currency } }) {
const { accounts, taxRates } = useItemFormContext();
const { values } = useFormikContext();
return (
<Box data-section-id="selling">
<ItemFormSectionTitle>Selling details</ItemFormSectionTitle>
{/*------------- Sellable checkbox ------------- */}
<FastField name={'sellable'} type="checkbox">
{({ form, field }) => (
<FormGroup inline={true} className={'form-group--sellable'}>
<Checkbox
inline={true}
label={<T id={'i_sell_this_item'} />}
name={'sellable'}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------- Selling price ------------- */}
<FFormGroup
name={'sell_price'}
label={<T id={'selling_price'} />}
inline
fill
fastField
>
<ControlGroup fill>
<InputPrependText text={base_currency} />
<FMoneyInputGroup
name={'sell_price'}
shouldUpdate={sellPriceFieldShouldUpdate}
sellable={values.sellable}
inputGroupProps={{ fill: true }}
disabled={!values.sellable}
fastField
/>
</ControlGroup>
</FFormGroup>
{/*------------- Selling account ------------- */}
<FFormGroup
label={<T id={'account'} />}
name={'sell_account_id'}
labelInfo={
<Hint content={<T id={'item.field.sell_account.hint'} />} />
}
inline={true}
fill
items={accounts}
sellable={values.sellable}
shouldUpdate={sellAccountFieldShouldUpdate}
fastField={true}
>
<AccountsSelect
name={'sell_account_id'}
items={accounts}
placeholder={<T id={'select_account'} />}
disabled={!values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
fill={true}
allowCreate={true}
fastField={true}
/>
</FFormGroup>
{/*------------- Sell Tax Rate ------------- */}
<FFormGroup
name={'sell_tax_rate_id'}
label={'Tax Rate'}
inline={true}
fill
>
<TaxRatesSelect
name={'sell_tax_rate_id'}
items={taxRates}
allowCreate
/>
</FFormGroup>
<FFormGroup
name={'sell_description'}
label={<T id={'description'} />}
inline={true}
fill
sellable={values.sellable}
shouldUpdate={sellDescriptionFieldShouldUpdate}
fastField
>
<FTextArea
name={'sell_description'}
growVertically={true}
height={280}
disabled={!values.sellable}
fill
fastField
/>
</FFormGroup>
</Box>
);
}
export const ItemFormSellingSection = compose(withCurrentOrganization())(
ItemFormSellingSectionBase,
);
+14 -75
View File
@@ -1,91 +1,30 @@
.page-form--item { .page-form--item {
$self: '.page-form'; $self: '.page-form';
padding: 20px; padding: 20px;
--x-header-border: #eaeaea; .bp4-form-group {
--x-section-border: #eaeaea; max-width: 500px;
margin-bottom: 14px;
.bp4-dark & { &.bp4-inline {
--x-header-border: var(--color-dark-gray3); .bp4-label {
--x-section-border: var(--color-dark-gray3); min-width: 140px;
}
#{$self}__header {
padding: 0;
}
#{$self}__primary-section {
overflow: hidden;
padding-top: 5px;
margin-bottom: 20px;
border-bottom: 1px solid var(--x-header-border);
padding-bottom: 5px;
max-width: 1000px;
}
#{$self}__body {
.bp4-form-group {
max-width: 500px;
margin-bottom: 14px;
&.bp4-inline {
.bp4-label {
min-width: 140px;
}
}
.bp4-form-content {
width: 100%;
} }
} }
.bp4-form-content {
h3 { width: 100%;
font-weight: 500;
font-size: 14px;
margin-bottom: 1.4rem;
}
.bp4-control {
h3 {
display: inline;
margin-bottom: 0;
}
}
.form-group--sellable,
.form-group--purchasable {
margin-bottom: 1rem;
}
.form-group--purchase-description,
.form-group--sell-description{
textarea{
width: 100%;
}
} }
} }
#{$self}__section { .form-group--sellable,
max-width: 850px; .form-group--purchasable {
margin-bottom: 1rem; margin-bottom: 1rem;
.bp4-form-group {
max-width: 400px;
}
&--selling-cost {
border-bottom: 1px solid var(--x-section-border);
margin-bottom: 1.25rem;
padding-bottom: 0.25rem;
}
} }
#{$self}__floating-actions { .form-group--purchase-description,
.form-group--sell-description {
.form-group--active { textarea {
display: inline-block; width: 100%;
margin: 0;
margin-left: 20px;
} }
} }